From 3a01b42aeb4d1b8bfcd01ab34adeed5de709499b Mon Sep 17 00:00:00 2001
From: beyond
Date: Sun, 6 Sep 2026 17:06:37 +0800
Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E8=A1=A5=E5=85=A8:=20?=
=?UTF-8?q?=E9=94=99=E8=AF=AF=E9=87=8D=E8=AF=95=E3=80=81=E7=A1=AE=E8=AE=A4?=
=?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E3=80=81TMDB=E9=94=99=E8=AF=AF?=
=?UTF-8?q?=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- AppNavHost: 详情页加载失败显示LumaErrorState+重试按钮
- SearchScreen: 搜索历史清除添加GlassConfirmDialog确认对话框
- SearchAllScreen: 修复重试机制(添加retryToken触发重新搜索)
- VideoPlayerScreen: 播放错误添加重试按钮(re-prepare+play)
- RecordsScreen: 日历TMDB API失败显示错误状态而非静默空白
Co-Authored-By: Claude Opus 5 (1M context)
---
.../myapplication/navigation/AppNavHost.kt | 14 +++++++++-
.../myapplication/screens/RecordsScreen.kt | 15 +++++++++--
.../myapplication/screens/SearchAllScreen.kt | 6 +++--
.../myapplication/screens/SearchScreen.kt | 23 +++++++++++++---
.../screens/VideoPlayerScreen.kt | 27 +++++++++++++++----
5 files changed, 71 insertions(+), 14 deletions(-)
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,
+ )
+ }
+ }
}
}
}