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.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)),
)
}
}
}
}

View file

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

View file

@ -1338,6 +1338,14 @@ val screensaverPreferences =
ScreensaverPreference.Start,
),
),
PreferenceGroup(
title = R.string.dim_screen,
preferences =
listOf(
ScreensaverPreference.DimToggle,
ScreensaverPreference.DimPercentage,
),
),
)
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 =
AppClickablePreference<AppPreferences>(
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
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
}

View file

@ -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 {

View file

@ -910,4 +910,7 @@
<string name="audio_books">Audio books</string>
<string name="server_version_not_supported">Server version not supported</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>