mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
Compare commits
3
Commits
b2c66bf03e
...
fcfc5561ae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcfc5561ae | ||
|
|
a562e4b316 | ||
|
|
538cb4177a |
@@ -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 remember { 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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,8 @@
|
||||
<string name="settings_replay_gain_preamp_value">%1$s dB</string>
|
||||
<string name="settings_replay_gain_prompt">Normalise loudness using the ReplayGain tags from the server</string>
|
||||
<string name="settings_replay_gain_title">ReplayGain</string>
|
||||
<string name="settings_music_options_keep_screen_on_description">Keep the screen on while now playing is open</string>
|
||||
<string name="settings_music_options_keep_screen_on_title">Keep screen on</string>
|
||||
<string name="settings_network_name">Network</string>
|
||||
<string name="settings_network_options_max_bitrate_mobile_title">Stream quality: Mobile</string>
|
||||
<string name="settings_network_options_max_bitrate_wifi_title">Stream quality: Wi-Fi</string>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.subtracks.ui.playback
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.playback.PlaybackState
|
||||
import com.subtracks.playback.QueueItem
|
||||
import com.subtracks.ui.theme.SubtracksTheme
|
||||
import com.subtracks.ui.theme.artworkColorsFromSeed
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
class NowPlayingKeepScreenOnTest {
|
||||
@get:Rule
|
||||
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
private val state =
|
||||
PlaybackState(
|
||||
item =
|
||||
QueueItem(
|
||||
id = "s-eiirp",
|
||||
title = "Everything In Its Right Place",
|
||||
artist = "Radiohead",
|
||||
album = "Kid A",
|
||||
coverArtId = "art-al-kid-a",
|
||||
),
|
||||
isPlaying = true,
|
||||
durationMs = 251_000,
|
||||
hasNext = true,
|
||||
hasPrevious = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun theScreenOnFlagTracksTheSetting() {
|
||||
var keepScreenOn by mutableStateOf(true)
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
NowPlayingScreen(
|
||||
state = state,
|
||||
positionMs = 62_000,
|
||||
title = "Kid A",
|
||||
coverArt = CoverArtRef("art-al-kid-a", "test:art-al-kid-a"),
|
||||
artwork = artworkColorsFromSeed(android.graphics.Color.rgb(120, 80, 200)),
|
||||
onBack = {},
|
||||
onQueue = {},
|
||||
onPlayPause = {},
|
||||
onNext = {},
|
||||
onPrevious = {},
|
||||
onSeek = {},
|
||||
keepScreenOn = keepScreenOn,
|
||||
)
|
||||
}
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
assertNotNull("now playing should hold the screen on", viewHoldingTheScreenOn())
|
||||
|
||||
composeRule.runOnIdle { keepScreenOn = false }
|
||||
composeRule.waitForIdle()
|
||||
assertNull("clearing the setting should release the screen-on flag", viewHoldingTheScreenOn())
|
||||
}
|
||||
|
||||
private fun viewHoldingTheScreenOn(): View? = findKeepingScreenOn(composeRule.activity.window.decorView)
|
||||
|
||||
private fun findKeepingScreenOn(view: View): View? {
|
||||
if (view.keepScreenOn) return view
|
||||
if (view is ViewGroup) {
|
||||
for (index in 0 until view.childCount) {
|
||||
findKeepingScreenOn(view.getChildAt(index))?.let { return it }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.subtracks.ui.settings
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.hasAnySibling
|
||||
import androidx.compose.ui.test.hasScrollToNodeAction
|
||||
import androidx.compose.ui.test.hasText
|
||||
import androidx.compose.ui.test.isToggleable
|
||||
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<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun theKeepScreenOnRowTurnsTheSettingOn() {
|
||||
var requested: Boolean? = null
|
||||
renderSettings(keepScreenOn = false) { requested = it }
|
||||
|
||||
composeRule.onNodeWithText("Keep screen on").performClick()
|
||||
composeRule.waitForIdle()
|
||||
|
||||
assertEquals(true, requested)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theKeepScreenOnRowTurnsTheSettingOff() {
|
||||
var requested: Boolean? = null
|
||||
renderSettings(keepScreenOn = true) { requested = it }
|
||||
|
||||
composeRule.onNodeWithText("Keep screen on").performClick()
|
||||
composeRule.waitForIdle()
|
||||
|
||||
assertEquals(false, requested)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theKeepScreenOnSwitchTurnsTheSettingOn() {
|
||||
var requested: Boolean? = null
|
||||
renderSettings(keepScreenOn = false) { requested = it }
|
||||
|
||||
composeRule
|
||||
.onNode(isToggleable() and hasAnySibling(hasText("Keep screen on")), useUnmergedTree = true)
|
||||
.performClick()
|
||||
composeRule.waitForIdle()
|
||||
|
||||
assertEquals(true, requested)
|
||||
}
|
||||
|
||||
private fun renderSettings(
|
||||
keepScreenOn: Boolean,
|
||||
onKeepScreenOnChange: (Boolean) -> Unit,
|
||||
) {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
SettingsScreen(
|
||||
sources = emptyList(),
|
||||
activeSourceId = null,
|
||||
wifiQuality = StreamQuality(),
|
||||
mobileQuality = StreamQuality(),
|
||||
syncConcurrency = 1,
|
||||
downloadQuality = StreamQuality(),
|
||||
downloadOverMetered = false,
|
||||
scrobbling = false,
|
||||
keepScreenOn = keepScreenOn,
|
||||
onSelectSource = {},
|
||||
onEditServer = {},
|
||||
onWifiQualityChange = {},
|
||||
onMobileQualityChange = {},
|
||||
onSyncConcurrencyChange = {},
|
||||
onDownloadQualityChange = {},
|
||||
onDownloadOverMeteredChange = {},
|
||||
onScrobblingChange = {},
|
||||
onKeepScreenOnChange = onKeepScreenOnChange,
|
||||
onAddServer = {},
|
||||
onOpenDownloads = {},
|
||||
onOpenLicenses = {},
|
||||
onShareLogs = {},
|
||||
onBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
awaitText("Add server")
|
||||
composeRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText("Keep screen on"))
|
||||
}
|
||||
|
||||
private fun awaitText(text: String) {
|
||||
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||
composeRule.onAllNodesWithText(text).fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user