feat: add a keep-screen-on toggle for now playing

A screen timeout mid-album is a common complaint for a music player: the
phone lies on a table while art or lyrics are on screen and the display
sleeps. Add a Settings toggle under Playback, off by default, that holds
the screen awake while now playing is open. The flag rides on the now
playing overlay through Compose's keepScreenOn modifier, and since the
overlay is only composed while expanded, collapsing it releases the flag
without any window-flag lifecycle handling.

Closes austinried/subtracks#201
This commit is contained in:
chiangww
2026-10-17 01:14:48 +00:00
committed by austinried
parent b2c66bf03e
commit 538cb4177a
7 changed files with 147 additions and 1 deletions
@@ -174,6 +174,14 @@ class UserPreferences(
private val replayGainPreampKey = floatPreferencesKey("replay_gain_preamp_db")
fun keepScreenOn(): Flow<Boolean> = store.data.map { prefs -> prefs[keepScreenOnKey] ?: false }
suspend fun setKeepScreenOn(enabled: Boolean) {
store.edit { prefs -> prefs[keepScreenOnKey] = enabled }
}
private val keepScreenOnKey = booleanPreferencesKey("keep_screen_on")
fun offlineMode(): Flow<Boolean> = store.data.map { prefs -> prefs[offlineModeKey] ?: false }
suspend fun setOfflineMode(enabled: Boolean) {
@@ -60,6 +60,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.keepScreenOn
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
@@ -76,6 +77,7 @@ import com.subtracks.R
import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.Song
import com.subtracks.data.prefs.UserPreferences
import com.subtracks.data.repo.DownloadRepository
import com.subtracks.data.repo.LibraryRepository
import com.subtracks.data.source.StarType
@@ -191,8 +193,10 @@ fun NowPlayingRoute(
controller: PlaybackController = koinInject(),
libraryRepository: LibraryRepository = koinInject(),
downloadRepository: DownloadRepository = koinInject(),
userPreferences: UserPreferences = koinInject(),
) {
val state by controller.state.collectAsStateWithLifecycle()
val keepScreenOn by userPreferences.keepScreenOn().collectAsStateWithLifecycle(initialValue = false)
val positionMs by controller.positionMs.collectAsStateWithLifecycle()
val sourceId = state.context?.sourceId
val songId = state.item?.id
@@ -272,6 +276,7 @@ fun NowPlayingRoute(
starred = song?.starred != null,
onToggleStar = song?.let { s -> { setStar(StarType.Song, s.id, s.starred == null) } },
onSeek = controller::seekTo,
keepScreenOn = keepScreenOn,
modifier = modifier,
)
}
@@ -290,6 +295,7 @@ fun NowPlayingScreen(
onNext: () -> Unit,
onPrevious: () -> Unit,
modifier: Modifier = Modifier,
keepScreenOn: Boolean = false,
onSkipPrevious: () -> Unit = onPrevious,
onShuffle: () -> Unit = {},
onRepeat: () -> Unit = {},
@@ -366,7 +372,12 @@ fun NowPlayingScreen(
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
ArtworkTheme(artwork) {
Box(modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) {
Box(
modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.then(if (keepScreenOn) Modifier.keepScreenOn() else Modifier),
) {
HeroGradient(
colors = artwork,
scrollPx = { 0f },
@@ -123,6 +123,7 @@ fun SettingsRoute(
val downloadQuality by viewModel.downloadQuality.collectAsStateWithLifecycle()
val downloadOverMetered by viewModel.downloadOverMetered.collectAsStateWithLifecycle()
val scrobbling by viewModel.scrobbling.collectAsStateWithLifecycle()
val keepScreenOn by viewModel.keepScreenOn.collectAsStateWithLifecycle()
val offline by viewModel.offline.collectAsStateWithLifecycle()
val verboseLogging by viewModel.verboseLogging.collectAsStateWithLifecycle()
val replayGainMode by viewModel.replayGainMode.collectAsStateWithLifecycle()
@@ -142,6 +143,7 @@ fun SettingsRoute(
scrobbling = scrobbling,
replayGainMode = replayGainMode,
replayGainPreamp = replayGainPreamp,
keepScreenOn = keepScreenOn,
offline = offline,
verboseLogging = verboseLogging,
onSelectSource = viewModel::selectSource,
@@ -156,6 +158,7 @@ fun SettingsRoute(
onScrobblingChange = viewModel::setScrobbling,
onReplayGainModeChange = viewModel::setReplayGainMode,
onReplayGainPreampChange = viewModel::setReplayGainPreamp,
onKeepScreenOnChange = viewModel::setKeepScreenOn,
onOfflineChange = viewModel::setOfflineMode,
onVerboseLoggingChange = viewModel::setVerboseLogging,
onAddServer = onAddServer,
@@ -195,6 +198,7 @@ fun SettingsScreen(
downloadOverMetered: Boolean,
scrobbling: Boolean,
modifier: Modifier = Modifier,
keepScreenOn: Boolean = false,
offline: Boolean = false,
verboseLogging: Boolean = false,
syncMode: SyncMode = SyncMode.WifiOnly,
@@ -213,6 +217,7 @@ fun SettingsScreen(
onScrobblingChange: (Boolean) -> Unit,
onReplayGainModeChange: (ReplayGainMode) -> Unit = {},
onReplayGainPreampChange: (Float) -> Unit = {},
onKeepScreenOnChange: (Boolean) -> Unit = {},
onOfflineChange: (Boolean) -> Unit = {},
onVerboseLoggingChange: (Boolean) -> Unit = {},
onAddServer: () -> Unit,
@@ -380,6 +385,26 @@ fun SettingsScreen(
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
item {
ListItem(
headlineContent = { Text(stringResource(R.string.settings_music_options_keep_screen_on_title)) },
supportingContent = { Text(stringResource(R.string.settings_music_options_keep_screen_on_description)) },
trailingContent = {
Switch(
checked = keepScreenOn,
onCheckedChange = onKeepScreenOnChange,
colors =
SwitchDefaults.colors(
uncheckedThumbColor = MaterialTheme.colorScheme.onSurfaceVariant,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
),
)
},
modifier = Modifier.clickable { onKeepScreenOnChange(!keepScreenOn) },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
item { SectionHeader(stringResource(R.string.downloads_title)) }
item {
ListItem(
@@ -76,6 +76,11 @@ class SettingsViewModel(
.replayGainPreampDb()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), 0f)
val keepScreenOn: StateFlow<Boolean> =
userPreferences
.keepScreenOn()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
val offline: StateFlow<Boolean> = sourceRepository.offline
val verboseLogging: StateFlow<Boolean> =
@@ -134,4 +139,8 @@ class SettingsViewModel(
fun setReplayGainPreamp(value: Float) {
viewModelScope.launch { userPreferences.setReplayGainPreampDb(value) }
}
fun setKeepScreenOn(enabled: Boolean) {
viewModelScope.launch { userPreferences.setKeepScreenOn(enabled) }
}
}