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.PreferenceScreenOption
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.minutes
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
const val DEFAULT_BITRATE = 20 * MEGA_BIT
private val bitrateValues =
@ -529,6 +552,7 @@ val basicPreferences =
AppPreference.ControllerTimeout,
AppPreference.AutoPlayNextUp,
AppPreference.AutoPlayNextDelay,
AppPreference.PassOutProtection,
AppPreference.SkipBackOnResume,
),
),

View file

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

View file

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

View file

@ -196,6 +196,7 @@ fun PlaybackPage(
controllerViewState = controllerViewState,
updateSkipIndicator = updateSkipIndicator,
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
onInteraction = viewModel::reportInteraction,
)
val showSegment =
@ -413,12 +414,7 @@ fun PlaybackPage(
.align(Alignment.BottomCenter),
) {
nextUp?.let {
var autoPlayEnabled by
remember {
mutableStateOf(
preferences.appPreferences.playbackPreferences.autoPlayNext,
)
}
var autoPlayEnabled by remember { mutableStateOf(viewModel.shouldAutoPlayNextUp()) }
var timeLeft by remember {
mutableLongStateOf(
preferences.appPreferences.playbackPreferences.autoPlayNextDelaySeconds,
@ -452,7 +448,10 @@ fun PlaybackPage(
description = it.data.overview,
imageUrl = it.imageUrl,
aspectRatio = it.data.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
onClick = { viewModel.playUpNextUp() },
onClick = {
viewModel.reportInteraction()
viewModel.playUpNextUp()
},
timeLeft = if (autoPlayEnabled) timeLeft.seconds else null,
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.serializer.toUUIDOrNull
import timber.log.Timber
import java.util.Date
import java.util.UUID
import javax.inject.Inject
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() {
playlist.value?.let {
if (it.hasNext()) {

View file

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

View file

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