diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomSeekProvider.kt b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomSeekProvider.kt index 4e2061c4c3..725f9a473f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomSeekProvider.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/CustomSeekProvider.kt @@ -21,19 +21,16 @@ class CustomSeekProvider( private val api: ApiClient, private val context: Context, private val trickPlayEnabled: Boolean, + private val forwardTime: Long ) : PlaybackSeekDataProvider() { - companion object { - private const val SEEK_LENGTH = 10000L - } - private val imageRequests = mutableMapOf() override fun getSeekPositions(): LongArray { if (!videoPlayerAdapter.canSeek()) return LongArray(0) val duration = videoPlayerAdapter.duration - val size = ceil(duration.toDouble() / SEEK_LENGTH.toDouble()).toInt() + 1 - return LongArray(size) { i -> min(i * SEEK_LENGTH, duration) } + val size = ceil(duration.toDouble() / forwardTime.toDouble()).toInt() + 1 + return LongArray(size) { i -> min(i * forwardTime, duration) } } override fun getThumbnail(index: Int, callback: ResultCallback) { @@ -51,7 +48,7 @@ class CustomSeekProvider( val trickPlayInfo = trickPlayResolutions?.values?.firstOrNull() if (trickPlayInfo == null) return - val currentTimeMs = (index * SEEK_LENGTH).coerceIn(0, videoPlayerAdapter.duration) + val currentTimeMs = (index * forwardTime).coerceIn(0, videoPlayerAdapter.duration) val currentTile = currentTimeMs.floorDiv(trickPlayInfo.interval).toInt() val tileSize = trickPlayInfo.tileWidth * trickPlayInfo.tileHeight diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/LeanbackOverlayFragment.java b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/LeanbackOverlayFragment.java index 42bb1e30a6..02f3133155 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/LeanbackOverlayFragment.java +++ b/app/src/main/java/org/jellyfin/androidtv/ui/playback/overlay/LeanbackOverlayFragment.java @@ -10,6 +10,7 @@ import androidx.leanback.app.PlaybackSupportFragment; import org.jellyfin.androidtv.preference.UserPreferences; +import org.jellyfin.androidtv.preference.UserSettingPreferences; import org.jellyfin.androidtv.ui.playback.CustomPlaybackOverlayFragment; import org.jellyfin.androidtv.ui.playback.PlaybackController; import org.jellyfin.androidtv.ui.playback.PlaybackControllerContainer; @@ -24,6 +25,7 @@ public class LeanbackOverlayFragment extends PlaybackSupportFragment { private VideoPlayerAdapter playerAdapter; private boolean shouldShowOverlay = true; private Lazy playbackControllerContainer = inject(PlaybackControllerContainer.class); + private final Lazy userSettingPreferences = inject(UserSettingPreferences.class); private Lazy imageLoader = inject(ImageLoader.class); private Lazy api = inject(ApiClient.class); private Lazy userPreferences = inject(UserPreferences.class); @@ -99,8 +101,10 @@ public void mediaInfoChanged() { playerGlue.invalidatePlaybackControls(); playerGlue.setSeekEnabled(playerAdapter.canSeek()); + + long skipForwardLength = userSettingPreferences.getValue().get(UserSettingPreferences.Companion.getSkipForwardLength()).longValue(); boolean enableTrickPlay = userPreferences.getValue().get(UserPreferences.Companion.getTrickPlayEnabled()); - playerGlue.setSeekProvider(playerAdapter.canSeek() ? new CustomSeekProvider(playerAdapter, imageLoader.getValue(), api.getValue(), requireContext(), enableTrickPlay) : null); + playerGlue.setSeekProvider(playerAdapter.canSeek() ? new CustomSeekProvider(playerAdapter, imageLoader.getValue(), api.getValue(), requireContext(), enableTrickPlay, skipForwardLength) : null); recordingStateChanged(); playerAdapter.updateDuration(); } diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/preference/dsl/OptionsItemSeekbar.kt b/app/src/main/java/org/jellyfin/androidtv/ui/preference/dsl/OptionsItemSeekbar.kt index 7c78faf734..2191401ddf 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/preference/dsl/OptionsItemSeekbar.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/preference/dsl/OptionsItemSeekbar.kt @@ -31,8 +31,9 @@ class OptionsItemSeekbar( it.isEnabled = dependencyCheckFun() && enabled it.isVisible = visible it.title = title - it.min = min + // Max must be set before min because the setter of min checks if the value is below the current value of max it.max = max + it.min = min it.seekBarIncrement = increment it.value = binder.get() it.showSeekBarValue = true diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt index e11b8ae482..b57c30538f 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/preference/screen/PlaybackPreferencesScreen.kt @@ -2,6 +2,7 @@ package org.jellyfin.androidtv.ui.preference.screen import org.jellyfin.androidtv.R import org.jellyfin.androidtv.preference.UserPreferences +import org.jellyfin.androidtv.preference.UserSettingPreferences import org.jellyfin.androidtv.preference.constant.AudioBehavior import org.jellyfin.androidtv.preference.constant.NEXTUP_TIMER_DISABLED import org.jellyfin.androidtv.preference.constant.NextUpBehavior @@ -15,14 +16,19 @@ import org.jellyfin.androidtv.ui.preference.dsl.enum import org.jellyfin.androidtv.ui.preference.dsl.link import org.jellyfin.androidtv.ui.preference.dsl.optionsScreen import org.jellyfin.androidtv.ui.preference.dsl.seekbar +import org.jellyfin.preference.store.PreferenceStore import org.jellyfin.sdk.model.api.MediaSegmentType import org.koin.android.ext.android.inject import kotlin.math.roundToInt class PlaybackPreferencesScreen : OptionsFragment() { private val userPreferences: UserPreferences by inject() + private val userSettingPreferences: UserSettingPreferences by inject() private val mediaSegmentRepository: MediaSegmentRepository by inject() + override val stores: Array> + get() = arrayOf(userSettingPreferences) + override val screen by optionsScreen { setTitle(R.string.pref_playback) @@ -60,6 +66,19 @@ class PlaybackPreferencesScreen : OptionsFragment() { setContent(R.string.sum_enable_cinema_mode) bind(userPreferences, UserPreferences.cinemaModeEnabled) } + + @Suppress("MagicNumber") + seekbar { + setTitle(R.string.skip_forward_length) + setContent(R.string.skip_forward_length) + min = 5_000 + max = 30_000 + increment = 5_000 + valueFormatter = object : DurationSeekBarPreference.ValueFormatter() { + override fun display(value: Int) = "${value / 1000}s" + } + bind(userSettingPreferences, UserSettingPreferences.skipForwardLength) + } } category { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6e3056b5df..86e432b118 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -523,6 +523,7 @@ Previews Recaps Unknown segments + Skip forward length Enable trickplay in video player %1$s second diff --git a/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt b/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt index 29917b5b39..fc3ac5c844 100644 --- a/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt +++ b/app/src/test/kotlin/ui/playback/overlay/CustomSeekProviderTests.kt @@ -11,7 +11,7 @@ class CustomSeekProviderTests : FunSpec({ every { canSeek() } returns true every { duration } returns 30000L } - val customSeekProvider = CustomSeekProvider(videoPlayerAdapter, mockk(), mockk(), mockk(), false) + val customSeekProvider = CustomSeekProvider(videoPlayerAdapter, mockk(), mockk(), mockk(), false, 10_000) customSeekProvider.seekPositions shouldBe arrayOf(0L, 10000L, 20000L, 30000L) } @@ -21,7 +21,7 @@ class CustomSeekProviderTests : FunSpec({ every { canSeek() } returns true every { duration } returns 45000L } - val customSeekProvider = CustomSeekProvider(videoPlayerAdapter, mockk(), mockk(), mockk(), false) + val customSeekProvider = CustomSeekProvider(videoPlayerAdapter, mockk(), mockk(), mockk(), false, 10_000) customSeekProvider.seekPositions shouldBe arrayOf(0L, 10000, 20000, 30000, 40000, 45000) } @@ -30,7 +30,7 @@ class CustomSeekProviderTests : FunSpec({ val videoPlayerAdapter = mockk { every { canSeek() } returns false } - val customSeekProvider = CustomSeekProvider(videoPlayerAdapter, mockk(), mockk(), mockk(), false) + val customSeekProvider = CustomSeekProvider(videoPlayerAdapter, mockk(), mockk(), mockk(), false, 10_000) customSeekProvider.seekPositions.size shouldBe 0 }