From c4264a32634308f47915f602e63bd8165b00fd52 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 17 Aug 2026 22:53:40 -0400 Subject: [PATCH] Adds option to dim the screensaver (#1834) ## Description Adds a new sceensaver setting to add dimming. A larger value is darker This works with or without the in-app screensaver. If dimming is enabled, the screen be will be dimmed after inactivity. If both dimming and the in-app screensaver are enabled, the screensaver will show dimmed after inactivity. ### Related issues Closes #1179 ### Testing Emulator ## Screenshots N/A, it's just darker ## AI or LLM usage None --- .../damontecres/wholphin/MainContent.kt | 19 +++++- .../wholphin/WholphinDreamService.kt | 14 +++- .../wholphin/preferences/AppPreference.kt | 8 +++ .../preferences/ScreensaverPreference.kt | 31 +++++++++ .../wholphin/services/AppUpgradeHandler.kt | 8 +++ .../wholphin/services/ScreensaverService.kt | 68 +++++++++++++++++-- app/src/main/proto/WholphinDataStore.proto | 2 + app/src/main/res/values/strings.xml | 3 + 8 files changed, 144 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt index 78545930..25221874 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainContent.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -136,12 +137,26 @@ fun MainContent( val screenSaverState by screensaverService.state.collectAsState() if (screenSaverState.enabled || screenSaverState.enabledTemp) { AnimatedVisibility( - screenSaverState.show, - Modifier.fillMaxSize(), + visible = screenSaverState.show, + enter = ScreensaverService.enterAnimation, + exit = ScreensaverService.exitAnimation, + modifier = Modifier.fillMaxSize(), ) { AppScreensaver(userPreferences.appPreferences, Modifier.fillMaxSize()) } } + AnimatedVisibility( + visible = screenSaverState.showDim || (screenSaverState.dimEnabled && screenSaverState.show), + enter = ScreensaverService.enterAnimation, + exit = ScreensaverService.exitAnimation, + modifier = Modifier.fillMaxSize(), + ) { + val alpha = + userPreferences.appPreferences.interfacePreferences.screensaverPreference.dimPercent / 100f + Box( + Modifier.background(Color.Black.copy(alpha = alpha)), + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt index a281b101..41a87222 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/WholphinDreamService.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.ComposeView import androidx.datastore.core.DataStore import androidx.lifecycle.Lifecycle @@ -94,6 +95,14 @@ class WholphinDreamService : preferencesDataStore.data.collectLatest { prefs = it } } prefs?.let { prefs -> + val alpha = + prefs.interfacePreferences.screensaverPreference.run { + if (dimEnabled) { + dimPercent / 100f + } else { + 1f + } + } CoilConfig( prefs = prefs, okHttpClient = okHttpClient, @@ -109,7 +118,10 @@ class WholphinDreamService : showClock = screensaverPrefs.showClock, duration = screensaverPrefs.duration.milliseconds, animate = screensaverPrefs.animate, - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .alpha(alpha = alpha), ) } } 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 a3f5b660..43d2d53a 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 @@ -1338,6 +1338,14 @@ val screensaverPreferences = ScreensaverPreference.Start, ), ), + PreferenceGroup( + title = R.string.dim_screen, + preferences = + listOf( + ScreensaverPreference.DimToggle, + ScreensaverPreference.DimPercentage, + ), + ), ) data class AppSwitchPreference( diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/ScreensaverPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/ScreensaverPreference.kt index 1e1ad91c..04fb4a63 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/ScreensaverPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/ScreensaverPreference.kt @@ -176,6 +176,37 @@ object ScreensaverPreference { }, ) + val DimToggle = + AppSwitchPreference( + title = R.string.dim_after_delay, + defaultValue = AppPreference.ShowClock.defaultValue, + getter = { it.interfacePreferences.screensaverPreference.dimEnabled }, + setter = { prefs, value -> + prefs.updateScreensaverPreferences { dimEnabled = value } + }, + summaryOn = R.string.yes, + summaryOff = R.string.no, + ) + + val DimPercentage = + AppSliderPreference( + title = R.string.dim_percentage, + defaultValue = 75, + min = 0, + max = 95, + interval = 5, + getter = { + it.interfacePreferences.screensaverPreference.dimPercent + .toLong() + }, + setter = { prefs, value -> + prefs.updateScreensaverPreferences { dimPercent = value.toInt() } + }, + summarizer = { value -> + value?.let { "$value%" } + }, + ) + val Start = AppClickablePreference( title = R.string.start_screensaver, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index cc814210..29348be6 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -418,5 +418,13 @@ class AppUpgradeHandler } } } + + if (previous.isEqualOrBefore(Version.fromString("1.0.6-0-g0"))) { + appPreferences.updateData { + it.updateScreensaverPreferences { + dimPercent = ScreensaverPreference.DimPercentage.defaultValue.toInt() + } + } + } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt index 871a20e2..90b0c04a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/ScreensaverService.kt @@ -1,6 +1,9 @@ package com.github.damontecres.wholphin.services import android.content.Context +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import coil3.imageLoader import coil3.request.ImageRequest import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope @@ -57,6 +60,7 @@ class ScreensaverService val keepScreenOn = MutableStateFlow(false) private var waitJob: Job? = null + private var dimJob: Job? = null init { userPreferencesService.flow @@ -65,7 +69,14 @@ class ScreensaverService val enabled = prefs.appPreferences.interfacePreferences.screensaverPreference.enabled keepScreenOnInternal(enabled) - ScreensaverState(enabled, false, false, false) + ScreensaverState( + enabled = enabled, + enabledTemp = false, + active = false, + paused = false, + dimEnabled = prefs.appPreferences.interfacePreferences.screensaverPreference.dimEnabled, + dimActive = false, + ) } }.launchIn(scope) } @@ -74,12 +85,16 @@ class ScreensaverService * Reset the timer before showing the in-app screensaver */ fun pulse() { + Timber.v("state=%s", _state.value) waitJob?.cancel() if (_state.value.enabled) { // Timber.v("pulse") _state.update { if (!it.active) { - it.copy(active = false) + it.copy( + active = false, + dimActive = false, + ) } else { it } @@ -94,7 +109,30 @@ class ScreensaverService .appPreferences.interfacePreferences.screensaverPreference.startDelay.milliseconds delay(startDelay) _state.update { - it.copy(active = true) + it.copy( + active = true, + dimActive = it.dimEnabled, + ) + } + } + } + } + dimJob?.cancel() + if (_state.value.dimEnabled) { + _state.update { + it.copy(dimActive = false) + } + if (!_state.value.paused) { + dimJob = + scope.launch(ExceptionHandler()) { + val startDelay = + userPreferencesService + .getCurrent() + .appPreferences.interfacePreferences.screensaverPreference.startDelay.milliseconds + delay(startDelay) + Timber.v("state dim=%s", _state.value) + _state.update { + it.copy(dimActive = !_state.value.paused) } } } @@ -109,6 +147,7 @@ class ScreensaverService it.copy( enabledTemp = true, active = true, + dimActive = true, ) } } @@ -121,9 +160,13 @@ class ScreensaverService it.copy( enabledTemp = false, active = false, + dimActive = false, ) } - if (cancelJob) waitJob?.cancel() + if (cancelJob) { + waitJob?.cancel() + dimJob?.cancel() + } } /** @@ -131,12 +174,16 @@ class ScreensaverService */ fun keepScreenOn(keep: Boolean) { scope.launchDefault { - val screensaverEnabled = _state.value.enabled + val screensaverEnabled = _state.value.run { enabled || dimEnabled } Timber.d("Keep screen on: %s, screensaverEnabled=%s", keep, screensaverEnabled) if (screensaverEnabled) { // Page is requesting to keep screen on, so we don't wait to show the screensaver _state.update { - it.copy(active = false, paused = keep) + it.copy( + active = false, + dimActive = false, + paused = keep, + ) } if (!keep) { pulse() @@ -245,6 +292,11 @@ class ScreensaverService ) return ApiRequestPager(api, request, GetItemsRequestHandler, scope).init() } + + companion object { + val enterAnimation = fadeIn(animationSpec = tween(durationMillis = 1000)) + val exitAnimation = fadeOut(animationSpec = tween(durationMillis = 500)) + } } data class ScreensaverState( @@ -252,6 +304,10 @@ data class ScreensaverState( val enabledTemp: Boolean, val active: Boolean, val paused: Boolean, + val dimEnabled: Boolean = false, + val dimActive: Boolean = false, ) { val show get() = (enabled || enabledTemp) && active && !paused + + val showDim get() = dimEnabled && dimActive && !paused } diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index f190b43b..98698f7c 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -174,6 +174,8 @@ message ScreensaverPreferences{ bool enabled = 5; repeated string item_types = 6; bool show_clock = 7; + bool dim_enabled = 8; + int32 dim_percent = 9; } message SearchPreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3b1bd402..990d28f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -910,4 +910,7 @@ Audio books Server version not supported Appears on + Dim screen after delay + Dim percentage + Dim screen