From 8904360fb5af3df960060aa6459f06c37b50e848 Mon Sep 17 00:00:00 2001 From: damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:21:23 -0400 Subject: [PATCH] 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. --- .../wholphin/preferences/AppPreference.kt | 24 +++++++++++++++++++ .../preferences/AppPreferencesSerializer.kt | 3 +++ .../ui/playback/PlaybackKeyHandler.kt | 3 +++ .../wholphin/ui/playback/PlaybackPage.kt | 13 +++++----- .../wholphin/ui/playback/PlaybackViewModel.kt | 18 ++++++++++++++ app/src/main/proto/WholphinDataStore.proto | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 56 insertions(+), 7 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 271e2a38..68e89992 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 @@ -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 { }, ) + 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, ), ), 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 c0289afe..f10e15f7 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 @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt index 00e1333f..7420b038 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackKeyHandler.kt @@ -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 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 d7688d0a..2f321f4b 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 @@ -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 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 e185cd46..8b14f4d8 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 @@ -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()) { diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index bc93e765..2f8613a9 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -34,6 +34,7 @@ message PlaybackPreferences { SkipSegmentBehavior skip_previews = 14; PlaybackOverrides overrides = 15; + int64 pass_out_protection_ms = 16; } message HomePagePreferences{ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 496b123c..e6639fc4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,5 +83,6 @@ Direct play ASS subtitles Direct play PGS subtitles Trailers + Passout Protection