diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 12e39399..308ed909 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -661,6 +661,19 @@ sealed interface AppPreference { destination = Destination.Settings(PreferenceScreenOption.SKIP_SEGMENTS), ) + val DpadSeekModePref = + AppChoicePreference( + title = R.string.d_pad_seek_mode_title, + defaultValue = DpadSeekMode.SKIP_TIME, + getter = { it.playbackPreferences.dpadSeekMode }, + setter = { prefs, value -> + prefs.updatePlaybackPreferences { dpadSeekMode = value } + }, + displayValues = R.array.dpad_seek_mode_options, + indexToValue = { DpadSeekMode.forNumber(it) }, + valueToIndex = { if (it != DpadSeekMode.UNRECOGNIZED) it.number else 0 }, + ) + val GlobalContentScale = AppChoicePreference( title = R.string.global_content_scale, @@ -1226,6 +1239,7 @@ val advancedPreferences = AppPreference.CinemaMode, AppPreference.GlobalContentScale, AppPreference.SkipSegments, + AppPreference.DpadSeekModePref, AppPreference.MaxBitrate, AppPreference.RefreshRateSwitching, AppPreference.ResolutionSwitching, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index 1e84663d..abcee865 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -55,6 +55,7 @@ class AppPreferencesSerializer AppPreference.RefreshRateSwitching.defaultValue resolutionSwitching = AppPreference.ResolutionSwitching.defaultValue cinemaMode = AppPreference.CinemaMode.defaultValue + dpadSeekMode = AppPreference.DpadSeekModePref.defaultValue overrides = PlaybackOverrides diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index b61d6b89..f403c7a9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -123,6 +123,7 @@ fun NowPlayingPage( // seekBack = preferences.playbackPreferences.skipBackMs.milliseconds, controllerViewState = controllerViewState, updateSkipIndicator = {}, + clearSkipIndicator = {}, skipBackOnResume = null, // skipBackOnResume = preferences.playbackPreferences.skipBackOnResume, onInteraction = viewModel::reportInteraction, @@ -132,6 +133,7 @@ fun NowPlayingPage( }, onPlaybackDialogTypeClick = { }, getDurationMs = { player.duration }, + dpadSeekMode = preferences.playbackPreferences.dpadSeekMode, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt index 7209fbd9..f885826c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.media3.common.Player import androidx.media3.common.util.Util +import com.github.damontecres.wholphin.preferences.DpadSeekMode import com.github.damontecres.wholphin.ui.seekBack import com.github.damontecres.wholphin.ui.seekForward import kotlin.time.Duration @@ -24,11 +25,15 @@ class PlaybackKeyHandler( private val getDurationMs: () -> Long, private val controllerViewState: ControllerViewState, private val updateSkipIndicator: (Long) -> Unit, + private val clearSkipIndicator: () -> Unit, private val skipBackOnResume: Duration?, private val oneClickPause: Boolean, private val onInteraction: () -> Unit, private val onStop: () -> Unit, private val onPlaybackDialogTypeClick: (PlaybackDialogType) -> Unit, + private val isDpadSeekVisible: () -> Boolean = { false }, + private val onDpadSeek: (Long) -> Unit = { }, + private val dpadSeekMode: DpadSeekMode, ) { private var leftHandledByRepeat = false private var rightHandledByRepeat = false @@ -49,20 +54,19 @@ class PlaybackKeyHandler( if (!controllerViewState.controlsVisible) { if (skipWithLeftRight && isSkipBack(it)) { if (isLtr) { - updateSkipIndicator(-seekBack.inWholeMilliseconds) - player.seekBack(seekBack) + seekBy(-seekBack) } else { - updateSkipIndicator(seekForward.inWholeMilliseconds) - player.seekForward(seekForward) + seekBy(seekForward) } } else if (skipWithLeftRight && isSkipForward(it)) { if (isLtr) { - player.seekForward(seekForward) - updateSkipIndicator(seekForward.inWholeMilliseconds) + seekBy(seekForward) } else { - player.seekBack(seekBack) - updateSkipIndicator(seekBack.inWholeMilliseconds) + seekBy(-seekBack) } + } else if (isEnterKey(it) && isDpadSeekVisible()) { + // If d-pad seek bar is visible, hide it + clearSkipIndicator.invoke() } else if (oneClickPause && isEnterKey(it)) { val wasPlaying = player.isPlaying Util.handlePlayPauseButtonAction(player) @@ -180,12 +184,24 @@ class PlaybackKeyHandler( ) { if (isBack) { val skipDuration = seekBack * multiplier - player.seekBack(skipDuration) - updateSkipIndicator(-skipDuration.inWholeMilliseconds) + seekBy(-skipDuration) } else { val skipDuration = seekForward * multiplier - player.seekForward(skipDuration) - updateSkipIndicator(skipDuration.inWholeMilliseconds) + seekBy(skipDuration) + } + } + + private fun seekBy(duration: Duration) { + val durationMs = duration.inWholeMilliseconds + if (dpadSeekMode == DpadSeekMode.TRICKPLAY) { + onDpadSeek.invoke(durationMs) + } else { + if (duration < Duration.ZERO) { + player.seekBack(-duration) + } else { + player.seekForward(duration) + } + updateSkipIndicator(durationMs) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index 62eebd04..46c7753d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -11,6 +11,8 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box @@ -68,6 +70,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.mpv.MpvPlayer import com.github.damontecres.wholphin.preferences.AssPlaybackMode +import com.github.damontecres.wholphin.preferences.DpadSeekMode import com.github.damontecres.wholphin.preferences.PlayerBackend import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.skipBackOnResume @@ -78,6 +81,7 @@ import com.github.damontecres.wholphin.ui.components.BasicDialog import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.playback.overlay.DpadSeekOverlay import com.github.damontecres.wholphin.ui.playback.overlay.PauseIndicator import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackAction import com.github.damontecres.wholphin.ui.playback.overlay.PlaybackOverlay @@ -221,6 +225,23 @@ fun PlaybackPageContent( skipIndicatorDuration += delta skipPosition = player.currentPosition } + val onDpadSeek: (Long) -> Unit = { deltaMs -> + if (skipIndicatorDuration == 0L) { + skipPosition = player.currentPosition + } + if ((skipIndicatorDuration > 0 && deltaMs < 0) || + (skipIndicatorDuration < 0 && deltaMs > 0) + ) { + skipIndicatorDuration = 0L + } + skipIndicatorDuration += deltaMs + skipPosition = + (skipPosition + deltaMs).coerceIn( + minimumValue = 0L, + maximumValue = player.duration.coerceAtLeast(0L), + ) + seekBarState.onValueChange(skipPosition) + } val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val keyHandler = remember(isLtr, preferences) { @@ -234,6 +255,7 @@ fun PlaybackPageContent( getDurationMs = { player.duration.coerceAtLeast(0L) }, controllerViewState = controllerViewState, updateSkipIndicator = updateSkipIndicator, + clearSkipIndicator = { skipIndicatorDuration = 0 }, skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume, onInteraction = viewModel::reportInteraction, oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause, @@ -242,6 +264,11 @@ fun PlaybackPageContent( viewModel.navigationManager.goBack() }, onPlaybackDialogTypeClick = { playbackDialog = it }, + isDpadSeekVisible = { + prefs.dpadSeekMode == DpadSeekMode.TRICKPLAY && skipIndicatorDuration != 0L + }, + onDpadSeek = onDpadSeek, + dpadSeekMode = prefs.dpadSeekMode, ) } @@ -351,8 +378,38 @@ fun PlaybackPageContent( } } + AnimatedVisibility( + visible = + !controllerViewState.controlsVisible && + skipIndicatorDuration != 0L && + prefs.dpadSeekMode == DpadSeekMode.TRICKPLAY, + enter = fadeIn() + slideInVertically { it / 2 }, + exit = fadeOut() + slideOutVertically { it }, + modifier = + Modifier + .align(Alignment.BottomCenter), + ) { + DpadSeekOverlay( + player = player, + seekPositionMs = skipPosition, + trickplayInfo = state.currentMediaInfo.trickPlayInfo, + trickplayUrlFor = viewModel::getTrickplayUrl, + modifier = + Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + .fillMaxWidth(.95f), + ) + // Clear the overlay after a delay + LaunchedEffect(skipIndicatorDuration) { + delay(1.5.seconds) + skipIndicatorDuration = 0L + } + } + // If D-pad skipping, show the amount skipped in an animation - if (!controllerViewState.controlsVisible && skipIndicatorDuration != 0L) { + if (!controllerViewState.controlsVisible && skipIndicatorDuration != 0L && prefs.dpadSeekMode != DpadSeekMode.TRICKPLAY) { + // Skip time mode: show seek distance indicator SkipIndicator( durationMs = skipIndicatorDuration, onFinish = { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/DpadSeekOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/DpadSeekOverlay.kt new file mode 100644 index 00000000..0a3ed634 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/DpadSeekOverlay.kt @@ -0,0 +1,71 @@ +package com.github.damontecres.wholphin.ui.playback.overlay + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.media3.common.Player +import org.jellyfin.sdk.model.api.TrickplayInfo + +/** + * The compact overlay shown during D-Pad seeking in trickplay mode. + */ +@Composable +fun DpadSeekOverlay( + player: Player, + seekPositionMs: Long, + trickplayInfo: TrickplayInfo?, + trickplayUrlFor: (Int) -> String?, + modifier: Modifier = Modifier, +) { + val durationMs = player.duration.coerceAtLeast(0L) + val seekProgressPercent = + if (durationMs > 0) { + (seekPositionMs.toDouble() / durationMs).toFloat().coerceIn(0f, 1f) + } else { + 0f + } + val bufferedProgress = + if (durationMs > 0) { + (player.bufferedPosition.toDouble() / durationMs).toFloat().coerceIn(0f, 1f) + } else { + 0f + } + + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(0.dp), + ) { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + TrickplayPreview( + seekProgressMs = seekPositionMs, + trickplayInfo = trickplayInfo, + trickplayUrlFor = trickplayUrlFor, + modifier = + Modifier + .align(Alignment.BottomStart) + .offsetByPercent(xPercentage = seekProgressPercent) + .padding(bottom = 8.dp), + ) + } + StaticSeekBarImpl( + progress = seekProgressPercent, + bufferedProgress = bufferedProgress, + durationMs = durationMs, + modifier = Modifier.fillMaxWidth(), + ) + SeekTimecodes( + positionMs = seekPositionMs, + durationMs = durationMs, + modifier = Modifier.fillMaxWidth(), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt index 79f44929..d8d43300 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackControls.kt @@ -272,32 +272,45 @@ fun SeekBar( seekBack = seekBack, seekForward = seekForward, ) - Row( + SeekTimecodes( + positionMs = position, + durationMs = player.duration, modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - val resources = LocalResources.current - val positionSec = position / 1000 - val remainingSec = (player.duration - position) / 1000 - val positionText = remember(positionSec) { resources.formatDuration(positionSec.seconds) } - val remainingText = remember(remainingSec) { "-${resources.formatDuration(remainingSec.seconds)}" } - Text( - text = positionText, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelLarge, - modifier = - Modifier - .padding(8.dp), - ) - Text( - text = remainingText, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelLarge, - modifier = - Modifier - .padding(8.dp), - ) - } + ) + } +} + +@Composable +fun SeekTimecodes( + positionMs: Long, + durationMs: Long, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + val resources = LocalResources.current + val positionSec = positionMs / 1000 + val remainingSec = (durationMs - positionMs) / 1000 + val positionText = remember(positionSec) { resources.formatDuration(positionSec.seconds) } + val remainingText = remember(remainingSec) { "-${resources.formatDuration(remainingSec.seconds)}" } + Text( + text = positionText, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + modifier = + Modifier + .padding(8.dp), + ) + Text( + text = remainingText, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + modifier = + Modifier + .padding(8.dp), + ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt index 9af4a171..5f5891de 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/PlaybackOverlay.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -41,13 +40,10 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.media3.common.Player -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text import coil3.compose.AsyncImage import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.Chapter @@ -55,7 +51,6 @@ import com.github.damontecres.wholphin.data.model.PlaylistItem import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.LocalImageUrlService import com.github.damontecres.wholphin.ui.components.TimeDisplay -import com.github.damontecres.wholphin.ui.formatDuration import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.playback.AnalyticsState import com.github.damontecres.wholphin.ui.playback.ControllerViewState @@ -65,7 +60,6 @@ import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.MediaSegmentDto import org.jellyfin.sdk.model.api.TrickplayInfo import kotlin.time.Duration -import kotlin.time.Duration.Companion.seconds /** * The overlay during playback showing controls, seek preview image, debug info, etc @@ -288,33 +282,10 @@ fun PlaybackOverlay( xPercentage = seekProgressPercent.coerceIn(0f, 1f), ).padding(bottom = controllerHeight - titleHeight - subtitleHeight), ) { - if (trickplayInfo != null) { - val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight - val index = - (seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage - val imageUrl = remember(index) { trickplayUrlFor(index) } - - if (imageUrl != null) { - SeekPreviewImage( - modifier = Modifier, - previewImageUrl = imageUrl, - seekProgressMs = seekProgressMs, - trickPlayInfo = trickplayInfo, - ) - } - } - val resources = LocalResources.current - val seekText = remember(seekProgressMs / 1000L) { resources.formatDuration((seekProgressMs / 1000L).seconds) } - Text( - text = seekText, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelLarge, - modifier = - Modifier - .background( - Color.Black.copy(alpha = 0.6f), - shape = RoundedCornerShape(4.dp), - ).padding(horizontal = 8.dp, vertical = 4.dp), + TrickplayPreview( + seekProgressMs = seekProgressMs, + trickplayInfo = trickplayInfo, + trickplayUrlFor = trickplayUrlFor, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/SeekBar.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/SeekBar.kt index d0dfec31..c03be433 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/SeekBar.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/SeekBar.kt @@ -108,6 +108,30 @@ fun SteppedSeekBarImpl( ) } +/** + * A read-only seek bar for showing progress without taking focus or handling D-Pad input. + * + * This is used by overlays where seeking is handled elsewhere and the bar is only a visual indicator. + */ +@Composable +fun StaticSeekBarImpl( + progress: Float, + durationMs: Long, + bufferedProgress: Float, + modifier: Modifier = Modifier, +) { + SeekBarDisplay( + enabled = false, + progress = progress, + bufferedProgress = bufferedProgress, + durationMs = durationMs, + onLeft = {}, + onRight = {}, + interactionSource = remember { MutableInteractionSource() }, + modifier = modifier, + ) +} + /** * A seek (or scrubber) bar which seeks forward or back a fixed amount of time per move */ diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/TrickplayPreview.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/TrickplayPreview.kt new file mode 100644 index 00000000..4871a77a --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/overlay/TrickplayPreview.kt @@ -0,0 +1,63 @@ +package com.github.damontecres.wholphin.ui.playback.overlay + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import com.github.damontecres.wholphin.ui.formatDuration +import org.jellyfin.sdk.model.api.TrickplayInfo +import kotlin.time.Duration.Companion.seconds + +@Composable +fun TrickplayPreview( + seekProgressMs: Long, + trickplayInfo: TrickplayInfo?, + trickplayUrlFor: (Int) -> String?, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + if (trickplayInfo != null) { + val tilesPerImage = trickplayInfo.tileWidth * trickplayInfo.tileHeight + val index = + (seekProgressMs / trickplayInfo.interval).toInt() / tilesPerImage + val imageUrl = remember(index) { trickplayUrlFor(index) } + + if (imageUrl != null) { + SeekPreviewImage( + modifier = Modifier, + previewImageUrl = imageUrl, + seekProgressMs = seekProgressMs, + trickPlayInfo = trickplayInfo, + ) + } + } + val resources = LocalResources.current + val seekProgressSec = seekProgressMs / 1000L + val seekText = remember(seekProgressSec) { resources.formatDuration(seekProgressSec.seconds) } + Text( + text = seekText, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.labelLarge, + modifier = + Modifier + .background( + Color.Black.copy(alpha = 0.6f), + shape = RoundedCornerShape(4.dp), + ).padding(horizontal = 8.dp, vertical = 4.dp), + ) + } +} diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index 4b1f6c34..021db051 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -87,6 +87,12 @@ message PlaybackPreferences { bool resolution_switching = 23; string external_player = 24; bool cinema_mode = 25; + DpadSeekMode dpad_seek_mode = 26; +} + +enum DpadSeekMode { + SKIP_TIME = 0; + TRICKPLAY = 1; } message HomePagePreferences{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa33cf7b..5f5a54de 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -180,6 +180,9 @@ Add to playlist Pause with one click Press D-Pad center to pause/play + D-Pad seek mode + Skip time + Trickplay previews Italicize font Font Background @@ -847,6 +850,11 @@ Enable experimental settings? Experimental settings may be unstable!\n\n These may removed or changed in any release. Video tunneling + + @string/dpad_seek_mode_skip_time + @string/dpad_seek_mode_trickplay + + Blocklisted Unavailable Subtitle mode