From 39239dd44467314b11ea04163b1c54b7c707bddf Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 5 Sep 2026 12:11:30 -0400 Subject: [PATCH] Add wrap around scrolling to settings pages (#1937) ## Description Add scrolling from top to bottom and vice versa. This makes it quicker to get to Advanced Settings for example. ### Related issues N/A ### Testing Emulator ## Screenshots N/A ## AI or LLM usage None --- .../wholphin/preferences/Extensions.kt | 59 +++++++++++++++++++ .../wholphin/ui/components/Dialogs.kt | 32 ++-------- .../ui/preferences/PreferencesContent.kt | 31 +++++++--- .../subtitle/SubtitlePreferencesContent.kt | 29 +++++++-- 4 files changed, 112 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/Extensions.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/Extensions.kt index 0000878b..478bffb6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/Extensions.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/Extensions.kt @@ -1,5 +1,19 @@ package com.github.damontecres.wholphin.preferences +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.playback.isDown +import com.github.damontecres.wholphin.ui.playback.isUp +import com.github.damontecres.wholphin.ui.tryRequestFocus +import kotlinx.coroutines.launch import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -10,3 +24,48 @@ val PlaybackPreferences.skipBackOnResume: Duration? } else { null } + +/** + * Adds modifiers so that pressing Up on the first element scrolls to the bottom and vice versa + */ +@Composable +fun Modifier.lazyListWrapScrolling( + state: LazyListState, + focused: Boolean, + isFirst: Boolean, + isLast: Boolean, + firstFocusRequester: FocusRequester, + lastFocusRequester: FocusRequester, +): Modifier { + val scope = rememberCoroutineScope() + return this + .ifElse( + isFirst, + Modifier + .focusRequester(firstFocusRequester) + .onKeyEvent { + if (focused && isUp(it) && it.type == KeyEventType.KeyDown) { + scope.launch { + state.animateScrollToItem(state.layoutInfo.totalItemsCount - 1) + lastFocusRequester.tryRequestFocus() + } + return@onKeyEvent true + } + false + }, + ).ifElse( + isLast, + Modifier + .focusRequester(lastFocusRequester) + .onKeyEvent { + if (focused && isDown(it) && it.type == KeyEventType.KeyDown) { + scope.launch { + state.animateScrollToItem(0) + firstFocusRequester.tryRequestFocus() + } + return@onKeyEvent true + } + false + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt index f1091839..d7bd9951 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/Dialogs.kt @@ -63,15 +63,12 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.data.model.TrackIndex +import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.formatBitrate -import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.playback.SimpleMediaStream -import com.github.damontecres.wholphin.ui.playback.isDown -import com.github.damontecres.wholphin.ui.playback.isUp import com.github.damontecres.wholphin.ui.roundMinutes -import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -322,30 +319,13 @@ fun DialogPopupContent( modifier = Modifier .focusRequester(focusRequesters[index]) - .ifElse( + .lazyListWrapScrolling( + listState, + focused, index == 0, - Modifier.onKeyEvent { - if (focused && isUp(it) && it.type == KeyEventType.KeyDown) { - scope.launch { - listState.animateScrollToItem(dialogItems.lastIndex) - focusRequesters[dialogItems.lastIndex].tryRequestFocus() - } - return@onKeyEvent true - } - false - }, - ).ifElse( index == dialogItems.lastIndex, - Modifier.onKeyEvent { - if (focused && isDown(it) && it.type == KeyEventType.KeyDown) { - scope.launch { - listState.animateScrollToItem(0) - focusRequesters[0].tryRequestFocus() - } - return@onKeyEvent true - } - false - }, + focusRequesters[0], + focusRequesters[dialogItems.lastIndex], ), ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index d57de598..c97117b7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.preferences.SkipSegmentPreferences import com.github.damontecres.wholphin.preferences.advancedPreferences import com.github.damontecres.wholphin.preferences.basicPreferences import com.github.damontecres.wholphin.preferences.experimentalPreferences +import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling import com.github.damontecres.wholphin.preferences.screensaverPreferences import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences import com.github.damontecres.wholphin.services.Release @@ -99,6 +100,9 @@ fun PreferencesContent( val context = LocalContext.current val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } + val firstFocusRequester = remember { FocusRequester() } + val lastFocusRequester = remember { FocusRequester() } + var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) } val state = rememberLazyListState() var preferences by remember { mutableStateOf(initialPreferences) } @@ -135,7 +139,7 @@ fun PreferencesContent( } } - val movementSounds = true + val movementSounds = false val installedVersion = updateVM.currentVersion val updateAvailable = remember(updateState.release) { @@ -185,6 +189,12 @@ fun PreferencesContent( } } + val showUpdate = + UpdateChecker.ACTIVE && + preferenceScreenOption == PreferenceScreenOption.BASIC && + preferences.autoCheckForUpdates && + updateAvailable + AnimatedVisibility( visible = visible, enter = fadeIn() + slideInHorizontally { it / 2 }, @@ -214,11 +224,7 @@ fun PreferencesContent( contentPadding = PaddingValues(16.dp), modifier = Modifier.fillMaxSize(), ) { - if (UpdateChecker.ACTIVE && - preferenceScreenOption == PreferenceScreenOption.BASIC && - preferences.autoCheckForUpdates && - updateAvailable - ) { + if (showUpdate) { item { val updateFocusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { @@ -237,6 +243,7 @@ fun PreferencesContent( modifier = Modifier .focusRequester(updateFocusRequester) + .focusRequester(firstFocusRequester) .playSoundOnFocus(movementSounds), ) } @@ -262,15 +269,25 @@ fun PreferencesContent( .flatten() groupPreferences.forEachIndexed { prefIndex, pref -> pref as AppPreference + val isFirst = groupIndex == 0 && prefIndex == 0 && !showUpdate + val isLast = + groupIndex == prefList.lastIndex && prefIndex == groupPreferences.lastIndex item { val interactionSource = remember { MutableInteractionSource() } + val focused = interactionSource.collectIsFocusedAsState().value val focusModifier = Modifier .ifElse( groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, Modifier.focusRequester(focusRequester), + ).lazyListWrapScrolling( + state, + focused, + isFirst, + isLast, + firstFocusRequester, + lastFocusRequester, ) - val focused = interactionSource.collectIsFocusedAsState().value LaunchedEffect(focused) { if (focused) { focusedIndex = Pair(groupIndex, prefIndex) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt index f130016f..32da9613 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt @@ -40,6 +40,7 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.SubtitlePreferences +import com.github.damontecres.wholphin.preferences.lazyListWrapScrolling import com.github.damontecres.wholphin.preferences.resetSubtitles import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.preferences.ClickPreference @@ -64,6 +65,8 @@ fun SubtitlePreferencesContent( val context = LocalContext.current val scope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } + val firstFocusRequester = remember { FocusRequester() } + val lastFocusRequester = remember { FocusRequester() } var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) } val state = rememberLazyListState() @@ -123,6 +126,9 @@ fun SubtitlePreferencesContent( groupPreferences.forEachIndexed { prefIndex, pref -> pref as AppPreference item { + val isFirst = groupIndex == 0 && prefIndex == 0 + val isLast = + groupIndex == prefList.lastIndex && prefIndex == groupPreferences.lastIndex val interactionSource = remember { MutableInteractionSource() } val bringIntoViewRequester = remember { BringIntoViewRequester() } val focused = interactionSource.collectIsFocusedAsState().value @@ -137,6 +143,21 @@ fun SubtitlePreferencesContent( bringIntoViewRequester.bringIntoView() } } + val focusModifier = + Modifier + .ifElse( + groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, + Modifier.focusRequester(focusRequester), + ).bringIntoViewRequester(bringIntoViewRequester) + .lazyListWrapScrolling( + state, + focused, + isFirst, + isLast, + firstFocusRequester, + lastFocusRequester, + ) + when (pref) { SubtitleSettings.Reset -> { ClickPreference( @@ -152,6 +173,7 @@ fun SubtitlePreferencesContent( } }, interactionSource = interactionSource, + modifier = focusModifier, ) } @@ -187,12 +209,7 @@ fun SubtitlePreferencesContent( } }, interactionSource = interactionSource, - modifier = - Modifier - .ifElse( - groupIndex == focusedIndex.first && prefIndex == focusedIndex.second, - Modifier.focusRequester(focusRequester), - ).bringIntoViewRequester(bringIntoViewRequester), + modifier = focusModifier, ) } }