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,
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(
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(

View file

@ -43,6 +43,7 @@ class AppPreferencesSerializer
skipRecaps = AppPreference.SkipRecaps.defaultValue
passOutProtectionMs =
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
showNextUpWhen = AppPreference.ShowNextUpTiming.defaultValue
overrides =
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.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,8 +419,14 @@ fun PlaybackPage(
// Next up episode
BackHandler(nextUp != null) {
if (player.isPlaying) {
scope.launch(ExceptionHandler()) {
viewModel.cancelUpNextEpisode()
}
} else {
viewModel.navigationManager.goBack()
}
}
AnimatedVisibility(
nextUp != null,
modifier =
@ -470,7 +477,7 @@ fun PlaybackPage(
Modifier
.padding(8.dp)
// .height(128.dp)
.fillMaxHeight(.5f)
.fillMaxHeight(1 - playerSize)
.fillMaxWidth(.66f)
.align(Alignment.BottomCenter)
.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.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<TrickplayInfo?>(null)
val chapters = MutableLiveData<List<Chapter>>(listOf())
val currentSegment = EqualityMutableLiveData<MediaSegmentDto?>(null)
private val autoSkippedSegments = mutableSetOf<UUID>()
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,13 +659,25 @@ 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 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 =
when (currentSegment.type) {
MediaSegmentType.COMMERCIAL -> prefs.skipCommercials
@ -688,7 +695,8 @@ class PlaybackViewModel
}
SkipSegmentBehavior.ASK_TO_SKIP -> {
this@PlaybackViewModel.currentSegment.value = currentSegment
this@PlaybackViewModel.currentSegment.value =
currentSegment
}
else -> {
@ -696,7 +704,8 @@ class PlaybackViewModel
}
}
}
} else {
}
} else if (currentSegment == null) {
withContext(Dispatchers.Main) {
this@PlaybackViewModel.currentSegment.value = null
}

View file

@ -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{

View file

@ -37,4 +37,9 @@
<item>Never use FFmpeg decoders</item>
</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>

View file

@ -100,5 +100,7 @@
<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_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>