mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-07 03:04:50 +01:00
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
This commit is contained in:
parent
1f3a861c47
commit
39239dd444
4 changed files with 112 additions and 39 deletions
|
|
@ -1,5 +1,19 @@
|
||||||
package com.github.damontecres.wholphin.preferences
|
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
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
|
@ -10,3 +24,48 @@ val PlaybackPreferences.skipBackOnResume: Duration?
|
||||||
} else {
|
} else {
|
||||||
null
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,12 @@ import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.TrackIndex
|
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.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.formatBitrate
|
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.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.playback.SimpleMediaStream
|
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.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -322,30 +319,13 @@ fun DialogPopupContent(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(focusRequesters[index])
|
.focusRequester(focusRequesters[index])
|
||||||
.ifElse(
|
.lazyListWrapScrolling(
|
||||||
|
listState,
|
||||||
|
focused,
|
||||||
index == 0,
|
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,
|
index == dialogItems.lastIndex,
|
||||||
Modifier.onKeyEvent {
|
focusRequesters[0],
|
||||||
if (focused && isDown(it) && it.type == KeyEventType.KeyDown) {
|
focusRequesters[dialogItems.lastIndex],
|
||||||
scope.launch {
|
|
||||||
listState.animateScrollToItem(0)
|
|
||||||
focusRequesters[0].tryRequestFocus()
|
|
||||||
}
|
|
||||||
return@onKeyEvent true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ import com.github.damontecres.wholphin.preferences.SkipSegmentPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.basicPreferences
|
import com.github.damontecres.wholphin.preferences.basicPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.experimentalPreferences
|
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.screensaverPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.services.Release
|
import com.github.damontecres.wholphin.services.Release
|
||||||
|
|
@ -99,6 +100,9 @@ fun PreferencesContent(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val firstFocusRequester = remember { FocusRequester() }
|
||||||
|
val lastFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
var preferences by remember { mutableStateOf(initialPreferences) }
|
var preferences by remember { mutableStateOf(initialPreferences) }
|
||||||
|
|
@ -135,7 +139,7 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val movementSounds = true
|
val movementSounds = false
|
||||||
val installedVersion = updateVM.currentVersion
|
val installedVersion = updateVM.currentVersion
|
||||||
val updateAvailable =
|
val updateAvailable =
|
||||||
remember(updateState.release) {
|
remember(updateState.release) {
|
||||||
|
|
@ -185,6 +189,12 @@ fun PreferencesContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val showUpdate =
|
||||||
|
UpdateChecker.ACTIVE &&
|
||||||
|
preferenceScreenOption == PreferenceScreenOption.BASIC &&
|
||||||
|
preferences.autoCheckForUpdates &&
|
||||||
|
updateAvailable
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible,
|
visible = visible,
|
||||||
enter = fadeIn() + slideInHorizontally { it / 2 },
|
enter = fadeIn() + slideInHorizontally { it / 2 },
|
||||||
|
|
@ -214,11 +224,7 @@ fun PreferencesContent(
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
if (UpdateChecker.ACTIVE &&
|
if (showUpdate) {
|
||||||
preferenceScreenOption == PreferenceScreenOption.BASIC &&
|
|
||||||
preferences.autoCheckForUpdates &&
|
|
||||||
updateAvailable
|
|
||||||
) {
|
|
||||||
item {
|
item {
|
||||||
val updateFocusRequester = remember { FocusRequester() }
|
val updateFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -237,6 +243,7 @@ fun PreferencesContent(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(updateFocusRequester)
|
.focusRequester(updateFocusRequester)
|
||||||
|
.focusRequester(firstFocusRequester)
|
||||||
.playSoundOnFocus(movementSounds),
|
.playSoundOnFocus(movementSounds),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -262,15 +269,25 @@ fun PreferencesContent(
|
||||||
.flatten()
|
.flatten()
|
||||||
groupPreferences.forEachIndexed { prefIndex, pref ->
|
groupPreferences.forEachIndexed { prefIndex, pref ->
|
||||||
pref as AppPreference<AppPreferences, Any>
|
pref as AppPreference<AppPreferences, Any>
|
||||||
|
val isFirst = groupIndex == 0 && prefIndex == 0 && !showUpdate
|
||||||
|
val isLast =
|
||||||
|
groupIndex == prefList.lastIndex && prefIndex == groupPreferences.lastIndex
|
||||||
item {
|
item {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val focused = interactionSource.collectIsFocusedAsState().value
|
||||||
val focusModifier =
|
val focusModifier =
|
||||||
Modifier
|
Modifier
|
||||||
.ifElse(
|
.ifElse(
|
||||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
||||||
Modifier.focusRequester(focusRequester),
|
Modifier.focusRequester(focusRequester),
|
||||||
|
).lazyListWrapScrolling(
|
||||||
|
state,
|
||||||
|
focused,
|
||||||
|
isFirst,
|
||||||
|
isLast,
|
||||||
|
firstFocusRequester,
|
||||||
|
lastFocusRequester,
|
||||||
)
|
)
|
||||||
val focused = interactionSource.collectIsFocusedAsState().value
|
|
||||||
LaunchedEffect(focused) {
|
LaunchedEffect(focused) {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
focusedIndex = Pair(groupIndex, prefIndex)
|
focusedIndex = Pair(groupIndex, prefIndex)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.SubtitlePreferences
|
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.preferences.resetSubtitles
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.preferences.ClickPreference
|
import com.github.damontecres.wholphin.ui.preferences.ClickPreference
|
||||||
|
|
@ -64,6 +65,8 @@ fun SubtitlePreferencesContent(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val firstFocusRequester = remember { FocusRequester() }
|
||||||
|
val lastFocusRequester = remember { FocusRequester() }
|
||||||
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
|
||||||
val state = rememberLazyListState()
|
val state = rememberLazyListState()
|
||||||
|
|
||||||
|
|
@ -123,6 +126,9 @@ fun SubtitlePreferencesContent(
|
||||||
groupPreferences.forEachIndexed { prefIndex, pref ->
|
groupPreferences.forEachIndexed { prefIndex, pref ->
|
||||||
pref as AppPreference<SubtitlePreferences, Any>
|
pref as AppPreference<SubtitlePreferences, Any>
|
||||||
item {
|
item {
|
||||||
|
val isFirst = groupIndex == 0 && prefIndex == 0
|
||||||
|
val isLast =
|
||||||
|
groupIndex == prefList.lastIndex && prefIndex == groupPreferences.lastIndex
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
val focused = interactionSource.collectIsFocusedAsState().value
|
val focused = interactionSource.collectIsFocusedAsState().value
|
||||||
|
|
@ -137,6 +143,21 @@ fun SubtitlePreferencesContent(
|
||||||
bringIntoViewRequester.bringIntoView()
|
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) {
|
when (pref) {
|
||||||
SubtitleSettings.Reset -> {
|
SubtitleSettings.Reset -> {
|
||||||
ClickPreference(
|
ClickPreference(
|
||||||
|
|
@ -152,6 +173,7 @@ fun SubtitlePreferencesContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
modifier = focusModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,12 +209,7 @@ fun SubtitlePreferencesContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
modifier =
|
modifier = focusModifier,
|
||||||
Modifier
|
|
||||||
.ifElse(
|
|
||||||
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
|
|
||||||
Modifier.focusRequester(focusRequester),
|
|
||||||
).bringIntoViewRequester(bringIntoViewRequester),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue