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
<img width="1280" height="771" alt="next_up_credits Large"
src="https://github.com/user-attachments/assets/9a03d0ec-325d-4efe-a28e-51270ddaaa89"
/>
This commit is contained in:
damontecres 2025-11-02 10:51:59 -05:00 committed by GitHub
parent 715dc1abda
commit d76f829804
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 87 additions and 37 deletions

View file

@ -602,6 +602,19 @@ sealed interface AppPreference<T> {
summaryOn = R.string.enabled, summaryOn = R.string.enabled,
summaryOff = R.string.nav_drawer_switch_on_focus_summary_off, summaryOff = R.string.nav_drawer_switch_on_focus_summary_off,
) )
val ShowNextUpTiming =
AppChoicePreference<ShowNextUpWhen>(
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( listOf(
AppPreference.HomePageItems, AppPreference.HomePageItems,
AppPreference.RewatchNextUp, AppPreference.RewatchNextUp,
AppPreference.CombineContinueNext,
AppPreference.PlayThemeMusic, AppPreference.PlayThemeMusic,
AppPreference.RememberSelectedTab, AppPreference.RememberSelectedTab,
AppPreference.ThemeColors, AppPreference.ThemeColors,
@ -625,11 +637,17 @@ val basicPreferences =
listOf( listOf(
AppPreference.SkipForward, AppPreference.SkipForward,
AppPreference.SkipBack, AppPreference.SkipBack,
AppPreference.ControllerTimeout, AppPreference.SkipBackOnResume,
),
),
PreferenceGroup(
title = R.string.next_up,
preferences =
listOf(
AppPreference.ShowNextUpTiming,
AppPreference.AutoPlayNextUp, AppPreference.AutoPlayNextUp,
AppPreference.AutoPlayNextDelay, AppPreference.AutoPlayNextDelay,
AppPreference.PassOutProtection, AppPreference.PassOutProtection,
AppPreference.SkipBackOnResume,
), ),
), ),
PreferenceGroup( PreferenceGroup(
@ -664,7 +682,9 @@ val advancedPreferences =
title = R.string.ui_interface, title = R.string.ui_interface,
preferences = preferences =
listOf( listOf(
AppPreference.CombineContinueNext,
AppPreference.NavDrawerSwitchOnFocus, AppPreference.NavDrawerSwitchOnFocus,
AppPreference.ControllerTimeout,
), ),
), ),
PreferenceGroup( PreferenceGroup(

View file

@ -43,6 +43,7 @@ class AppPreferencesSerializer
skipRecaps = AppPreference.SkipRecaps.defaultValue skipRecaps = AppPreference.SkipRecaps.defaultValue
passOutProtectionMs = passOutProtectionMs =
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue
overrides = overrides =
PlaybackOverrides PlaybackOverrides

View file

@ -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.components.LoadingPage
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.tryRequestFocus 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.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode import com.github.damontecres.wholphin.util.seasonEpisode
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import java.util.UUID import java.util.UUID
@ -162,7 +164,6 @@ fun PlaybackPage(
val scaledModifier = val scaledModifier =
Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp) Modifier.resizeWithContentScale(contentScale, presentationState.videoSizeDp)
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val controllerFocusRequester = remember { FocusRequester() }
val playPauseState = rememberPlayPauseButtonState(player) val playPauseState = rememberPlayPauseButtonState(player)
val seekBarState = rememberSeekBarState(player, scope) val seekBarState = rememberSeekBarState(player, scope)
@ -215,9 +216,9 @@ fun PlaybackPage(
Box( Box(
modifier 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( Box(
modifier = modifier =
Modifier Modifier
@ -418,8 +419,14 @@ fun PlaybackPage(
// Next up episode // Next up episode
BackHandler(nextUp != null) { BackHandler(nextUp != null) {
if (player.isPlaying) {
scope.launch(ExceptionHandler()) {
viewModel.cancelUpNextEpisode()
}
} else {
viewModel.navigationManager.goBack() viewModel.navigationManager.goBack()
} }
}
AnimatedVisibility( AnimatedVisibility(
nextUp != null, nextUp != null,
modifier = modifier =
@ -470,7 +477,7 @@ fun PlaybackPage(
Modifier Modifier
.padding(8.dp) .padding(8.dp)
// .height(128.dp) // .height(128.dp)
.fillMaxHeight(.5f) .fillMaxHeight(1 - playerSize)
.fillMaxWidth(.66f) .fillMaxWidth(.66f)
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.background( .background(

View file

@ -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.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.MediaExtensionStatus 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.SkipSegmentBehavior
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -97,12 +98,6 @@ import javax.inject.Inject
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
enum class TranscodeType {
DIRECT_PLAY,
DIRECT_STREAM,
TRANSCODE,
}
data class StreamDecision( data class StreamDecision(
val itemId: UUID, val itemId: UUID,
val type: PlayMethod, val type: PlayMethod,
@ -159,6 +154,7 @@ class PlaybackViewModel
val trickplay = MutableLiveData<TrickplayInfo?>(null) val trickplay = MutableLiveData<TrickplayInfo?>(null)
val chapters = MutableLiveData<List<Chapter>>(listOf()) val chapters = MutableLiveData<List<Chapter>>(listOf())
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null) val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
private val autoSkippedSegments = mutableSetOf<UUID>()
private lateinit var preferences: UserPreferences private lateinit var preferences: UserPreferences
private lateinit var deviceProfile: DeviceProfile private lateinit var deviceProfile: DeviceProfile
@ -244,6 +240,7 @@ class PlaybackViewModel
): Boolean = ): Boolean =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Timber.i("Playing ${base.id}") Timber.i("Playing ${base.id}")
autoSkippedSegments.clear()
if (base.type !in supportItemKinds) { if (base.type !in supportItemKinds) {
showToast( showToast(
context, context,
@ -358,11 +355,9 @@ class PlaybackViewModel
audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED, audioIndex = audioIndex ?: TrackIndex.UNSPECIFIED,
subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED, subtitleIndex = subtitleIndex ?: TrackIndex.UNSPECIFIED,
) )
withContext(Dispatchers.Main) {
this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@PlaybackViewModel.currentItemPlayback.value = itemPlaybackToUse
this@PlaybackViewModel.audioStreams.value = audioStreams this@PlaybackViewModel.audioStreams.value = audioStreams
this@PlaybackViewModel.subtitleStreams.value = subtitleStreams this@PlaybackViewModel.subtitleStreams.value = subtitleStreams
@ -664,13 +659,25 @@ class PlaybackViewModel
.firstOrNull { .firstOrNull {
it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks it.type != MediaSegmentType.UNKNOWN && currentTicks >= it.startTicks && currentTicks < it.endTicks
} }
if (currentSegment != null) { if (currentSegment != null && autoSkippedSegments.add(currentSegment.id)) {
Timber.d( Timber.d(
"Found media segment for %s: %s, %s", "Found media segment for %s: %s, %s",
currentSegment.itemId, currentSegment.itemId,
currentSegment.id, currentSegment.id,
currentSegment.type, currentSegment.type,
) )
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
}
} else {
val behavior = val behavior =
when (currentSegment.type) { when (currentSegment.type) {
MediaSegmentType.COMMERCIAL -> prefs.skipCommercials MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
@ -688,7 +695,8 @@ class PlaybackViewModel
} }
SkipSegmentBehavior.ASK_TO_SKIP -> { SkipSegmentBehavior.ASK_TO_SKIP -> {
this@PlaybackViewModel.currentSegment.value = currentSegment this@PlaybackViewModel.currentSegment.value =
currentSegment
} }
else -> { else -> {
@ -696,7 +704,8 @@ class PlaybackViewModel
} }
} }
} }
} else { }
} else if (currentSegment == null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@PlaybackViewModel.currentSegment.value = null this@PlaybackViewModel.currentSegment.value = null
} }

View file

@ -3,6 +3,11 @@ syntax = "proto3";
option java_package = "com.github.damontecres.wholphin.preferences"; option java_package = "com.github.damontecres.wholphin.preferences";
option java_multiple_files = true; option java_multiple_files = true;
enum ShowNextUpWhen{
END_OF_PLAYBACK = 0;
DURING_CREDITS = 1;
}
enum SkipSegmentBehavior{ enum SkipSegmentBehavior{
IGNORE = 0; IGNORE = 0;
AUTO_SKIP = 1; AUTO_SKIP = 1;
@ -52,6 +57,7 @@ message PlaybackPreferences {
PlaybackOverrides overrides = 15; PlaybackOverrides overrides = 15;
int64 pass_out_protection_ms = 16; int64 pass_out_protection_ms = 16;
PrefContentScale global_content_scale = 17; PrefContentScale global_content_scale = 17;
ShowNextUpWhen show_next_up_when = 18;
} }
message HomePagePreferences{ message HomePagePreferences{

View file

@ -37,4 +37,9 @@
<item>Never use FFmpeg decoders</item> <item>Never use FFmpeg decoders</item>
</string-array> </string-array>
<string-array name="show_next_up_when_options">
<item>At the end of playback</item>
<item>During end credits/outro</item>
</string-array>
</resources> </resources>

View file

@ -100,5 +100,7 @@
<string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string> <string name="ffmpeg_extension_pref">Use FFmpeg decoder module</string>
<string name="nav_drawer_switch_on_focus">Switch nav drawer pages on focus</string> <string name="nav_drawer_switch_on_focus">Switch nav drawer pages on focus</string>
<string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string> <string name="nav_drawer_switch_on_focus_summary_off">Click to switch pages</string>
<string name="show_next_up_when">Show next up</string>
<string name="next_up">Next Up</string>
</resources> </resources>