From 47109bab4e4fdbf6596409bd283f964a8966c710 Mon Sep 17 00:00:00 2001 From: wdw89 Date: Sun, 21 Jun 2026 22:31:31 +0800 Subject: [PATCH 01/41] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0TV=E9=81=A5?= =?UTF-8?q?=E6=8E=A7=E5=99=A8D-pad=E7=84=A6=E7=82=B9=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MediaSmall: Card(onClick) + focusGroup() + border焦点边框 - FavouriteScreen: 触摸长按(LaunchedEffect) + D-pad长按(Handler) - HomeScreen: Tab改用Surface + LazyVerticalGrid添加focusGroup() - VideoPlayerScreen: D-pad控制改用KeyUp,左右键隐藏时先展示UI - VideoPlayerControl: 添加modifier和sliderFocusRequester参数 - Slider: 添加focusRequester参数 --- .../presentation/component/MediaSmall.kt | 95 +++++++++++++++---- .../screen/favourite/FavouriteScreen.kt | 90 ++++++++++++++++-- .../presentation/screen/home/HomeScreen.kt | 33 +++---- .../screen/videoplayer/VideoPlayerScreen.kt | 42 +++++--- .../videoplayer/VideoPlayerControl.kt | 29 +++--- .../lanlinju/videoplayer/component/Slider.kt | 4 +- 6 files changed, 221 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt b/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt index 20965882..7253293e 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt @@ -2,8 +2,8 @@ package com.lanlinju.animius.presentation.component import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.border +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -14,17 +14,27 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest import com.lanlinju.animius.util.CROSSFADE_DURATION @@ -41,6 +51,7 @@ fun MediaSmallRow( content: @Composable (T) -> Unit ) { LazyRow( + modifier = Modifier.focusGroup(), horizontalArrangement = Arrangement.spacedBy(dimensionResource(Res.dimen.small_padding)), contentPadding = PaddingValues( start = dimensionResource(Res.dimen.large_padding), @@ -60,29 +71,81 @@ fun MediaSmallRow( * @param label A label for the [image], if this is `null`, the [label] is not shown. * @param onClick Action to happen when the card is clicked. */ -@OptIn(ExperimentalFoundationApi::class) @Composable fun MediaSmall( image: String?, onClick: () -> Unit, - onLongClick: (() -> Unit)? = null, modifier: Modifier = Modifier, enabled: Boolean = true, label: String? = null, onSuccess: (Bitmap) -> Unit = { } ) { - MediaSmall( + var isFocused by remember { mutableStateOf(false) } + val cardShape = RoundedCornerShape(dimensionResource(Res.dimen.media_card_corner_radius)) + + Card( + onClick = onClick, + enabled = enabled, modifier = modifier - .clip(RoundedCornerShape(dimensionResource(Res.dimen.media_card_corner_radius))) - .combinedClickable( - enabled = enabled, - onClick = onClick, - onLongClick = onLongClick + .onFocusChanged { isFocused = it.isFocused } + .border( + width = if (isFocused) 3.dp else 0.dp, + color = if (isFocused) Color.White else Color.Transparent, + shape = cardShape ), - image = image, - label = label, - onSuccess = onSuccess - ) + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + shape = cardShape, + ) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(image) + .crossfade(CROSSFADE_DURATION) + .build(), + contentDescription = label, + contentScale = ContentScale.Crop, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(0.7f) + .clip(cardShape), + onSuccess = { + val bitmap = (it.result.drawable as BitmapDrawable).bitmap + onSuccess(bitmap) + } + ) + + if (label != null) + Box { + Text( + text = " \n ", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + maxLines = 2, + modifier = Modifier.padding( + vertical = dimensionResource(Res.dimen.media_card_text_padding_vertical) + ) + ) + + Text( + text = label, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.labelLarge, + maxLines = 2, + // TODO: Add a custom overflow indicator: + // https://proandroiddev.com/detect-text-overflow-in-jetpack-compose-56c0b83da5a5. + overflow = TextOverflow.Visible, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth() + .padding( + horizontal = dimensionResource(Res.dimen.media_card_text_padding_horizontal), + vertical = dimensionResource(Res.dimen.media_card_text_padding_vertical) + ) + ) + } + } } @Composable @@ -160,4 +223,4 @@ fun PreviewMediaSmall() { onClick = { }, modifier = Modifier.width(dimensionResource(Res.dimen.media_card_width)) ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/favourite/FavouriteScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/favourite/FavouriteScreen.kt index ef6f5c3f..608ed497 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/favourite/FavouriteScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/favourite/FavouriteScreen.kt @@ -1,5 +1,8 @@ package com.lanlinju.animius.presentation.screen.favourite +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -20,6 +23,10 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,8 +34,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset @@ -41,6 +55,7 @@ import com.lanlinju.animius.presentation.component.SourceBadge import com.lanlinju.animius.presentation.component.StateHandler import com.lanlinju.animius.util.SourceMode import com.lanlinju.animius.util.isWideScreen +import kotlinx.coroutines.delay @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -73,7 +88,8 @@ fun FavouriteScreen( LazyVerticalGrid( modifier = Modifier - .padding(paddingValues), + .padding(paddingValues) + .focusGroup(), columns = if (isWideScreen(context)) GridCells.Adaptive(dimensionResource(R.dimen.min_media_card_width)) else GridCells.Fixed( 3 ), @@ -85,7 +101,25 @@ fun FavouriteScreen( items(favouriteList) { anime -> var expanded by remember { mutableStateOf(false) } + var longPressConsumed by remember { mutableStateOf(false) } + var touchPressed by remember { mutableStateOf(false) } val haptic = LocalHapticFeedback.current + val longPressTimeout = LocalViewConfiguration.current.longPressTimeoutMillis + val handler = remember { Handler(Looper.getMainLooper()) } + + DisposableEffect(Unit) { + onDispose { handler.removeCallbacksAndMessages(null) } + } + + LaunchedEffect(touchPressed) { + if (touchPressed) { + delay(longPressTimeout) + longPressConsumed = true + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + expanded = true + touchPressed = false + } + } Box { SourceBadge( @@ -97,15 +131,51 @@ fun FavouriteScreen( image = anime.imgUrl, label = anime.title, onClick = { - onNavigateToAnimeDetail( - anime.detailUrl, - anime.sourceMode - ) + if (!longPressConsumed) { + onNavigateToAnimeDetail( + anime.detailUrl, + anime.sourceMode + ) + } + longPressConsumed = false }, - onLongClick = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - expanded = true - } + modifier = Modifier + .onPreviewKeyEvent { event -> + when { + event.key == Key.DirectionCenter && event.type == KeyEventType.KeyDown -> { + handler.postDelayed({ + longPressConsumed = true + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + expanded = true + }, longPressTimeout) + true + } + event.key == Key.DirectionCenter && event.type == KeyEventType.KeyUp -> { + handler.removeCallbacksAndMessages(null) + if (!longPressConsumed) { + onNavigateToAnimeDetail( + anime.detailUrl, + anime.sourceMode + ) + } + longPressConsumed = false + true + } + else -> false + } + } + .pointerInput(Unit) { + awaitEachGesture { + awaitFirstDown(requireUnconsumed = false) + touchPressed = true + while (touchPressed) { + val event = awaitPointerEvent() + if (event.changes.all { !it.pressed }) { + touchPressed = false + } + } + } + } ) } @@ -139,4 +209,4 @@ fun FavouriteScreen( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt index 84c0136b..31ca6f1d 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -41,6 +42,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryScrollableTabRow +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -243,7 +245,7 @@ private fun GridLayoutTabs( GridCells.Fixed(3) } LazyVerticalGrid( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxSize().focusGroup(), columns = columns, verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -268,22 +270,21 @@ private fun Tab( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - Box( - modifier = modifier - .padding(8.dp) - .background( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), - shape = RoundedCornerShape(11.dp) - ) - .clip(RoundedCornerShape(11.dp)) - .clickable { onClick() } - .padding(horizontal = 8.dp, vertical = 4.dp) + Surface( + onClick = onClick, + modifier = modifier.padding(8.dp), + shape = RoundedCornerShape(11.dp), + color = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), + contentColor = if (selected) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.onBackground, ) { - Text( - text = text(), -// color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onBackground, - modifier = Modifier.align(Alignment.Center) - ) + Box( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = text(), + modifier = Modifier.align(Alignment.Center) + ) + } } } diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/videoplayer/VideoPlayerScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/videoplayer/VideoPlayerScreen.kt index d8b276f1..ec73fe0f 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/videoplayer/VideoPlayerScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/videoplayer/VideoPlayerScreen.kt @@ -199,6 +199,8 @@ fun VideoPlayScreen( ) { // Video player composable + val sliderFocusRequester = remember { FocusRequester() } + VideoPlayer( url = video.url, videoPosition = video.lastPlayPosition, @@ -209,9 +211,13 @@ fun VideoPlayScreen( .focusable() .defaultRemoteControlHandler( playerState = playerState, - onNextClick = { viewModel.playNextEpisode(playerState.player.currentPosition) } + onNextClick = { viewModel.playNextEpisode(playerState.player.currentPosition) }, + onSliderFocusRequest = { sliderFocusRequester.requestFocus() } ) ) { + val controlFocusRequester = remember { FocusRequester() } + val isAndroidTV = remember { isAndroidTV(activity) } + VideoPlayerControl( state = playerState, title = "${video.title}-${video.episodeName}", @@ -230,7 +236,9 @@ fun VideoPlayScreen( onForwardClick = { playerState.control.skip(85000) } ) }, - onDanmakuClick = { viewModel.setEnabledDanmaku(it) } + onDanmakuClick = { viewModel.setEnabledDanmaku(it) }, + modifier = if (isAndroidTV) Modifier.focusRequester(controlFocusRequester) else Modifier, + sliderFocusRequester = sliderFocusRequester ) } @@ -379,38 +387,44 @@ private fun DanmakuHost( private fun Modifier.defaultRemoteControlHandler( playerState: VideoPlayerState, onNextClick: () -> Unit = {}, + onSliderFocusRequest: () -> Unit = {}, ) = onKeyEvent { keyEvent: KeyEvent -> - if (keyEvent.type == KeyEventType.KeyDown) + if (keyEvent.type == KeyEventType.KeyUp) when (keyEvent.key) { Key.DirectionLeft -> { - playerState.showControlUi() - playerState.control.rewind() + if (!playerState.isControlUiVisible.value) { + playerState.showControlUi() + onSliderFocusRequest() + } true } Key.DirectionRight -> { - playerState.showControlUi() - playerState.control.forward() + if (!playerState.isControlUiVisible.value) { + playerState.showControlUi() + onSliderFocusRequest() + } true } Key.DirectionUp -> { - playerState.showEpisodeUi() + playerState.showControlUi() true } Key.DirectionDown -> { playerState.showControlUi() - onNextClick() true } Key.DirectionCenter, Key.Spacebar -> { - if (playerState.isPlaying.value) { - playerState.showControlUi() - playerState.control.pause() - } else { - playerState.control.play() + if (!playerState.isControlUiVisible.value) { + if (playerState.isPlaying.value) { + playerState.showControlUi() + playerState.control.pause() + } else { + playerState.control.play() + } } true } diff --git a/video-player/src/main/java/com/lanlinju/videoplayer/VideoPlayerControl.kt b/video-player/src/main/java/com/lanlinju/videoplayer/VideoPlayerControl.kt index c18a6337..ac6fdc41 100644 --- a/video-player/src/main/java/com/lanlinju/videoplayer/VideoPlayerControl.kt +++ b/video-player/src/main/java/com/lanlinju/videoplayer/VideoPlayerControl.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.unit.dp import com.lanlinju.videoplayer.icons.Fullscreen import com.lanlinju.videoplayer.icons.FullscreenExit @@ -59,10 +60,12 @@ fun VideoPlayerControl( onNextClick: () -> Unit = {}, onDanmakuClick: (Boolean) -> Unit = {}, optionsContent: (@Composable () -> Unit)? = null, + modifier: Modifier = Modifier, + sliderFocusRequester: FocusRequester = remember { FocusRequester() }, ) { CompositionLocalProvider(LocalContentColor provides contentColor) { Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(background) .padding( @@ -93,7 +96,8 @@ fun VideoPlayerControl( state = state, enabledDanmaku = danmakuEnabled, onNextClick = onNextClick, - onDanmakuClick = onDanmakuClick + onDanmakuClick = onDanmakuClick, + sliderFocusRequester = sliderFocusRequester, ) } } @@ -154,7 +158,8 @@ private fun BottomControlBar( state: VideoPlayerState, enabledDanmaku: Boolean, onNextClick: () -> Unit, - onDanmakuClick: (Boolean) -> Unit + onDanmakuClick: (Boolean) -> Unit, + sliderFocusRequester: FocusRequester, ) { val timestamp = remember( @@ -187,6 +192,7 @@ private fun BottomControlBar( .height(30.dp), isSeeking = state.isSeeking.value, color = progressLineColor, + focusRequester = sliderFocusRequester, ) if (!state.isSeeking.value) { @@ -352,18 +358,11 @@ private fun AdaptiveIconButton( enabledIndication: Boolean = true, content: @Composable () -> Unit ) { - val indication = LocalIndication.current - - Box( - modifier = modifier - .clip(CircleShape) - .clickable( - onClick = onClick, - enabled = enabled, - interactionSource = interactionSource, - indication = if (enabledIndication) indication else null - ), - contentAlignment = Alignment.Center + IconButton( + onClick = onClick, + modifier = modifier.size(MediumIconButtonSize), + enabled = enabled, + interactionSource = interactionSource, ) { content() } diff --git a/video-player/src/main/java/com/lanlinju/videoplayer/component/Slider.kt b/video-player/src/main/java/com/lanlinju/videoplayer/component/Slider.kt index ffc6a0f4..4cd3579b 100644 --- a/video-player/src/main/java/com/lanlinju/videoplayer/component/Slider.kt +++ b/video-player/src/main/java/com/lanlinju/videoplayer/component/Slider.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay @@ -36,7 +37,8 @@ fun Slider( color: Color = MaterialTheme.colorScheme.primary, trackColor: Color = Color.LightGray.copy(alpha = 0.38f), secondTrackColor: Color =Color.LightGray.copy(alpha = 0.78f), - isSeeking: Boolean = false + isSeeking: Boolean = false, + focusRequester: FocusRequester = remember { FocusRequester() }, ) { val isAnimHeight = remember(isSeeking) { mutableStateOf(isSeeking) } val animHeight = animateDpAsState( From f84adee332f57dfde293618a6bb272e2b4a887f9 Mon Sep 17 00:00:00 2001 From: wdw89 Date: Tue, 23 Jun 2026 00:47:47 +0800 Subject: [PATCH 02/41] =?UTF-8?q?feat:=20TV=E9=81=A5=E6=8E=A7=E5=99=A8?= =?UTF-8?q?=E7=84=A6=E7=82=B9=E6=A0=B7=E5=BC=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Card border颜色统一为#BCC0C8(深色浅色一致) - Week页面TopAppBar标题区域改为圆角矩形焦点背景 - Home页面站源文本移除clickable,不再可聚焦 - 详情页标签改为非交互Box,不再可聚焦 --- .../presentation/component/MediaSmall.kt | 3 +- .../screen/detail/AnimeDetailScreen.kt | 36 +++++++++---------- .../presentation/screen/home/HomeScreen.kt | 8 +---- .../presentation/screen/week/WeekScreen.kt | 29 ++++++++------- 4 files changed, 36 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt b/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt index 7253293e..b2b81cac 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/component/MediaSmall.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource @@ -90,7 +91,7 @@ fun MediaSmall( .onFocusChanged { isFocused = it.isFocused } .border( width = if (isFocused) 3.dp else 0.dp, - color = if (isFocused) Color.White else Color.Transparent, + color = if (isFocused) Color(0xFFBCC0C8) else Color.Transparent, shape = cardShape ), colors = CardDefaults.cardColors( diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/detail/AnimeDetailScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/detail/AnimeDetailScreen.kt index d9f2cccf..b579091b 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/detail/AnimeDetailScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/detail/AnimeDetailScreen.kt @@ -7,6 +7,7 @@ import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.hoverable @@ -571,27 +572,22 @@ fun AnimeGenres( modifier = modifier ) { items(genres) { genre -> - SuggestionChip( - label = { - Text( - text = genre.uppercase(), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding( - vertical = dimensionResource(Res.dimen.small_padding) - ) + Box( + modifier = Modifier + .clip(CircleShape) + .background(color.copy(alpha = 0.25f)) + .border(0.dp, Color.Transparent, CircleShape) + ) { + Text( + text = genre.uppercase(), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding( + horizontal = dimensionResource(Res.dimen.small_padding), + vertical = dimensionResource(Res.dimen.small_padding) ) - }, - onClick = { }, - shape = CircleShape, - colors = SuggestionChipDefaults.suggestionChipColors( - containerColor = color.copy(alpha = 0.25f) - ), - border = SuggestionChipDefaults.suggestionChipBorder( - enabled = true, - borderColor = Color.Transparent - ), - ) + ) + } } } } diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt index 31ca6f1d..4f6ce795 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt @@ -273,7 +273,7 @@ private fun Tab( Surface( onClick = onClick, modifier = modifier.padding(8.dp), - shape = RoundedCornerShape(11.dp), + shape = RoundedCornerShape(20.dp), color = if (selected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.primary.copy(alpha = 0.1f), contentColor = if (selected) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.onBackground, ) { @@ -418,12 +418,6 @@ private fun HomeTile( .align(Alignment.BottomStart) .offset(y = 8.dp) .padding(vertical = if (isWideScreen && useGridLayout) 8.dp else 0.dp) - .run { - if (isWideScreen) { - // 获取焦点 - clickable { } - } else this - } ) } diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt index f20ead20..d4ef29ec 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch +import androidx.compose.material3.Surface import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text @@ -64,6 +65,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalClipboardManager @@ -132,19 +134,22 @@ fun WeekScreen( Column { TopAppBar( title = { - Column(modifier = Modifier.clickable { - showSourceSwitchDialog = true - }) { - Text( - text = stringResource(id = R.string.lbl_schedule), - style = MaterialTheme.typography.titleLarge - ) - Text( - text = SourceHolder.currentSourceMode.name, - style = MaterialTheme.typography.bodyLarge - ) + Surface( + onClick = { showSourceSwitchDialog = true }, + shape = RoundedCornerShape(20.dp), + color = Color.Transparent, + ) { + Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp)) { + Text( + text = stringResource(id = R.string.lbl_schedule), + style = MaterialTheme.typography.titleLarge + ) + Text( + text = SourceHolder.currentSourceMode.name, + style = MaterialTheme.typography.bodyLarge + ) + } } - }, actions = { AppBarAction( From b80773a0b0c304b12e61d2a8c7b8328877d3cf9b Mon Sep 17 00:00:00 2001 From: wdw89 Date: Thu, 25 Jun 2026 23:46:49 +0800 Subject: [PATCH 03/41] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E5=8A=A8?= =?UTF-8?q?=E6=BC=AB=E6=BA=90=E5=88=87=E6=8D=A2=E4=B8=BA=E7=9B=91=E5=90=AC?= =?UTF-8?q?+=E7=9B=B4=E6=8E=A5=E5=88=B7=E6=96=B0=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SourceHolder.isSourceChanged改为计数器,避免布尔标志竞争条件 - 两个页面切换源时递增计数器,所有页面同时刷新 - Home页面站源改为Surface+SourceSwitchDialog,与Week页面统一 - Home页面站源添加圆角矩形焦点背景 - 移除Week页面源切换的确认/取消按钮,选中即切换 --- .../presentation/screen/home/HomeScreen.kt | 65 +++++++++++++++---- .../presentation/screen/week/WeekScreen.kt | 51 ++++++++------- .../com/lanlinju/animius/util/SourceHolder.kt | 3 +- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt index 4f6ce795..614c9e80 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt @@ -36,6 +36,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -48,6 +50,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -55,7 +58,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale @@ -82,6 +87,7 @@ import com.lanlinju.animius.util.KEY_HOME_BACKGROUND_URI import com.lanlinju.animius.util.KEY_USE_GRID_LAYOUT import com.lanlinju.animius.util.SourceHolder import com.lanlinju.animius.util.SourceMode +import com.lanlinju.animius.presentation.screen.week.SourceSwitchDialog import com.lanlinju.animius.util.bannerParallax import com.lanlinju.animius.util.isWideScreen import com.lanlinju.animius.util.rememberPreference @@ -95,11 +101,11 @@ fun HomeScreen( ) { val homeViewModel = hiltViewModel() val availableDataList = homeViewModel.homeDataList.collectAsState() + val isSourceChanged by SourceHolder.isSourceChanged - LaunchedEffect(SourceHolder.isSourceChanged) { - if (SourceHolder.isSourceChanged) { + LaunchedEffect(isSourceChanged) { + if (isSourceChanged > 0) { homeViewModel.refresh() - SourceHolder.isSourceChanged = false } } @@ -149,7 +155,8 @@ fun HomeScreen( it.detailUrl, SourceHolder.currentSourceMode ) - } + }, + onRefresh = { homeViewModel.refresh() } ) } } @@ -166,6 +173,8 @@ private fun HomeContent( homeBackgroundColor: Color, onSwitchGridLayout: (Boolean) -> Unit, onItemClick: (Anime) -> Unit, + onSourceChanged: () -> Unit = {}, + onRefresh: () -> Unit = {}, modifier: Modifier = Modifier ) { Column(modifier = modifier) { @@ -182,7 +191,9 @@ private fun HomeContent( if (isWideScreen) { HomeTile( useGridLayout = useGridLayout, - onSwitchGridLayout = onSwitchGridLayout + onSwitchGridLayout = onSwitchGridLayout, + onSourceChanged = onSourceChanged, + onRefresh = onRefresh ) } @@ -387,8 +398,12 @@ private fun HomeTile( useGridLayout: Boolean, onSwitchGridLayout: (Boolean) -> Unit, onClick: () -> Unit = {}, + onSourceChanged: () -> Unit = {}, + onRefresh: () -> Unit = {}, ) { val isWideScreen = isWideScreen(LocalContext.current) + var showSourceSwitchDialog by remember { mutableStateOf(false) } + var currentSourceName by remember { mutableStateOf(SourceHolder.currentSourceMode.name) } Row( modifier = modifier .fillMaxWidth(), @@ -401,6 +416,7 @@ private fun HomeTile( start = dimensionResource(Res.dimen.large_padding), bottom = if (!isWideScreen) dimensionResource(Res.dimen.medium_padding) else 0.dp ) + .focusGroup() ) { if (!isWideScreen) { Text( @@ -410,15 +426,36 @@ private fun HomeTile( modifier = Modifier.clickable { onClick() } ) } - Text( - text = SourceHolder.currentSourceMode.name, - color = MaterialTheme.colorScheme.onSecondaryContainer, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier - .align(Alignment.BottomStart) - .offset(y = 8.dp) - .padding(vertical = if (isWideScreen && useGridLayout) 8.dp else 0.dp) - ) + var isSourceFocused by remember { mutableStateOf(false) } + Surface( + onClick = { showSourceSwitchDialog = true }, + shape = RoundedCornerShape(20.dp), + color = if (isSourceFocused) { + if (MaterialTheme.colorScheme.background.luminance() < 0.5f) + Color.White.copy(alpha = 0.15f) + else + Color.Black.copy(alpha = 0.1f) + } else Color.Transparent, + modifier = Modifier.onFocusChanged { isSourceFocused = it.isFocused } + ) { + Text( + text = currentSourceName, + color = MaterialTheme.colorScheme.onSecondaryContainer, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 6.dp) + ) + } + if (showSourceSwitchDialog) { + SourceSwitchDialog( + onDismissRequest = { showSourceSwitchDialog = false }, + onRefresh = onRefresh, + onSourceChanged = { mode -> + currentSourceName = mode.name + SourceHolder.isSourceChanged.value++ + } + ) + } } if (!isWideScreen) { diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt index d4ef29ec..ca1f37ca 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/week/WeekScreen.kt @@ -121,6 +121,14 @@ fun WeekScreen( var showSourceSwitchDialog by remember { mutableStateOf(false) } var showSettingsDialog by remember { mutableStateOf(false) } var showDomainChangeDialog by remember { mutableStateOf(false) } + var currentSourceName by remember { mutableStateOf(SourceHolder.currentSourceMode.name) } + val isSourceChanged by SourceHolder.isSourceChanged + + LaunchedEffect(isSourceChanged) { + if (isSourceChanged > 0) { + viewModel.refresh() + } + } val context = LocalContext.current val uriHandler = LocalUriHandler.current @@ -145,7 +153,7 @@ fun WeekScreen( style = MaterialTheme.typography.titleLarge ) Text( - text = SourceHolder.currentSourceMode.name, + text = currentSourceName, style = MaterialTheme.typography.bodyLarge ) } @@ -229,6 +237,7 @@ fun WeekScreen( onDownloadUpdate = { lifecycleOwner -> viewModel.downloadVersionUpdate(context, lifecycleOwner) }, + onSourceChanged = { mode -> currentSourceName = mode.name }, ) } @@ -492,11 +501,13 @@ fun Dialogs( onDismissLoadingIndicationDialog: () -> Unit, onDownloadUpdate: (LifecycleOwner) -> Unit, onRefresh: () -> Unit, + onSourceChanged: (SourceMode) -> Unit = {} ) { if (showSourceSwitchDialog) { SourceSwitchDialog( onDismissRequest = onDismissSourceSwitchDialog, - onRefresh = onRefresh + onRefresh = onRefresh, + onSourceChanged = onSourceChanged ) } if (showSettingsDialog) { @@ -588,9 +599,10 @@ private fun LoadingIndicationDialog( } @Composable -private fun SourceSwitchDialog( +fun SourceSwitchDialog( onDismissRequest: () -> Unit, - onRefresh: () -> Unit + onRefresh: () -> Unit, + onSourceChanged: (SourceMode) -> Unit = {} ) { var currentSourceMode by rememberPreference(KEY_SOURCE_MODE, DEFAULT_ANIME_SOURCE) @@ -629,7 +641,15 @@ private fun SourceSwitchDialog( .background(MaterialTheme.colorScheme.surfaceVariant) .selectable( selected = (text == selectedOption), - onClick = { onOptionSelected(text) }, + onClick = { + val mode = SourceMode.valueOf(text) + currentSourceMode = mode + SourceHolder.isSourceChanged.value++ + SourceHolder.switchSource(mode) + onSourceChanged(mode) + onDismissRequest() + onRefresh() + }, role = Role.RadioButton ) .padding(start = dimensionResource(id = R.dimen.large_padding)), @@ -648,25 +668,7 @@ private fun SourceSwitchDialog( } } }, - confirmButton = { - TextButton( - onClick = { - val mode = SourceMode.valueOf(selectedOption) - currentSourceMode = mode // 保存默认源到偏好设置 - SourceHolder.isSourceChanged = true // 触发Home数据刷新 - SourceHolder.switchSource(mode) // 切换数据源 - onDismissRequest() // 关闭Dialog - onRefresh() // Week页面重新获取数据 - } - ) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(stringResource(R.string.cancel)) - } - } + confirmButton = {}, ) } @@ -808,7 +810,6 @@ private fun DomainChangeDialog( TextButton(onClick = { if (text.isNotEmpty()) { currentDomain = text - SourceHolder.isSourceChanged = true SourceHolder.currentSource.baseUrl = currentDomain onDismissRequest(true) } else { diff --git a/app/src/main/java/com/lanlinju/animius/util/SourceHolder.kt b/app/src/main/java/com/lanlinju/animius/util/SourceHolder.kt index 18b75ba2..b0747f5c 100644 --- a/app/src/main/java/com/lanlinju/animius/util/SourceHolder.kt +++ b/app/src/main/java/com/lanlinju/animius/util/SourceHolder.kt @@ -1,5 +1,6 @@ package com.lanlinju.animius.util +import androidx.compose.runtime.mutableStateOf import com.lanlinju.animius.application.AnimeApplication import com.lanlinju.animius.data.remote.parse.AgedmSource import com.lanlinju.animius.data.remote.parse.AnimeSource @@ -29,7 +30,7 @@ object SourceHolder { val currentSourceMode: SourceMode get() = _currentSourceMode - var isSourceChanged = false + var isSourceChanged = mutableStateOf(0) init { val preferences = AnimeApplication.getInstance().preferences From 76be33bc8964314f41c6018c706d56d470929948 Mon Sep 17 00:00:00 2001 From: wdw89 Date: Fri, 26 Jun 2026 00:12:00 +0800 Subject: [PATCH 04/41] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=E6=BA=90?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E7=BB=9F=E4=B8=80=E6=94=B9=E9=80=A0spec?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除Home站源自定义焦点颜色中的冗余代码 - 统一使用surfaceVariant焦点背景色 --- .../presentation/screen/home/HomeScreen.kt | 10 ++------ .../specs/2026-06-19-source-switch-spec.md | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 docs/compose/specs/2026-06-19-source-switch-spec.md diff --git a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt index 614c9e80..0ed42ff5 100644 --- a/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt +++ b/app/src/main/java/com/lanlinju/animius/presentation/screen/home/HomeScreen.kt @@ -57,10 +57,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale @@ -430,12 +429,7 @@ private fun HomeTile( Surface( onClick = { showSourceSwitchDialog = true }, shape = RoundedCornerShape(20.dp), - color = if (isSourceFocused) { - if (MaterialTheme.colorScheme.background.luminance() < 0.5f) - Color.White.copy(alpha = 0.15f) - else - Color.Black.copy(alpha = 0.1f) - } else Color.Transparent, + color = if (isSourceFocused) MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f) else Color.Transparent, modifier = Modifier.onFocusChanged { isSourceFocused = it.isFocused } ) { Text( diff --git a/docs/compose/specs/2026-06-19-source-switch-spec.md b/docs/compose/specs/2026-06-19-source-switch-spec.md new file mode 100644 index 00000000..73a5067a --- /dev/null +++ b/docs/compose/specs/2026-06-19-source-switch-spec.md @@ -0,0 +1,24 @@ +# [S1] 动漫源切换统一改造 + +## [S2] 问题 + +- Week 和 Home 页面切换源功能不统一:Week 页面用 AlertDialog + RadioButton,Home 页面无切换功能 +- 切换源后只有当前页面刷新,其他页面不同步 +- Week 页面切换源需要点击确认按钮,操作繁琐 +- Home 页面站源文本无焦点样式,不能使用 D-pad 操作 + +## [S3] 改动的文件 + +| 文件 | 改动 | +|------|------| +| `util/SourceHolder.kt` | `isSourceChanged` 改为 `mutableStateOf(0)` 计数器 | +| `screen/week/WeekScreen.kt` | `SourceSwitchDialog` 改为 public,添加 `onSourceChanged` 回调,移除确认/取消按钮 | +| `screen/home/HomeScreen.kt` | 站源文本改为 `Surface(onClick)` + `SourceSwitchDialog`,添加 `focusGroup()` | +| `screen/home/HomeScreen.kt` | 站源使用 `surfaceVariant.copy(alpha = 0.6f)` 焦点背景色 | + +## [S4] 关键实现 + +1. **跨页面刷新**: `SourceHolder.isSourceChanged` 用 `mutableStateOf(0)` 递增计数器代替布尔值 +2. **Week 页面**: `SourceSwitchDialog` 改为 public,选中 RadioButton 立即切换并刷新 +3. **Home 页面**: 站源名称用 `Surface(onClick) + RoundedCornerShape(20.dp)` + `sourceSwitchDialog` 弹出相同源选择列表 +4. **焦点背景**: `onFocusChanged` + `surfaceVariant.copy(alpha = 0.6f)` 统一圆角矩形效果 From b101d293cb957637cf05671be0b2a4c8bb485716 Mon Sep 17 00:00:00 2001 From: wdw89 Date: Sun, 5 Jul 2026 23:35:27 +0800 Subject: [PATCH 05/41] =?UTF-8?q?feat:=20=E6=90=9C=E7=B4=A2=E9=A1=B5?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20&=20=E9=AA=8C=E8=AF=81=E7=A0=81=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20&=20TV=E7=84=A6=E7=82=B9=E5=AF=BC=E8=88=AA=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 搜索页: - 重构搜索栏为双模式布局(编辑/非编辑),适配TV遥控器D-pad导航 - 编辑模式支持方向键防冲突、自动聚焦输入框 - 非编辑模式显示当前查询文本,点击/按确认键进入编辑 - 搜索结果自动聚焦第一项,移除isAndroidTV条件判断 - 添加BackHandler处理返回键优先级(菜单>编辑>返回) 验证码: - CaptchaWebViewActivity 添加 windowSoftInputMode=adjustResize - 自动轮询点击"我已了解/知道了/确定"公告按钮 - 自动聚焦验证码输入框,提升操作效率 - 添加BackHandler焦点流转: WebView→确认按钮→退出 - 按钮添加焦点边框高亮,支持TV导航 焦点导航统一: - 所有IconButton添加onFocusChanged高亮(primary色容器背景) - MediaSmall焦点边框改为primary主题色 - 移除各处isAndroidTV条件判断,统一支持所有设备 - SourceHolder.isSourceChanged改为MutableStateFlow - VideoPlayerScreen移除isAndroidTV守卫,统一焦点行为 - build.gradle.kts: ui.tooling改为implementation作用域 --- .github/prompts/opsx-apply.prompt.md | 152 +++++ .github/prompts/opsx-archive.prompt.md | 157 +++++ .github/prompts/opsx-explore.prompt.md | 171 +++++ .github/prompts/opsx-propose.prompt.md | 106 +++ .github/prompts/opsx-sync.prompt.md | 140 ++++ .github/skills/openspec-apply-change/SKILL.md | 159 +++++ .../skills/openspec-archive-change/SKILL.md | 117 ++++ .github/skills/openspec-explore/SKILL.md | 289 ++++++++ .github/skills/openspec-propose/SKILL.md | 113 +++ .github/skills/openspec-sync-specs/SKILL.md | 147 ++++ .gitignore | 1 + .junie/commands/opsx-apply.md | 152 +++++ .junie/commands/opsx-archive.md | 157 +++++ .junie/commands/opsx-explore.md | 171 +++++ .junie/commands/opsx-propose.md | 106 +++ .junie/commands/opsx-sync.md | 140 ++++ .junie/skills/openspec-apply-change/SKILL.md | 159 +++++ .../skills/openspec-archive-change/SKILL.md | 117 ++++ .junie/skills/openspec-explore/SKILL.md | 289 ++++++++ .junie/skills/openspec-propose/SKILL.md | 113 +++ .junie/skills/openspec-sync-specs/SKILL.md | 147 ++++ .mimocode/plans/1781852361992-silent-moon.md | 62 ++ .mimocode/plans/1781852362003-tidy-tiger.md | 93 +++ .opencode/commands/opsx-apply.md | 152 +++++ .opencode/commands/opsx-archive.md | 157 +++++ .opencode/commands/opsx-explore.md | 171 +++++ .opencode/commands/opsx-propose.md | 106 +++ .opencode/commands/opsx-sync.md | 140 ++++ .../plans/fix-episodelistcontrol-click.md | 21 + .../skills/openspec-apply-change/SKILL.md | 159 +++++ .../skills/openspec-archive-change/SKILL.md | 117 ++++ .opencode/skills/openspec-explore/SKILL.md | 289 ++++++++ .opencode/skills/openspec-propose/SKILL.md | 113 +++ .opencode/skills/openspec-sync-specs/SKILL.md | 147 ++++ AGENTS.md | 88 +++ app/build.gradle.kts | 2 +- app/src/main/AndroidManifest.xml | 3 +- .../presentation/component/MediaSmall.kt | 2 +- .../screen/captcha/CaptchaWebViewActivity.kt | 74 +- .../screen/detail/AnimeDetailScreen.kt | 190 ++++-- .../presentation/screen/home/HomeScreen.kt | 2 +- .../screen/search/SearchScreen.kt | 555 +++++++++++---- .../screen/search/SearchViewModel.kt | 8 +- .../screen/videoplayer/VideoPlayerScreen.kt | 15 +- .../presentation/screen/week/WeekScreen.kt | 2 +- .../com/lanlinju/animius/util/SourceHolder.kt | 4 +- app/src/main/res/values/strings.xml | 1 + .../plans/2025-06-19-tv-remote-fixes.md | 461 +++++++++++++ .../plans/2025-06-19-tv-remote-support.md | 641 ++++++++++++++++++ docs/compose/reports/tv-remote-support.md | 52 ++ .../specs/focus-border-and-long-press.md | 100 +++ ...026-06-27-tv-focus-inverse-color-design.md | 104 +++ openspec/config.yaml | 20 + 53 files changed, 6950 insertions(+), 204 deletions(-) create mode 100644 .github/prompts/opsx-apply.prompt.md create mode 100644 .github/prompts/opsx-archive.prompt.md create mode 100644 .github/prompts/opsx-explore.prompt.md create mode 100644 .github/prompts/opsx-propose.prompt.md create mode 100644 .github/prompts/opsx-sync.prompt.md create mode 100644 .github/skills/openspec-apply-change/SKILL.md create mode 100644 .github/skills/openspec-archive-change/SKILL.md create mode 100644 .github/skills/openspec-explore/SKILL.md create mode 100644 .github/skills/openspec-propose/SKILL.md create mode 100644 .github/skills/openspec-sync-specs/SKILL.md create mode 100644 .junie/commands/opsx-apply.md create mode 100644 .junie/commands/opsx-archive.md create mode 100644 .junie/commands/opsx-explore.md create mode 100644 .junie/commands/opsx-propose.md create mode 100644 .junie/commands/opsx-sync.md create mode 100644 .junie/skills/openspec-apply-change/SKILL.md create mode 100644 .junie/skills/openspec-archive-change/SKILL.md create mode 100644 .junie/skills/openspec-explore/SKILL.md create mode 100644 .junie/skills/openspec-propose/SKILL.md create mode 100644 .junie/skills/openspec-sync-specs/SKILL.md create mode 100644 .mimocode/plans/1781852361992-silent-moon.md create mode 100644 .mimocode/plans/1781852362003-tidy-tiger.md create mode 100644 .opencode/commands/opsx-apply.md create mode 100644 .opencode/commands/opsx-archive.md create mode 100644 .opencode/commands/opsx-explore.md create mode 100644 .opencode/commands/opsx-propose.md create mode 100644 .opencode/commands/opsx-sync.md create mode 100644 .opencode/plans/fix-episodelistcontrol-click.md create mode 100644 .opencode/skills/openspec-apply-change/SKILL.md create mode 100644 .opencode/skills/openspec-archive-change/SKILL.md create mode 100644 .opencode/skills/openspec-explore/SKILL.md create mode 100644 .opencode/skills/openspec-propose/SKILL.md create mode 100644 .opencode/skills/openspec-sync-specs/SKILL.md create mode 100644 AGENTS.md create mode 100644 docs/compose/plans/2025-06-19-tv-remote-fixes.md create mode 100644 docs/compose/plans/2025-06-19-tv-remote-support.md create mode 100644 docs/compose/reports/tv-remote-support.md create mode 100644 docs/compose/specs/focus-border-and-long-press.md create mode 100644 docs/superpowers/specs/2026-06-27-tv-focus-inverse-color-design.md create mode 100644 openspec/config.yaml diff --git a/.github/prompts/opsx-apply.prompt.md b/.github/prompts/opsx-apply.prompt.md new file mode 100644 index 00000000..5bd0b6bb --- /dev/null +++ b/.github/prompts/opsx-apply.prompt.md @@ -0,0 +1,152 @@ +--- +description: Implement tasks from an OpenSpec change (Experimental) +--- + +Implement tasks from an OpenSpec change. + +**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store ` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root. + +**Input**: Optionally specify a change name (e.g., `/opsx:apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes. + +**Steps** + +1. **Select the change** + + If a name is provided, use it. Otherwise: + - Infer from conversation context if the user mentioned a change + - Auto-select if only one active change exists + - If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select + + Always announce: "Using change: " and how to override (e.g., `/opsx:apply `). + +2. **Check status to understand the schema** + ```bash + openspec status --change "" --json + ``` + Parse the JSON to understand: + - `schemaName`: The workflow being used (e.g., "spec-driven") + - `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints + - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others) + +3. **Get apply instructions** + + ```bash + openspec instructions apply --change "" --json + ``` + + This returns: + - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema) + - Progress (total, complete, remaining) + - Task list with status + - Dynamic instruction based on current state + + **Handle states:** + - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx:continue` + - If `state: "all_done"`: congratulate, suggest archive + - Otherwise: proceed to implementation + +4. **Read context files** + + Read every file path listed under `contextFiles` from the apply instructions output. + The files depend on the schema being used: + - **spec-driven**: proposal, specs, design, tasks + - Other schemas: follow the contextFiles from CLI output + +5. **Show current progress** + + Display: + - Schema being used + - Progress: "N/M tasks complete" + - Remaining tasks overview + - Dynamic instruction from CLI + +6. **Implement tasks (loop until done or blocked)** + + For each pending task: + - Show which task is being worked on + - Make the code changes required + - Keep changes minimal and focused + - Mark task complete in the tasks file: `- [ ]` → `- [x]` + - Continue to next task + + **Pause if:** + - Task is unclear → ask for clarification + - Implementation reveals a design issue → suggest updating artifacts + - Error or blocker encountered → report and wait for guidance + - User interrupts + +7. **On completion or pause, show status** + + Display: + - Tasks completed this session + - Overall progress: "N/M tasks complete" + - If all done: suggest archive + - If paused: explain why and wait for guidance + +**Output During Implementation** + +``` +## Implementing: (schema: ) + +Working on task 3/7: +[...implementation happening...] +✓ Task complete + +Working on task 4/7: +[...implementation happening...] +✓ Task complete +``` + +**Output On Completion** + +``` +## Implementation Complete + +**Change:** +**Schema:** +**Progress:** 7/7 tasks complete ✓ + +### Completed This Session +- [x] Task 1 +- [x] Task 2 +... + +All tasks complete! You can archive this change with `/opsx:archive`. +``` + +**Output On Pause (Issue Encountered)** + +``` +## Implementation Paused + +**Change:** +**Schema:** +**Progress:** 4/7 tasks complete + +### Issue Encountered + + +**Options:** +1.