From d76f829804c5cc8186d9a5fb2dc9d76a5a450b5c Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:51:59 -0500 Subject: [PATCH] Option to show next up during credits/outros (#130) Adds an option to the show the next up during the end credits (outro) instead of at the very end of the video. The next up pushes up from the bottom and the video scales down to fit above. Still a WIP though ### Sample screenshot next_up_credits Large --- .../wholphin/preferences/AppPreference.kt | 26 ++++++- .../preferences/AppPreferencesSerializer.kt | 1 + .../wholphin/ui/playback/PlaybackPage.kt | 17 +++-- .../wholphin/ui/playback/PlaybackViewModel.kt | 67 +++++++++++-------- app/src/main/proto/WholphinDataStore.proto | 6 ++ app/src/main/res/values/preferences.xml | 5 ++ app/src/main/res/values/strings.xml | 2 + 7 files changed, 87 insertions(+), 37 deletions(-) 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 2805cbbd..5ef7a763 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 @@ -602,6 +602,19 @@ sealed interface AppPreference { summaryOn = R.string.enabled, summaryOff = R.string.nav_drawer_switch_on_focus_summary_off, ) + + val ShowNextUpTiming = + AppChoicePreference( + title = R.string.show_next_up_when, + defaultValue = ShowNextUpWhen.END_OF_PLAYBACK, + getter = { it.playbackPreferences.showNextUpWhen }, + setter = { prefs, value -> + prefs.updatePlaybackPreferences { showNextUpWhen = value } + }, + displayValues = R.array.show_next_up_when_options, + indexToValue = { ShowNextUpWhen.forNumber(it) }, + valueToIndex = { it.number }, + ) } } @@ -613,7 +626,6 @@ val basicPreferences = listOf( AppPreference.HomePageItems, AppPreference.RewatchNextUp, - AppPreference.CombineContinueNext, AppPreference.PlayThemeMusic, AppPreference.RememberSelectedTab, AppPreference.ThemeColors, @@ -625,11 +637,17 @@ val basicPreferences = listOf( AppPreference.SkipForward, AppPreference.SkipBack, - AppPreference.ControllerTimeout, + AppPreference.SkipBackOnResume, + ), + ), + PreferenceGroup( + title = R.string.next_up, + preferences = + listOf( + AppPreference.ShowNextUpTiming, AppPreference.AutoPlayNextUp, AppPreference.AutoPlayNextDelay, AppPreference.PassOutProtection, - AppPreference.SkipBackOnResume, ), ), PreferenceGroup( @@ -664,7 +682,9 @@ val advancedPreferences = title = R.string.ui_interface, preferences = listOf( + AppPreference.CombineContinueNext, AppPreference.NavDrawerSwitchOnFocus, + AppPreference.ControllerTimeout, ), ), PreferenceGroup( 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 4fce1470..bb90e4a8 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 @@ -43,6 +43,7 @@ class AppPreferencesSerializer skipRecaps = AppPreference.SkipRecaps.defaultValue passOutProtectionMs = AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds + showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue overrides = PlaybackOverrides 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 50a1d81b..ec9dc464 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 @@ -69,9 +69,11 @@ 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.tryRequestFocus +import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.seasonEpisode import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.extensions.ticks import java.util.UUID @@ -162,7 +164,6 @@ fun PlaybackPage( val scaledModifier = Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp) val focusRequester = remember { FocusRequester() } - val controllerFocusRequester = remember { FocusRequester() } val playPauseState = rememberPlayPauseButtonState(player) val seekBarState = rememberSeekBarState(player, scope) @@ -215,9 +216,9 @@ fun PlaybackPage( Box( modifier - .background(Color.Black), + .background(if (nextUp == null) Color.Black else MaterialTheme.colorScheme.background), ) { - val playerSize by animateFloatAsState(if (nextUp == null) 1f else .66f) + val playerSize by animateFloatAsState(if (nextUp == null) 1f else .6f) Box( modifier = Modifier @@ -418,7 +419,13 @@ fun PlaybackPage( // Next up episode BackHandler(nextUp != null) { - viewModel.navigationManager.goBack() + if (player.isPlaying) { + scope.launch(ExceptionHandler()) { + viewModel.cancelUpNextEpisode() + } + } else { + viewModel.navigationManager.goBack() + } } AnimatedVisibility( nextUp != null, @@ -470,7 +477,7 @@ fun PlaybackPage( Modifier .padding(8.dp) // .height(128.dp) - .fillMaxHeight(.5f) + .fillMaxHeight(1 - playerSize) .fillMaxWidth(.66f) .align(Alignment.BottomCenter) .background( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt index 1510830d..d1b523a6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackViewModel.kt @@ -33,6 +33,7 @@ import com.github.damontecres.wholphin.data.model.chooseStream import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.MediaExtensionStatus +import com.github.damontecres.wholphin.preferences.ShowNextUpWhen import com.github.damontecres.wholphin.preferences.SkipSegmentBehavior import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.launchIO @@ -97,12 +98,6 @@ import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -enum class TranscodeType { - DIRECT_PLAY, - DIRECT_STREAM, - TRANSCODE, -} - data class StreamDecision( val itemId: UUID, val type: PlayMethod, @@ -159,6 +154,7 @@ class PlaybackViewModel val trickplay = MutableLiveData(null) val chapters = MutableLiveData>(listOf()) val currentSegment = EqualityMutableLiveData(null) + private val autoSkippedSegments = mutableSetOf() private lateinit var preferences: UserPreferences private lateinit var deviceProfile: DeviceProfile @@ -244,6 +240,7 @@ class PlaybackViewModel ): Boolean = withContext(Dispatchers.IO) { Timber.i("Playing ${base.id}") + autoSkippedSegments.clear() if (base.type !in supportItemKinds) { showToast( context, @@ -358,11 +355,9 @@ class PlaybackViewModel audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED, ) - withContext(Dispatchers.Main) { - this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse - } withContext(Dispatchers.Main) { + this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse this@PlaybackViewModel.audioStreams.value = audioStreams this@PlaybackViewModel.subtitleStreams.value = subtitleStreams @@ -664,39 +659,53 @@ class PlaybackViewModel .firstOrNull { it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks } - if (currentSegment != null) { + if (currentSegment != null && autoSkippedSegments.add(currentSegment.id)) { Timber.d( "Found media segment for %s: %s, %s", currentSegment.itemId, currentSegment.id, currentSegment.type, ) - val behavior = - when (currentSegment.type) { - MediaSegmentType.COMMERCIAL -> prefs.skipCommercials - MediaSegmentType.PREVIEW -> prefs.skipPreviews - MediaSegmentType.RECAP -> prefs.skipRecaps - MediaSegmentType.OUTRO -> prefs.skipOutros - MediaSegmentType.INTRO -> prefs.skipIntros - MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE + val playlist = this@PlaybackViewModel.playlist.value + + if (currentSegment.type == MediaSegmentType.OUTRO && + prefs.showNextUpWhen == ShowNextUpWhen.DURING_CREDITS && + playlist != null && playlist.hasNext() + ) { + val nextItem = playlist.peek() + Timber.v("Setting next up during outro to ${nextItem?.id}") + withContext(Dispatchers.Main) { + nextUp.value = nextItem } - withContext(Dispatchers.Main) { - when (behavior) { - SkipSegmentBehavior.AUTO_SKIP -> { - this@PlaybackViewModel.currentSegment.value = null - player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1) + } else { + val behavior = + when (currentSegment.type) { + MediaSegmentType.COMMERCIAL -> prefs.skipCommercials + MediaSegmentType.PREVIEW -> prefs.skipPreviews + MediaSegmentType.RECAP -> prefs.skipRecaps + MediaSegmentType.OUTRO -> prefs.skipOutros + MediaSegmentType.INTRO -> prefs.skipIntros + MediaSegmentType.UNKNOWN -> SkipSegmentBehavior.IGNORE } + withContext(Dispatchers.Main) { + when (behavior) { + SkipSegmentBehavior.AUTO_SKIP -> { + this@PlaybackViewModel.currentSegment.value = null + player.seekTo(currentSegment.endTicks.ticks.inWholeMilliseconds + 1) + } - SkipSegmentBehavior.ASK_TO_SKIP -> { - this@PlaybackViewModel.currentSegment.value = currentSegment - } + SkipSegmentBehavior.ASK_TO_SKIP -> { + this@PlaybackViewModel.currentSegment.value = + currentSegment + } - else -> { - this@PlaybackViewModel.currentSegment.value = null + else -> { + this@PlaybackViewModel.currentSegment.value = null + } } } } - } else { + } else if (currentSegment == null) { withContext(Dispatchers.Main) { this@PlaybackViewModel.currentSegment.value = null } diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index f27f146a..f6023e49 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -3,6 +3,11 @@ syntax = "proto3"; option java_package = "com.github.damontecres.wholphin.preferences"; option java_multiple_files = true; +enum ShowNextUpWhen{ + END_OF_PLAYBACK = 0; + DURING_CREDITS = 1; +} + enum SkipSegmentBehavior{ IGNORE = 0; AUTO_SKIP = 1; @@ -52,6 +57,7 @@ message PlaybackPreferences { PlaybackOverrides overrides = 15; int64 pass_out_protection_ms = 16; PrefContentScale global_content_scale = 17; + ShowNextUpWhen show_next_up_when = 18; } message HomePagePreferences{ diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index bc8574f4..6e38778a 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -37,4 +37,9 @@ Never use FFmpeg decoders + + At the end of playback + During end credits/outro + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e51cc61..f2ca9395 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,5 +100,7 @@ Use FFmpeg decoder module Switch nav drawer pages on focus Click to switch pages + Show next up + Next Up