diff --git a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt index e4b829fa..cb35f8cf 100644 --- a/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt +++ b/app/src/main/kotlin/com/subtracks/data/prefs/UserPreferences.kt @@ -174,6 +174,14 @@ class UserPreferences( private val replayGainPreampKey = floatPreferencesKey("replay_gain_preamp_db") + fun keepScreenOn(): Flow = 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 = store.data.map { prefs -> prefs[offlineModeKey] ?: false } suspend fun setOfflineMode(enabled: Boolean) { diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt index 290bcf41..785520f7 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt @@ -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 }, diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt index 131f5be2..04607211 100644 --- a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsScreen.kt @@ -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( diff --git a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt index f59011d7..16caac8f 100644 --- a/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/settings/SettingsViewModel.kt @@ -76,6 +76,11 @@ class SettingsViewModel( .replayGainPreampDb() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), 0f) + val keepScreenOn: StateFlow = + userPreferences + .keepScreenOn() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false) + val offline: StateFlow = sourceRepository.offline val verboseLogging: StateFlow = @@ -134,4 +139,8 @@ class SettingsViewModel( fun setReplayGainPreamp(value: Float) { viewModelScope.launch { userPreferences.setReplayGainPreampDb(value) } } + + fun setKeepScreenOn(enabled: Boolean) { + viewModelScope.launch { userPreferences.setKeepScreenOn(enabled) } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 469a461e..b4c6e1a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,6 +85,8 @@ %1$s dB Normalise loudness using the ReplayGain tags from the server ReplayGain + Keep the screen awake while now playing is open + Keep screen on Network Stream quality: Mobile Stream quality: Wi-Fi diff --git a/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt b/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt index 58aa5252..ae540e09 100644 --- a/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/prefs/UserPreferencesTest.kt @@ -155,6 +155,24 @@ class UserPreferencesTest { file.delete() } + @Test + fun keepScreenOnDefaultsOffAndRoundTrips() = + runTest { + val file = File.createTempFile("user-prefs", ".preferences_pb").apply { delete() } + val store = PreferenceDataStoreFactory.create(scope = backgroundScope) { file } + val prefs = UserPreferences(store) + + assertEquals(false, prefs.keepScreenOn().first()) + + prefs.setKeepScreenOn(true) + assertEquals(true, prefs.keepScreenOn().first()) + + prefs.setKeepScreenOn(false) + assertEquals(false, prefs.keepScreenOn().first()) + + file.delete() + } + @Test fun lastSeedRoundTripsWithAndWithoutASecondary() = runTest { diff --git a/app/src/test/kotlin/com/subtracks/ui/settings/SettingsScreenTest.kt b/app/src/test/kotlin/com/subtracks/ui/settings/SettingsScreenTest.kt new file mode 100644 index 00000000..094ac9e5 --- /dev/null +++ b/app/src/test/kotlin/com/subtracks/ui/settings/SettingsScreenTest.kt @@ -0,0 +1,73 @@ +package com.subtracks.ui.settings + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.hasScrollToNodeAction +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToNode +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers +import com.subtracks.data.prefs.StreamQuality +import com.subtracks.ui.theme.SubtracksTheme +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import org.robolectric.annotation.GraphicsMode + +@RunWith(AndroidJUnit4::class) +@GraphicsMode(GraphicsMode.Mode.NATIVE) +@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL) +class SettingsScreenTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun theKeepScreenOnRowToggles() { + var requested: Boolean? = null + composeRule.setContent { + SubtracksTheme { + SettingsScreen( + sources = emptyList(), + activeSourceId = null, + wifiQuality = StreamQuality(), + mobileQuality = StreamQuality(), + syncConcurrency = 1, + downloadQuality = StreamQuality(), + downloadOverMetered = false, + scrobbling = false, + keepScreenOn = false, + onSelectSource = {}, + onEditServer = {}, + onWifiQualityChange = {}, + onMobileQualityChange = {}, + onSyncConcurrencyChange = {}, + onDownloadQualityChange = {}, + onDownloadOverMeteredChange = {}, + onScrobblingChange = {}, + onKeepScreenOnChange = { requested = it }, + onAddServer = {}, + onOpenDownloads = {}, + onOpenLicenses = {}, + onShareLogs = {}, + onBack = {}, + ) + } + } + awaitText("Add server") + composeRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText("Keep screen on")) + composeRule.onNodeWithText("Keep screen on").performClick() + composeRule.waitForIdle() + assertEquals(true, requested) + } + + private fun awaitText(text: String) { + composeRule.waitUntil(timeoutMillis = 5_000) { + composeRule.onAllNodesWithText(text).fetchSemanticsNodes().isNotEmpty() + } + } +}