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
This commit is contained in:
Damontecres 2026-08-17 22:53:40 -04:00 • committed by GitHub
parent 197af604d6
commit c4264a3263
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 144 additions and 9 deletions

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -136,12 +137,26 @@ fun MainContent(
val screenSaverState by screensaverService.state.collectAsState() val screenSaverState by screensaverService.state.collectAsState()
if (screenSaverState.enabled || screenSaverState.enabledTemp) { if (screenSaverState.enabled || screenSaverState.enabledTemp) {
AnimatedVisibility( AnimatedVisibility(
screenSaverState.show, visible = screenSaverState.show,
Modifier.fillMaxSize(), enter = ScreensaverService.enterAnimation,
exit = ScreensaverService.exitAnimation,
modifier = Modifier.fillMaxSize(),
) { ) {
AppScreensaver(userPreferences.appPreferences, 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)),
)
}
} }
} }
} }

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -94,6 +95,14 @@ class WholphinDreamService :
preferencesDataStore.data.collectLatest { prefs = it } preferencesDataStore.data.collectLatest { prefs = it }
} }
prefs?.let { prefs -> prefs?.let { prefs ->
val alpha =
prefs.interfacePreferences.screensaverPreference.run {
if (dimEnabled) {
dimPercent / 100f
} else {
1f
}
}
CoilConfig( CoilConfig(
prefs = prefs, prefs = prefs,
okHttpClient = okHttpClient, okHttpClient = okHttpClient,
@ -109,7 +118,10 @@ class WholphinDreamService :
showClock = screensaverPrefs.showClock, showClock = screensaverPrefs.showClock,
duration = screensaverPrefs.duration.milliseconds, duration = screensaverPrefs.duration.milliseconds,
animate = screensaverPrefs.animate, animate = screensaverPrefs.animate,
modifier = Modifier.fillMaxSize(), modifier =
Modifier
.fillMaxSize()
.alpha(alpha = alpha),
) )
} }
} }

View file

@ -1338,6 +1338,14 @@ val screensaverPreferences =
ScreensaverPreference.Start, ScreensaverPreference.Start,
), ),
), ),
PreferenceGroup(
title = R.string.dim_screen,
preferences =
listOf(
ScreensaverPreference.DimToggle,
ScreensaverPreference.DimPercentage,
),
),
) )
data class AppSwitchPreference<Pref>( data class AppSwitchPreference<Pref>(

View file

@ -176,6 +176,37 @@ object ScreensaverPreference {
}, },
) )
val DimToggle =
AppSwitchPreference<AppPreferences>(
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<AppPreferences>(
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 = val Start =
AppClickablePreference<AppPreferences>( AppClickablePreference<AppPreferences>(
title = R.string.start_screensaver, title = R.string.start_screensaver,

View file

@ -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()
}
}
}
} }
} }

View file

@ -1,6 +1,9 @@
package com.github.damontecres.wholphin.services package com.github.damontecres.wholphin.services
import android.content.Context 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.imageLoader
import coil3.request.ImageRequest import coil3.request.ImageRequest
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
@ -57,6 +60,7 @@ class ScreensaverService
val keepScreenOn = MutableStateFlow(false) val keepScreenOn = MutableStateFlow(false)
private var waitJob: Job? = null private var waitJob: Job? = null
private var dimJob: Job? = null
init { init {
userPreferencesService.flow userPreferencesService.flow
@ -65,7 +69,14 @@ class ScreensaverService
val enabled = val enabled =
prefs.appPreferences.interfacePreferences.screensaverPreference.enabled prefs.appPreferences.interfacePreferences.screensaverPreference.enabled
keepScreenOnInternal(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) }.launchIn(scope)
} }
@ -74,12 +85,16 @@ class ScreensaverService
* Reset the timer before showing the in-app screensaver * Reset the timer before showing the in-app screensaver
*/ */
fun pulse() { fun pulse() {
Timber.v("state=%s", _state.value)
waitJob?.cancel() waitJob?.cancel()
if (_state.value.enabled) { if (_state.value.enabled) {
// Timber.v("pulse") // Timber.v("pulse")
_state.update { _state.update {
if (!it.active) { if (!it.active) {
it.copy(active = false) it.copy(
active = false,
dimActive = false,
)
} else { } else {
it it
} }
@ -94,7 +109,30 @@ class ScreensaverService
.appPreferences.interfacePreferences.screensaverPreference.startDelay.milliseconds .appPreferences.interfacePreferences.screensaverPreference.startDelay.milliseconds
delay(startDelay) delay(startDelay)
_state.update { _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( it.copy(
enabledTemp = true, enabledTemp = true,
active = true, active = true,
dimActive = true,
) )
} }
} }
@ -121,9 +160,13 @@ class ScreensaverService
it.copy( it.copy(
enabledTemp = false, enabledTemp = false,
active = 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) { fun keepScreenOn(keep: Boolean) {
scope.launchDefault { scope.launchDefault {
val screensaverEnabled = _state.value.enabled val screensaverEnabled = _state.value.run { enabled || dimEnabled }
Timber.d("Keep screen on: %s, screensaverEnabled=%s", keep, screensaverEnabled) Timber.d("Keep screen on: %s, screensaverEnabled=%s", keep, screensaverEnabled)
if (screensaverEnabled) { if (screensaverEnabled) {
// Page is requesting to keep screen on, so we don't wait to show the screensaver // Page is requesting to keep screen on, so we don't wait to show the screensaver
_state.update { _state.update {
it.copy(active = false, paused = keep) it.copy(
active = false,
dimActive = false,
paused = keep,
)
} }
if (!keep) { if (!keep) {
pulse() pulse()
@ -245,6 +292,11 @@ class ScreensaverService
) )
return ApiRequestPager(api, request, GetItemsRequestHandler, scope).init() 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( data class ScreensaverState(
@ -252,6 +304,10 @@ data class ScreensaverState(
val enabledTemp: Boolean, val enabledTemp: Boolean,
val active: Boolean, val active: Boolean,
val paused: Boolean, val paused: Boolean,
val dimEnabled: Boolean = false,
val dimActive: Boolean = false,
) { ) {
val show get() = (enabled || enabledTemp) && active && !paused val show get() = (enabled || enabledTemp) && active && !paused
val showDim get() = dimEnabled && dimActive && !paused
} }

View file

@ -174,6 +174,8 @@ message ScreensaverPreferences{
bool enabled = 5; bool enabled = 5;
repeated string item_types = 6; repeated string item_types = 6;
bool show_clock = 7; bool show_clock = 7;
bool dim_enabled = 8;
int32 dim_percent = 9;
} }
message SearchPreferences { message SearchPreferences {

View file

@ -910,4 +910,7 @@
<string name="audio_books">Audio books</string> <string name="audio_books">Audio books</string>
<string name="server_version_not_supported">Server version not supported</string> <string name="server_version_not_supported">Server version not supported</string>
<string name="appears_on">Appears on</string> <string name="appears_on">Appears on</string>
<string name="dim_after_delay">Dim screen after delay</string>
<string name="dim_percentage">Dim percentage</string>
<string name="dim_screen">Dim screen</string>
</resources> </resources>