diff --git a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt index 4aec9e1..3e0a99d 100644 --- a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt +++ b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt @@ -10,6 +10,7 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -48,6 +49,7 @@ import com.example.myapplication.screens.SearchTopBar import com.example.myapplication.screens.SettingsScreen import com.example.myapplication.screens.SettingsTopBar import com.example.myapplication.screens.VideoPlayerScreen +import com.example.myapplication.ui.design.LumaErrorState import com.example.myapplication.ui.design.LumaLoading import com.example.myapplication.ui.design.LumaColors import com.example.myapplication.ui.design.rememberLumaLayerBackdrop @@ -370,9 +372,12 @@ private fun MediaDetailDestination( ) { var item by remember(server?.id, itemId) { mutableStateOf(null) } var loading by remember(server?.id, itemId) { mutableStateOf(true) } + var loadError by remember(server?.id, itemId) { mutableStateOf(false) } + var retryToken by remember(server?.id, itemId) { mutableIntStateOf(0) } - LaunchedEffect(server?.id, itemId) { + LaunchedEffect(server?.id, itemId, retryToken) { loading = true + loadError = false item = try { if (server == null || server.apiKey.isBlank() || itemId.isBlank()) { null @@ -382,6 +387,7 @@ private fun MediaDetailDestination( } catch (error: CancellationException) { throw error } catch (_: Exception) { + loadError = true null } loading = false @@ -390,6 +396,12 @@ private fun MediaDetailDestination( when { item != null -> MediaDetailScreen(item = item!!, server = server, servers = servers, onClose = onClose, onPlay = onPlay) loading -> DestinationLoading() + loadError -> LumaErrorState( + title = stringResource(Res.string.detail_load_failed), + description = stringResource(Res.string.error_content_sub), + retryLabel = stringResource(Res.string.retry), + onRetry = { retryToken++ }, + ) else -> DestinationMessage(stringResource(Res.string.detail_load_failed), background = Color.Transparent) } } diff --git a/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt index 79929df..64b351c 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/RecordsScreen.kt @@ -444,6 +444,7 @@ private fun CalendarTab(onEpisodeClick: (String, String) -> Unit = { _, _ -> }) var loading by remember { mutableStateOf(true) } var selectedDate by remember { mutableStateOf(null) } var weekOffset by remember { mutableIntStateOf(0) } + var calendarError by remember { mutableStateOf(false) } // P1: Key on watchlist size so calendar re-fetches when watchlist changes var watchlistVersion by remember { mutableIntStateOf(0) } @@ -455,9 +456,13 @@ private fun CalendarTab(onEpisodeClick: (String, String) -> Unit = { _, _ -> }) loading = false return@LaunchedEffect } + calendarError = false calendarEpisodes = try { TmdbRepository.getCalendarEpisodes(watchlist) - } catch (_: Exception) { emptyList() } + } catch (_: Exception) { + calendarError = true + emptyList() + } loading = false } @@ -495,8 +500,14 @@ private fun CalendarTab(onEpisodeClick: (String, String) -> Unit = { _, _ -> }) Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { LumaLoading(label = stringResource(Res.string.loading_search)) } + } else if (calendarError) { + LumaEmptyStateContainer( + icon = LumaIcon.Calendar, + title = stringResource(Res.string.error_content_title), + description = stringResource(Res.string.empty_calendar_sub), + verticalOffset = (-60).dp, + ) } else { - // P2: Remember filtered + grouped results to avoid recomputation on unrelated state changes val filteredEpisodes = remember(calendarEpisodes, selectedDate, today) { filterCalendarEpisodes(calendarEpisodes, selectedDate, today) } diff --git a/app/src/main/kotlin/com/example/myapplication/screens/SearchAllScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/SearchAllScreen.kt index 572686a..7bcdc87 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/SearchAllScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/SearchAllScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -68,8 +69,9 @@ fun SearchAllScreen( var results by remember(serverId, initialQuery) { mutableStateOf>(emptyList()) } var loading by remember(serverId, initialQuery) { mutableStateOf(true) } var error by remember(serverId, initialQuery) { mutableStateOf(false) } + var retryToken by remember(serverId, initialQuery) { mutableIntStateOf(0) } - androidx.compose.runtime.LaunchedEffect(serverId, initialQuery) { + androidx.compose.runtime.LaunchedEffect(serverId, initialQuery, retryToken) { val term = initialQuery.trim() if (server == null || server.apiKey.isBlank() || term.isBlank()) { results = emptyList() @@ -184,7 +186,7 @@ fun SearchAllScreen( title = stringResource(Res.string.error_content_title), description = stringResource(Res.string.error_content_sub), retryLabel = stringResource(Res.string.retry), - onRetry = { loading = true }, + onRetry = { retryToken++ }, ) } } diff --git a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt index af926e6..7d6f984 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/SearchScreen.kt @@ -58,6 +58,7 @@ import com.example.myapplication.data.EmbyServer import com.example.myapplication.ui.GlassSearchField import com.example.myapplication.ui.GlassTokens import com.example.myapplication.ui.LiquidGlassTopBar +import com.example.myapplication.ui.GlassConfirmDialog import com.example.myapplication.ui.LumaEmptyStateContainer import com.example.myapplication.ui.bottomBarContentPadding import com.example.myapplication.ui.design.LumaColors @@ -193,6 +194,7 @@ fun SearchScreen( val completedIds = remember { mutableSetOf() } var searching by remember { mutableStateOf(false) } var historyVersion by remember { mutableIntStateOf(0) } + var showClearHistoryConfirm by remember { mutableStateOf(false) } var lastSearchedQuery by remember { mutableStateOf("") } // Stabilize LaunchedEffect key — use only server IDs, not list reference @@ -348,10 +350,7 @@ fun SearchScreen( query.isBlank() && history.isNotEmpty() -> SearchHistoryList( history = history, onSelect = { onQueryChange(it) }, - onClear = { - clearSearchHistory(context) - historyVersion++ - }, + onClear = { showClearHistoryConfirm = true }, ) // ★ KEY FIX: searching = true always shows loading, never error/empty // This prevents the flash of error/empty state during transitions @@ -387,6 +386,22 @@ fun SearchScreen( } } } + + // Clear history confirmation dialog + if (showClearHistoryConfirm) { + GlassConfirmDialog( + title = stringResource(Res.string.search_history_clear), + message = stringResource(Res.string.search_history_clear), + confirmText = stringResource(Res.string.confirm_delete), + dismissText = stringResource(Res.string.confirm_cancel), + onConfirm = { + clearSearchHistory(context) + historyVersion++ + showClearHistoryConfirm = false + }, + onDismissRequest = { showClearHistoryConfirm = false }, + ) + } } // ─── Centered Loading ─────────────────────────────────────────────────────── diff --git a/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt index fd85479..07d6271 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/VideoPlayerScreen.kt @@ -587,11 +587,28 @@ fun VideoPlayerScreen( if (playerError != null) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text( - text = playerError ?: stringResource(Res.string.player_failed), - color = Color.White, - modifier = Modifier.padding(24.dp), - ) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = playerError ?: stringResource(Res.string.player_failed), + color = Color.White, + modifier = Modifier.padding(24.dp), + ) + androidx.compose.material3.TextButton( + onClick = { + playerError = null + playbackUrl?.let { url -> + player.setMediaItem(androidx.media3.common.MediaItem.fromUri(url)) + player.prepare() + player.play() + } + }, + ) { + Text( + text = stringResource(Res.string.retry), + color = LumaColors.accent, + ) + } + } } } }