Suport passout protection (#41)

Adds a setting to configure pass out protection which, when enabled,
will stop auto playing next up episodes if the user hasn't interacted
with the app within the configurable 1-3 hours.
This commit is contained in:
damontecres 2025-10-19 18:21:23 -04:00 committed by GitHub
parent c3cc284238
commit 8904360fb5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 56 additions and 7 deletions

View file

@ -8,6 +8,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
import com.github.damontecres.wholphin.ui.preferences.PreferenceValidation import com.github.damontecres.wholphin.ui.preferences.PreferenceValidation
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -267,6 +268,28 @@ sealed interface AppPreference<T> {
}, },
) )
val PassOutProtection =
AppSliderPreference(
title = R.string.pass_out_protection,
defaultValue = 2,
min = 0,
max = 3,
interval = 1,
getter = { it.playbackPreferences.passOutProtectionMs.milliseconds.inWholeHours },
setter = { prefs, value ->
prefs.updatePlaybackPreferences {
passOutProtectionMs = value.hours.inWholeMilliseconds
}
},
summarizer = { value ->
if (value == 0L) {
"Disabled"
} else {
"$value hours"
}
},
)
private const val MEGA_BIT = 1024 * 1024L private const val MEGA_BIT = 1024 * 1024L
const val DEFAULT_BITRATE = 20 * MEGA_BIT const val DEFAULT_BITRATE = 20 * MEGA_BIT
private val bitrateValues = private val bitrateValues =
@ -529,6 +552,7 @@ val basicPreferences =
AppPreference.ControllerTimeout, AppPreference.ControllerTimeout,
AppPreference.AutoPlayNextUp, AppPreference.AutoPlayNextUp,
AppPreference.AutoPlayNextDelay, AppPreference.AutoPlayNextDelay,
AppPreference.PassOutProtection,
AppPreference.SkipBackOnResume, AppPreference.SkipBackOnResume,
), ),
), ),

View file

@ -8,6 +8,7 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class AppPreferencesSerializer class AppPreferencesSerializer
@ -42,6 +43,8 @@ class AppPreferencesSerializer
skipCommercials = AppPreference.SkipCommercials.defaultValue skipCommercials = AppPreference.SkipCommercials.defaultValue
skipPreviews = AppPreference.SkipPreviews.defaultValue skipPreviews = AppPreference.SkipPreviews.defaultValue
skipRecaps = AppPreference.SkipRecaps.defaultValue skipRecaps = AppPreference.SkipRecaps.defaultValue
passOutProtectionMs =
AppPreference.PassOutProtection.defaultValue.hours.inWholeMilliseconds
overrides = overrides =
PlaybackOverrides PlaybackOverrides

View file

@ -23,8 +23,11 @@ class PlaybackKeyHandler(
private val controllerViewState: ControllerViewState, private val controllerViewState: ControllerViewState,
private val updateSkipIndicator: (Long) -> Unit, private val updateSkipIndicator: (Long) -> Unit,
private val skipBackOnResume: Duration?, private val skipBackOnResume: Duration?,
private val onInteraction: () -> Unit,
) { ) {
fun onKeyEvent(it: KeyEvent): Boolean { fun onKeyEvent(it: KeyEvent): Boolean {
if (it.type == KeyEventType.KeyUp) onInteraction.invoke()
var result = true var result = true
if (!controlsEnabled) { if (!controlsEnabled) {
result = false result = false

View file

@ -196,6 +196,7 @@ fun PlaybackPage(
controllerViewState = controllerViewState, controllerViewState = controllerViewState,
updateSkipIndicator = updateSkipIndicator, updateSkipIndicator = updateSkipIndicator,
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume, skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
onInteraction = viewModel::reportInteraction,
) )
val showSegment = val showSegment =
@ -413,12 +414,7 @@ fun PlaybackPage(
.align(Alignment.BottomCenter), .align(Alignment.BottomCenter),
) { ) {
nextUp?.let { nextUp?.let {
var autoPlayEnabled by var autoPlayEnabled by remember { mutableStateOf(viewModel.shouldAutoPlayNextUp()) }
remember {
mutableStateOf(
preferences.appPreferences.playbackPreferences.autoPlayNext,
)
}
var timeLeft by remember { var timeLeft by remember {
mutableLongStateOf( mutableLongStateOf(
preferences.appPreferences.playbackPreferences.autoPlayNextDelaySeconds, preferences.appPreferences.playbackPreferences.autoPlayNextDelaySeconds,
@ -452,7 +448,10 @@ fun PlaybackPage(
description = it.data.overview, description = it.data.overview,
imageUrl = it.imageUrl, imageUrl = it.imageUrl,
aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9), aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
onClick = { viewModel.playUpNextUp() }, onClick = {
viewModel.reportInteraction()
viewModel.playUpNextUp()
},
timeLeft = if (autoPlayEnabled) timeLeft.seconds else null, timeLeft = if (autoPlayEnabled) timeLeft.seconds else null,
modifier = modifier =
Modifier Modifier

View file

@ -74,6 +74,7 @@ import org.jellyfin.sdk.model.extensions.inWholeTicks
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
import java.util.Date
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration import kotlin.time.Duration
@ -649,6 +650,23 @@ class PlaybackViewModel
} }
} }
private var lastInteractionDate: Date = Date()
fun reportInteraction() {
Timber.v("reportInteraction")
lastInteractionDate = Date()
}
fun shouldAutoPlayNextUp(): Boolean =
preferences.appPreferences.playbackPreferences.let {
it.autoPlayNext &&
if (it.passOutProtectionMs > 0) {
(Date().time - lastInteractionDate.time) < it.passOutProtectionMs
} else {
true
}
}
fun playUpNextUp() { fun playUpNextUp() {
playlist.value?.let { playlist.value?.let {
if (it.hasNext()) { if (it.hasNext()) {

View file

@ -34,6 +34,7 @@ message PlaybackPreferences {
SkipSegmentBehavior skip_previews = 14; SkipSegmentBehavior skip_previews = 14;
PlaybackOverrides overrides = 15; PlaybackOverrides overrides = 15;
int64 pass_out_protection_ms = 16;
} }
message HomePagePreferences{ message HomePagePreferences{

View file

@ -83,5 +83,6 @@
<string name="direct_play_ass">Direct play ASS subtitles</string> <string name="direct_play_ass">Direct play ASS subtitles</string>
<string name="direct_play_pgs">Direct play PGS subtitles</string> <string name="direct_play_pgs">Direct play PGS subtitles</string>
<string name="trailers">Trailers</string> <string name="trailers">Trailers</string>
<string name="pass_out_protection">Passout Protection</string>
</resources> </resources>