From d2adcc8963b96151ac455de40d1a2bd8b97f8f2c Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 15 Aug 2026 22:02:39 +0300 Subject: [PATCH] properly restore focus on episode screen --- .../episode/EpisodeOverviewComponent.kt | 3 +- .../overview/episode/EpisodeOverviewScreen.kt | 54 +++++++++++-------- .../chat/presentation/ChatScreen.kt | 6 ++- .../updater/presentation/UpdateDialog.kt | 6 ++- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewComponent.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewComponent.kt index 9ba977d..d650690 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewComponent.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewComponent.kt @@ -20,7 +20,6 @@ import org.koin.core.annotation.Named import ru.shadowsparky.domain.Clip import ru.shadowsparky.domain.ClipboardManager import ru.shadowsparky.domain.DispatcherProvider -import ru.shadowsparky.ui.nav.RootComponent import ru.shadowsparky.ui.nav.ComponentFactory import ru.shadowsparky.ui.nav.RouteNavigator import ru.shadowsparky.vbox.shared.domain.AddOfflineVideoUseCase @@ -71,7 +70,7 @@ class EpisodeOverviewComponent( } } - val recentlyFlow = recentlyWatchedRepository.queryRecentlyWatched(id, seasonId) + private val recentlyFlow = recentlyWatchedRepository.queryRecentlyWatched(id, seasonId) private val episodes = recentlyFlow.combine(episodesFromServer) { watched, season -> val episode = season.episodes diff --git a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewScreen.kt b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewScreen.kt index 2c4b944..455ccb5 100644 --- a/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewScreen.kt +++ b/apps/vbox/client/shared/src/commonMain/kotlin/ru/shadowsparky/vbox/shared/presentation/overview/episode/EpisodeOverviewScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -30,6 +31,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.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable @@ -39,6 +41,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.unit.dp import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -175,38 +178,45 @@ fun EpisodesOverviewList( buttonText = stringResource(Res.string.retry) ) { loadLinks() } } else if (episodes != null) { + val lazyListState = rememberLazyListState() + var focusedIndex by rememberSaveable { mutableIntStateOf(0) } val requester = remember { FocusRequester() } ExpressiveLazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = contentPadding, - content = { - section { - itemsIndexed( - episodes, - { it.episode.episode } - ) { index, episode -> - EpisodeInfo( - if (index == 0) { + state = lazyListState, + ) { + section { + itemsIndexed( + items = episodes, + key = { item -> item.episode.episode } + ) { index, episode -> + EpisodeInfo( + Modifier.onFocusChanged { state -> + if (state.isFocused) focusedIndex = index + }.then( + if (index == focusedIndex) { Modifier.focusRequester(requester) } else { Modifier - }, - onLaunch, - episode, - onClipboardAction, - index, - onOfflineClicked, - isSaveMode - ) - } + } + ), + onLaunch, + episode, + onClipboardAction, + index, + onOfflineClicked, + isSaveMode + ) } } - ) + } LaunchedEffect(episodes) { - if (episodes.isNotEmpty()) { - withFrameNanos {} - requester.requestFocus() - } + if (episodes.isEmpty()) return@LaunchedEffect + val index = focusedIndex.coerceIn(episodes.indices) + lazyListState.scrollToItem(index) + withFrameNanos {} + requester.requestFocus() } } else { Loading() diff --git a/feature/chat/chat-client/src/commonMain/kotlin/ru/shadowsparky/chat/presentation/ChatScreen.kt b/feature/chat/chat-client/src/commonMain/kotlin/ru/shadowsparky/chat/presentation/ChatScreen.kt index 849da89..b120b0b 100644 --- a/feature/chat/chat-client/src/commonMain/kotlin/ru/shadowsparky/chat/presentation/ChatScreen.kt +++ b/feature/chat/chat-client/src/commonMain/kotlin/ru/shadowsparky/chat/presentation/ChatScreen.kt @@ -32,8 +32,9 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -130,6 +131,7 @@ fun ChatContent(component: ChatComponent, paddingValues: PaddingValues, state: C } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ChatContent( contentPadding: PaddingValues, @@ -224,7 +226,7 @@ private fun ChatContent( ) } if (state.isSending) { - LinearProgressIndicator(Modifier.fillMaxWidth()) + LinearWavyProgressIndicator(Modifier.fillMaxWidth()) } Spacer(Modifier.height(4.dp)) Row { diff --git a/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt b/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt index beaf54f..2b5709a 100644 --- a/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt +++ b/feature/updater/updater-client/src/commonMain/kotlin/ru/shadowsparky/updater/presentation/UpdateDialog.kt @@ -24,7 +24,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -178,6 +179,7 @@ private fun UpdatePermissionContent( } } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun UpdateDownloadingContent( progress: Float, @@ -194,7 +196,7 @@ private fun UpdateDownloadingContent( text = stringResource(Res.string.updater_title_downloading), style = MaterialTheme.typography.titleMedium ) - LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) + LinearWavyProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) val downloadedMb = ((downloadedBytes / (1024f * 1024f)) * 10).toInt() / 10.0 val totalMb = ((totalBytes / (1024f * 1024f)) * 10).toInt() / 10.0