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
---
.../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