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 50b63ab..f8071dc 100644 --- a/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt +++ b/app/src/main/kotlin/com/example/myapplication/navigation/AppNavHost.kt @@ -77,7 +77,9 @@ fun AppNavHost( if (currentEntry?.destination?.route == AppRoute.MediaDetail.pattern && currentEntry.arguments?.getString("itemId") == itemId ) return@let - navController.navigate(AppRoute.MediaDetail.create(serverId, itemId)) + navController.navigate(AppRoute.MediaDetail.create(serverId, itemId)) { + launchSingleTop = true + } } } @@ -180,7 +182,9 @@ fun AppNavHost( val alreadyPlaying = currentEntry?.destination?.route == AppRoute.VideoPlayer.pattern && currentEntry.arguments?.getString("itemId") == itemId if (!alreadyPlaying) { - navController.navigate(AppRoute.VideoPlayer.create(serverId, itemId, seriesId, seasonId)) + navController.navigate(AppRoute.VideoPlayer.create(serverId, itemId, seriesId, seasonId)) { + launchSingleTop = true + } } }, ) diff --git a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt index 8d877cf..a9f3cfc 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/HomeScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalTextStyle @@ -584,6 +585,7 @@ fun HomeScreen( val heroItems = remember(latestItems) { latestItems.take(8) } val heroPagerState = rememberPagerState(pageCount = { heroItems.size }) + val homeListState = rememberLazyListState() val activeHeroItem = heroItems.getOrNull(heroPagerState.currentPage) val immersiveBackground = rememberPosterImmersiveBackground(heroImageUrl(activeHeroItem, activeServer?.fullUrl.orEmpty())) @@ -690,7 +692,8 @@ fun HomeScreen( } else { val server = activeServer LazyColumn( - Modifier.fillMaxSize(), + state = homeListState, + modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = bottomBarContentPadding(90.dp).calculateBottomPadding()) ) { if (heroItems.isNotEmpty()) { diff --git a/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt b/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt index af586a2..1c390ee 100644 --- a/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt +++ b/app/src/main/kotlin/com/example/myapplication/screens/MediaDetailScreen.kt @@ -80,6 +80,7 @@ import com.example.myapplication.data.EmbyServer import com.example.myapplication.R as Res import com.kyant.backdrop.Backdrop import top.yukonga.miuix.kmp.basic.Text +import kotlinx.coroutines.CancellationException import java.util.Locale private val LocalDetailText = staticCompositionLocalOf { Color.White } @@ -117,22 +118,32 @@ fun MediaDetailScreen( LaunchedEffect(item.id, server?.id, server?.apiKey) { if (server == null || server.apiKey.isBlank()) return@LaunchedEffect refreshing = true - val fullItem = EmbyRepository.getItem(server, item.id) - if (fullItem == null) { - loadError = detailLoadFailed - } else { - detail = fullItem - loadError = null - } - refreshing = false + try { + val fullItem = EmbyRepository.getItem(server, item.id) + if (fullItem == null) { + loadError = detailLoadFailed + } else { + detail = fullItem + loadError = null + } - if (detail.type == "Series") { - val loadedSeasons = EmbyRepository.getSeasons(server, item.id) - seasons = loadedSeasons - selectedSeasonId = loadedSeasons.firstOrNull()?.id - } else { - seasons = emptyList() - selectedSeasonId = null + if (detail.type == "Series") { + val loadedSeasons = EmbyRepository.getSeasons(server, item.id) + seasons = loadedSeasons + selectedSeasonId = loadedSeasons + .firstOrNull { it.id == selectedSeasonId } + ?.id + ?: loadedSeasons.firstOrNull()?.id + } else { + seasons = emptyList() + selectedSeasonId = null + } + } catch (error: CancellationException) { + throw error + } catch (_: Exception) { + loadError = detailLoadFailed + } finally { + refreshing = false } } @@ -141,7 +152,13 @@ fun MediaDetailScreen( val seriesId = item.id val seasonId = selectedSeasonId ?: return@LaunchedEffect if (detail.type != "Series" || seasonId.isBlank()) return@LaunchedEffect - episodes = EmbyRepository.getEpisodes(currentServer, seriesId, seasonId) + episodes = try { + EmbyRepository.getEpisodes(currentServer, seriesId, seasonId) + } catch (error: CancellationException) { + throw error + } catch (_: Exception) { + emptyList() + } } val seriesFallbackImageUrl = EmbyRepository.buildImageUrl(server?.fullUrl.orEmpty(), detail, maxWidth = 640) diff --git a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaDesignSystem.kt b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaDesignSystem.kt index ec6964d..2331498 100644 --- a/app/src/main/kotlin/com/example/myapplication/ui/design/LumaDesignSystem.kt +++ b/app/src/main/kotlin/com/example/myapplication/ui/design/LumaDesignSystem.kt @@ -224,8 +224,11 @@ fun Modifier.lumaFallbackGlass( endY = size.height * 0.72f, ) onDrawWithContent { - drawContent() + // The fallback surface is a material layer. Its highlight must + // sit behind the content; drawing it after drawContent makes + // labels and icons look washed out on devices without Backdrop. drawRect(highlight) + drawContent() } } .then(if (border) Modifier.border(0.6.dp, edge, shape) else Modifier) diff --git a/memory/2026-08-28.md b/memory/2026-08-28.md index 823d945..d215420 100644 --- a/memory/2026-08-28.md +++ b/memory/2026-08-28.md @@ -3,3 +3,9 @@ 文件变更: MainActivity.kt、AppTheme.kt、LumaDesignSystem.kt、LiquidBottomTabs.kt、AppBottomBar.kt、SettingsScreen.kt、strings.xml、values-en/strings.xml 及既有 UI 收尾文件。 教训: 远端有新提交时先暂存跟踪文件再合并;模式手动选择与模拟器安全硬门需同时保留。 标签: #lumaplayer #fallback-ui #glass-mode #renderthread #remote-merge + +[PROJECT:lumaplayer] 页面状态与导航收尾 +结论: 修复降级高光覆盖、首页滚动恢复及详情/播放器重复导航与异常态,构建和 API35 启动回归成功。 +文件变更: ui/design/LumaDesignSystem.kt、screens/HomeScreen.kt、navigation/AppNavHost.kt、screens/MediaDetailScreen.kt。 +教训: 页面请求异常需保留取消异常;可保存滚动状态和 launchSingleTop 能减少返回闪烁与重复页面。 +标签: #lumaplayer #navigation #state-restoration #fallback-ui diff --git a/memory/MEMORY.md b/memory/MEMORY.md index 6af2ef9..2449cbc 100644 --- a/memory/MEMORY.md +++ b/memory/MEMORY.md @@ -10,3 +10,4 @@ - 详细状态见 [projects.md](projects.md),踩坑见 [lessons.md](lessons.md)。 - 最新收尾:公共分段控件、按钮、输入框、主题/语言/资源库弹层统一走同一套降级毛玻璃;底栏降级选中胶囊改为内容下层绘制;新增玻璃模式持久化设置;状态栏/导航栏图标随深色主题同步;Debug/Lint/Unit 与 API35 模拟器回归通过。 - 最新改动:播放器进度条改为自绘可访问 SeekBar,媒体首页、Vision 收藏和搜索加载态改为统一骨架占位;移除未使用的 Material 图标依赖,完整 Debug/Lint/Unit 通过。 +- 最新校验:修复降级玻璃高光覆盖内容、首页滚动状态恢复与重复详情/播放器导航;Debug/Lint/Unit 和 API35 模拟器启动回归成功。 diff --git a/memory/lessons.md b/memory/lessons.md index 45827f9..5c0927b 100644 --- a/memory/lessons.md +++ b/memory/lessons.md @@ -19,3 +19,4 @@ - 中:Emby 播放回报必须沿用 PlaybackInfo 返回的 `PlaySessionId`/`MediaSourceId`,并将开始、进度、停止事件串行化;把回报协程绑定到播放器 Composable 生命周期会在返回页面时丢失停止事件。 - 高:液态玻璃不能以“API 版本高”作为唯一启用条件;Kyant Backdrop 还需通过设备安全门和运行帧率门控,低帧率/不安全 GPU 必须在创建 LayerBackdrop 前降级,避免 RenderThread 崩溃。 - 高:玻璃模式已改为用户手动选择,不能再用 Choreographer 帧率变化自动切换;但模拟器/低版本仍必须在创建 Kyant LayerBackdrop 前保留硬件安全门,避免 RenderThread SIGSEGV。 +- 中:降级玻璃的边缘高光应在 `drawContent()` 之前绘制;否则公共玻璃容器中的文字和图标会被高光层洗白。 diff --git a/memory/projects.md b/memory/projects.md index 564f5dc..47c4872 100644 --- a/memory/projects.md +++ b/memory/projects.md @@ -37,3 +37,4 @@ - 2026-08-27:播放器进度条替换为自绘拖拽控件并补充无障碍进度调节,Home/Vision/Search 的媒体加载态改为统一骨架占位;移除未使用的 Material 图标依赖。`assembleDebug`、`lintDebug`、`testDebugUnitTest` 成功。 - 2026-08-27:底栏图标调整为 36dp、搜索图标 41dp,资源库无图标服务器使用正确的服务器图标;搜索清除与图标库返回补足 48dp 触控范围。完整 Debug/Lint/Unit 构建和 API35 模拟器回归通过。 - 2026-08-28:完整读取本地 `luna/README.txt`、`AI_PROMPT.txt` 与降级参考 contact sheet;修复降级底栏选中胶囊覆盖内容的问题,统一为稳定渐变毛玻璃;移除 Choreographer 帧率自动切换,设置页新增液态玻璃/毛玻璃手动选择并持久化;合并远端 3 个播放器提交后 `assembleDebug`、`lintDebug` 与 API35 模拟器启动回归通过。 +- 2026-08-28:继续收尾降级 UI;修复 fallback 高光绘制顺序,首页接入可保存滚动状态,详情/播放器导航启用 `launchSingleTop` 防重复 Push,详情刷新和剧集加载异常进入已有错误态;`assembleDebug`、`lintDebug`、`testDebugUnitTest` 及 API35 模拟器启动回归成功。