playback: make the song tap shuffle behaviour a preference

This commit is contained in:
austinried
2026-11-12 07:05:20 +00:00
parent d62bff5ace
commit e94b365495
13 changed files with 231 additions and 11 deletions
@@ -59,6 +59,8 @@ const val MAX_REPLAY_GAIN_PREAMP_DB = 15f
enum class SyncMode { Off, WifiOnly, Always } enum class SyncMode { Off, WifiOnly, Always }
enum class SongTapBehavior { InOrder, KeepShuffle, Shuffle }
enum class ReplayGainMode { Off, Track, Album, Auto } enum class ReplayGainMode { Off, Track, Album, Auto }
data class ArtworkSeedValue( data class ArtworkSeedValue(
@@ -129,6 +131,19 @@ class UserPreferences(
private val syncModeKey = stringPreferencesKey("sync_mode") private val syncModeKey = stringPreferencesKey("sync_mode")
fun songTapBehavior(): Flow<SongTapBehavior> =
store.data.map { prefs ->
prefs[songTapBehaviorKey]?.let { stored ->
SongTapBehavior.entries.firstOrNull { it.name == stored }
} ?: SongTapBehavior.InOrder
}
suspend fun setSongTapBehavior(behavior: SongTapBehavior) {
store.edit { prefs -> prefs[songTapBehaviorKey] = behavior.name }
}
private val songTapBehaviorKey = stringPreferencesKey("song_tap_behavior")
fun lastSyncAt(): Flow<Long> = store.data.map { prefs -> prefs[lastSyncAtKey] ?: 0L } fun lastSyncAt(): Flow<Long> = store.data.map { prefs -> prefs[lastSyncAtKey] ?: 0L }
suspend fun setLastSyncAt(value: Long) { suspend fun setLastSyncAt(value: Long) {
@@ -128,7 +128,7 @@ fun appModule(
single(createdAtStart = true) { DownloadNotifier(context.applicationContext, get(), scope = get()).also { it.start() } } single(createdAtStart = true) { DownloadNotifier(context.applicationContext, get(), scope = get()).also { it.start() } }
single(createdAtStart = true) { SyncNotifier(context.applicationContext, get<SyncManager>().status, scope = get()).also { it.start() } } single(createdAtStart = true) { SyncNotifier(context.applicationContext, get<SyncManager>().status, scope = get()).also { it.start() } }
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get(), get()) } single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get(), get()) }
single { PlaybackController(get(), get(), get(), get(), showMessage = toast, scope = get(named("playback"))) } single { PlaybackController(get(), get(), get(), get(), get(), showMessage = toast, scope = get(named("playback"))) }
single(createdAtStart = true) { single(createdAtStart = true) {
val playback = get<PlaybackController>() val playback = get<PlaybackController>()
Scrobbler( Scrobbler(
@@ -6,6 +6,8 @@ import com.subtracks.data.model.AudioEncoding
import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.QueueEntry import com.subtracks.data.model.QueueEntry
import com.subtracks.data.model.QueueKind import com.subtracks.data.model.QueueKind
import com.subtracks.data.prefs.SongTapBehavior
import com.subtracks.data.prefs.UserPreferences
import com.subtracks.data.repo.DownloadRepository import com.subtracks.data.repo.DownloadRepository
import com.subtracks.data.repo.QueueRepository import com.subtracks.data.repo.QueueRepository
import com.subtracks.data.repo.QueueSnapshot import com.subtracks.data.repo.QueueSnapshot
@@ -22,6 +24,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@@ -106,6 +109,7 @@ private data class ReloadUndo(
private data class PendingPlay( private data class PendingPlay(
val entry: QueueEntry, val entry: QueueEntry,
val position: Long, val position: Long,
val behavior: SongTapBehavior?,
) )
class PlaybackController( class PlaybackController(
@@ -113,6 +117,7 @@ class PlaybackController(
private val queueRepository: QueueRepository, private val queueRepository: QueueRepository,
private val connection: PlayerConnection, private val connection: PlayerConnection,
private val downloads: DownloadRepository, private val downloads: DownloadRepository,
private val preferences: UserPreferences,
private val showMessage: (UiMessage) -> Unit = {}, private val showMessage: (UiMessage) -> Unit = {},
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate), private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
) { ) {
@@ -190,7 +195,7 @@ class PlaybackController(
val shuffle = pendingShuffle val shuffle = pendingShuffle
pendingShuffle = null pendingShuffle = null
when { when {
pending != null -> start(pending.entry, pending.position) pending != null -> startPending(pending)
shuffle != null -> startShuffled(shuffle) shuffle != null -> startShuffled(shuffle)
else -> restore() else -> restore()
} }
@@ -239,6 +244,16 @@ class PlaybackController(
startOrdinal: Long, startOrdinal: Long,
) = play(queueRepository.starredEntry(sourceId), startOrdinal) ) = play(queueRepository.starredEntry(sourceId), startOrdinal)
fun playAlbumInOrder(
sourceId: Long,
albumId: String,
) = play(queueRepository.albumEntry(sourceId, albumId), 0, SongTapBehavior.InOrder)
fun playPlaylistInOrder(
sourceId: Long,
playlistId: String,
) = play(queueRepository.playlistEntry(sourceId, playlistId), 0, SongTapBehavior.InOrder)
fun shuffleAlbum( fun shuffleAlbum(
sourceId: Long, sourceId: Long,
albumId: String, albumId: String,
@@ -968,22 +983,35 @@ class PlaybackController(
private fun play( private fun play(
entry: QueueEntry, entry: QueueEntry,
startPosition: Long, startPosition: Long,
behavior: SongTapBehavior? = null,
) { ) {
if (player == null) { if (player == null) {
pendingPlay = PendingPlay(entry, startPosition) pendingPlay = PendingPlay(entry, startPosition, behavior)
connect() connect()
return return
} }
scope.launch { start(entry, startPosition) } scope.launch { start(entry, startPosition, behavior ?: preferences.songTapBehavior().first()) }
}
private suspend fun startPending(pending: PendingPlay) {
val behavior = pending.behavior ?: preferences.songTapBehavior().first()
start(pending.entry, pending.position, behavior)
} }
private suspend fun start( private suspend fun start(
entry: QueueEntry, entry: QueueEntry,
startPosition: Long, startPosition: Long,
behavior: SongTapBehavior,
) = startLock.withLock { ) = startLock.withLock {
val snapshot = resetQueue(entry)
val modes = queueRepository.modes() val modes = queueRepository.modes()
shuffleEnabled = false val shuffle =
when (behavior) {
SongTapBehavior.InOrder -> false
SongTapBehavior.KeepShuffle -> modes.shuffle
SongTapBehavior.Shuffle -> true
}
val snapshot = resetQueue(entry)
shuffleEnabled = shuffle
repeatMode = modes.repeat.toRepeatMode() repeatMode = modes.repeat.toRepeatMode()
player?.setRepeatOne(repeatMode == RepeatMode.One) player?.setRepeatOne(repeatMode == RepeatMode.One)
queueSourceId = entry.sourceId queueSourceId = entry.sourceId
@@ -991,7 +1019,14 @@ class PlaybackController(
stopLocked() stopLocked()
return@withLock return@withLock
} }
val position = startPosition.coerceIn(0, snapshot.contextSize - 1) val start = startPosition.coerceIn(0, snapshot.contextSize - 1)
val position =
if (shuffle) {
queueRepository.setShuffle(pinnedSeed(snapshot.contextSize, start))
0L
} else {
start
}
queueRepository.setCursor(position) queueRepository.setCursor(position)
this.snapshot = readSnapshot() this.snapshot = readSnapshot()
loadWindow(position, autoplay = true) loadWindow(position, autoplay = true)
@@ -96,7 +96,7 @@ class AlbumDetailViewModel(
fun playAll() { fun playAll() {
viewModelScope.launch { viewModelScope.launch {
playbackController.playAlbum(sourceId.first(), albumId, 0) playbackController.playAlbumInOrder(sourceId.first(), albumId)
} }
} }
@@ -94,7 +94,7 @@ class PlaylistDetailViewModel(
fun playAll() { fun playAll() {
viewModelScope.launch { viewModelScope.launch {
playbackController.playPlaylist(sourceId.first(), playlistId, 0) playbackController.playPlaylistInOrder(sourceId.first(), playlistId)
} }
} }
@@ -74,6 +74,7 @@ import com.subtracks.data.prefs.DEFAULT_SYNC_INTERVAL_MINUTES
import com.subtracks.data.prefs.MAX_REPLAY_GAIN_PREAMP_DB import com.subtracks.data.prefs.MAX_REPLAY_GAIN_PREAMP_DB
import com.subtracks.data.prefs.MIN_REPLAY_GAIN_PREAMP_DB import com.subtracks.data.prefs.MIN_REPLAY_GAIN_PREAMP_DB
import com.subtracks.data.prefs.ReplayGainMode import com.subtracks.data.prefs.ReplayGainMode
import com.subtracks.data.prefs.SongTapBehavior
import com.subtracks.data.prefs.SortNameMode import com.subtracks.data.prefs.SortNameMode
import com.subtracks.data.prefs.StreamQuality import com.subtracks.data.prefs.StreamQuality
import com.subtracks.data.prefs.SyncMode import com.subtracks.data.prefs.SyncMode
@@ -103,6 +104,7 @@ private enum class SettingsDialog {
ReplayGain, ReplayGain,
ReplayGainPreamp, ReplayGainPreamp,
SortNames, SortNames,
SongTap,
} }
@Composable @Composable
@@ -131,6 +133,7 @@ fun SettingsRoute(
val replayGainMode by viewModel.replayGainMode.collectAsStateWithLifecycle() val replayGainMode by viewModel.replayGainMode.collectAsStateWithLifecycle()
val replayGainPreamp by viewModel.replayGainPreamp.collectAsStateWithLifecycle() val replayGainPreamp by viewModel.replayGainPreamp.collectAsStateWithLifecycle()
val sortNameMode by viewModel.sortNameMode.collectAsStateWithLifecycle() val sortNameMode by viewModel.sortNameMode.collectAsStateWithLifecycle()
val songTapBehavior by viewModel.songTapBehavior.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
SettingsScreen( SettingsScreen(
@@ -147,6 +150,7 @@ fun SettingsRoute(
replayGainMode = replayGainMode, replayGainMode = replayGainMode,
replayGainPreamp = replayGainPreamp, replayGainPreamp = replayGainPreamp,
sortNameMode = sortNameMode, sortNameMode = sortNameMode,
songTapBehavior = songTapBehavior,
keepScreenOn = keepScreenOn, keepScreenOn = keepScreenOn,
offline = offline, offline = offline,
verboseLogging = verboseLogging, verboseLogging = verboseLogging,
@@ -163,6 +167,7 @@ fun SettingsRoute(
onReplayGainModeChange = viewModel::setReplayGainMode, onReplayGainModeChange = viewModel::setReplayGainMode,
onReplayGainPreampChange = viewModel::setReplayGainPreamp, onReplayGainPreampChange = viewModel::setReplayGainPreamp,
onSortNameModeChange = viewModel::setSortNameMode, onSortNameModeChange = viewModel::setSortNameMode,
onSongTapBehaviorChange = viewModel::setSongTapBehavior,
onKeepScreenOnChange = viewModel::setKeepScreenOn, onKeepScreenOnChange = viewModel::setKeepScreenOn,
onOfflineChange = viewModel::setOfflineMode, onOfflineChange = viewModel::setOfflineMode,
onVerboseLoggingChange = viewModel::setVerboseLogging, onVerboseLoggingChange = viewModel::setVerboseLogging,
@@ -211,6 +216,7 @@ fun SettingsScreen(
replayGainMode: ReplayGainMode = ReplayGainMode.Auto, replayGainMode: ReplayGainMode = ReplayGainMode.Auto,
replayGainPreamp: Float = 0f, replayGainPreamp: Float = 0f,
sortNameMode: SortNameMode = SortNameMode.Name, sortNameMode: SortNameMode = SortNameMode.Name,
songTapBehavior: SongTapBehavior = SongTapBehavior.InOrder,
onSelectSource: (Long) -> Unit, onSelectSource: (Long) -> Unit,
onEditServer: (Long) -> Unit, onEditServer: (Long) -> Unit,
onWifiQualityChange: (StreamQuality) -> Unit, onWifiQualityChange: (StreamQuality) -> Unit,
@@ -224,6 +230,7 @@ fun SettingsScreen(
onReplayGainModeChange: (ReplayGainMode) -> Unit = {}, onReplayGainModeChange: (ReplayGainMode) -> Unit = {},
onReplayGainPreampChange: (Float) -> Unit = {}, onReplayGainPreampChange: (Float) -> Unit = {},
onSortNameModeChange: (SortNameMode) -> Unit = {}, onSortNameModeChange: (SortNameMode) -> Unit = {},
onSongTapBehaviorChange: (SongTapBehavior) -> Unit = {},
onKeepScreenOnChange: (Boolean) -> Unit = {}, onKeepScreenOnChange: (Boolean) -> Unit = {},
onOfflineChange: (Boolean) -> Unit = {}, onOfflineChange: (Boolean) -> Unit = {},
onVerboseLoggingChange: (Boolean) -> Unit = {}, onVerboseLoggingChange: (Boolean) -> Unit = {},
@@ -421,6 +428,14 @@ fun SettingsScreen(
colors = ListItemDefaults.colors(containerColor = Color.Transparent), colors = ListItemDefaults.colors(containerColor = Color.Transparent),
) )
} }
item {
ListItem(
headlineContent = { Text(stringResource(R.string.settings_music_options_song_tap_title)) },
supportingContent = { Text(songTapBehaviorLabel(LocalResources.current, songTapBehavior)) },
modifier = Modifier.clickable { dialog = SettingsDialog.SongTap },
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
)
}
item { SectionHeader(stringResource(R.string.downloads_title)) } item { SectionHeader(stringResource(R.string.downloads_title)) }
item { item {
ListItem( ListItem(
@@ -627,6 +642,14 @@ fun SettingsScreen(
) )
} }
SettingsDialog.SongTap -> {
SongTapBehaviorDialog(
selected = songTapBehavior,
onSelect = onSongTapBehaviorChange,
onDismiss = { dialog = null },
)
}
null -> { null -> {
Unit Unit
} }
@@ -826,6 +849,51 @@ private fun SortNameModeDialog(
) )
} }
private val songTapBehaviorOptions = SongTapBehavior.entries
private fun songTapBehaviorLabel(
resources: Resources,
behavior: SongTapBehavior,
): String =
when (behavior) {
SongTapBehavior.InOrder -> resources.getString(R.string.settings_music_options_song_tap_in_order)
SongTapBehavior.KeepShuffle -> resources.getString(R.string.settings_music_options_song_tap_keep_shuffle)
SongTapBehavior.Shuffle -> resources.getString(R.string.settings_music_options_song_tap_shuffle)
}
@Composable
private fun SongTapBehaviorDialog(
selected: SongTapBehavior,
onSelect: (SongTapBehavior) -> Unit,
onDismiss: () -> Unit,
) {
val resources = LocalResources.current
var draft by remember(selected) { mutableStateOf(selected) }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.settings_music_options_song_tap_title)) },
text = {
ChoiceGroup(
header = stringResource(R.string.settings_music_options_song_tap_prompt),
options = songTapBehaviorOptions.map { it to songTapBehaviorLabel(resources, it) },
selected = draft,
onSelect = { draft = it },
)
},
confirmButton = {
TextButton(
onClick = {
onSelect(draft)
onDismiss()
},
) { Text(stringResource(R.string.action_done)) }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.actions_cancel)) }
},
)
}
@Composable @Composable
private fun ReplayGainModeDialog( private fun ReplayGainModeDialog(
selected: ReplayGainMode, selected: ReplayGainMode,
@@ -6,6 +6,7 @@ import com.subtracks.data.model.Source
import com.subtracks.data.net.NetworkMode import com.subtracks.data.net.NetworkMode
import com.subtracks.data.prefs.DEFAULT_SYNC_INTERVAL_MINUTES import com.subtracks.data.prefs.DEFAULT_SYNC_INTERVAL_MINUTES
import com.subtracks.data.prefs.ReplayGainMode import com.subtracks.data.prefs.ReplayGainMode
import com.subtracks.data.prefs.SongTapBehavior
import com.subtracks.data.prefs.SortNameMode import com.subtracks.data.prefs.SortNameMode
import com.subtracks.data.prefs.StreamQuality import com.subtracks.data.prefs.StreamQuality
import com.subtracks.data.prefs.SyncMode import com.subtracks.data.prefs.SyncMode
@@ -82,6 +83,11 @@ class SettingsViewModel(
.sortNameMode() .sortNameMode()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SortNameMode.Name) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SortNameMode.Name)
val songTapBehavior: StateFlow<SongTapBehavior> =
userPreferences
.songTapBehavior()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), SongTapBehavior.InOrder)
val keepScreenOn: StateFlow<Boolean> = val keepScreenOn: StateFlow<Boolean> =
userPreferences userPreferences
.keepScreenOn() .keepScreenOn()
@@ -153,4 +159,8 @@ class SettingsViewModel(
fun setSortNameMode(mode: SortNameMode) { fun setSortNameMode(mode: SortNameMode) {
viewModelScope.launch { userPreferences.setSortNameMode(mode) } viewModelScope.launch { userPreferences.setSortNameMode(mode) }
} }
fun setSongTapBehavior(behavior: SongTapBehavior) {
viewModelScope.launch { userPreferences.setSongTapBehavior(behavior) }
}
} }
+5
View File
@@ -83,6 +83,11 @@
<string name="settings_music_name">Playback</string> <string name="settings_music_name">Playback</string>
<string name="settings_music_options_scrobble_description_on">Send now playing and play counts</string> <string name="settings_music_options_scrobble_description_on">Send now playing and play counts</string>
<string name="settings_music_options_scrobble_title">Scrobble to server</string> <string name="settings_music_options_scrobble_title">Scrobble to server</string>
<string name="settings_music_options_song_tap_in_order">Play without shuffling</string>
<string name="settings_music_options_song_tap_keep_shuffle">Keep current shuffle mode</string>
<string name="settings_music_options_song_tap_prompt">When tapping a song in a list</string>
<string name="settings_music_options_song_tap_shuffle">Play and shuffle</string>
<string name="settings_music_options_song_tap_title">New song tap behavior</string>
<string name="settings_replay_gain_mode_album">Album gain</string> <string name="settings_replay_gain_mode_album">Album gain</string>
<string name="settings_replay_gain_mode_auto">Auto</string> <string name="settings_replay_gain_mode_auto">Auto</string>
<string name="settings_replay_gain_mode_off">Off</string> <string name="settings_replay_gain_mode_off">Off</string>
@@ -26,6 +26,7 @@ import com.subtracks.data.model.Song
import com.subtracks.data.model.SongDownload import com.subtracks.data.model.SongDownload
import com.subtracks.data.model.Source import com.subtracks.data.model.Source
import com.subtracks.data.net.NetworkMode import com.subtracks.data.net.NetworkMode
import com.subtracks.data.prefs.SongTapBehavior
import com.subtracks.data.prefs.StreamQuality import com.subtracks.data.prefs.StreamQuality
import com.subtracks.data.prefs.UserPreferences import com.subtracks.data.prefs.UserPreferences
import com.subtracks.data.prefs.fakeUserPreferences import com.subtracks.data.prefs.fakeUserPreferences
@@ -113,6 +114,7 @@ class PlaybackControllerTest {
queues, queues,
FakePlayerConnection(handle), FakePlayerConnection(handle),
downloads, downloads,
prefs,
showMessage = { messages += it.resolve(resources) }, showMessage = { messages += it.resolve(resources) },
scope = controllerScope, scope = controllerScope,
) )
@@ -2519,6 +2521,60 @@ class PlaybackControllerTest {
assertEquals("s4", runBlocking { queues.itemAt(snapshot, controller.state.value.position!!)?.song?.id }) assertEquals("s4", runBlocking { queues.itemAt(snapshot, controller.state.value.position!!)?.song?.id })
} }
@Test
fun tappingASongCanKeepTheCurrentShuffleMode() {
seedAlbum(5, sourceId = 1)
runBlocking { prefs.setSongTapBehavior(SongTapBehavior.KeepShuffle) }
controller.playAlbum(1, "al1", 0)
await {
controller.state.value.item
?.id == "s1"
}
controller.toggleShuffle()
await { controller.state.value.shuffle }
controller.playAlbum(1, "al1", 3)
await {
controller.state.value.item
?.id == "s4" && controller.state.value.shuffle
}
assertEquals(0L, controller.state.value.position)
val snapshot = runBlocking { queues.snapshot() }
assertTrue(snapshot.shuffled)
assertEquals("s4", runBlocking { queues.itemAt(snapshot, controller.state.value.position!!)?.song?.id })
}
@Test
fun tappingASongCanStartShuffled() {
seedAlbum(5, sourceId = 1)
runBlocking { prefs.setSongTapBehavior(SongTapBehavior.Shuffle) }
controller.playAlbum(1, "al1", 3)
await {
controller.state.value.item
?.id == "s4" && controller.state.value.shuffle
}
assertEquals(0L, controller.state.value.position)
assertTrue(runBlocking { queues.snapshot().shuffled })
}
@Test
fun playAllStartsInOrderRegardlessOfTheSongTapBehavior() {
seedAlbum(3, sourceId = 1)
runBlocking { prefs.setSongTapBehavior(SongTapBehavior.Shuffle) }
controller.playAlbumInOrder(1, "al1")
await {
controller.state.value.item
?.id == "s1" && !controller.state.value.shuffle
}
assertEquals(0L, controller.state.value.position)
}
@Test @Test
fun jumpingInTheQueueWhileShuffledKeepsTheOrder() { fun jumpingInTheQueueWhileShuffledKeepsTheOrder() {
seedAlbum(5, sourceId = 1) seedAlbum(5, sourceId = 1)
@@ -3643,7 +3699,7 @@ class PlaybackControllerTest {
handle: FakePlayerHandle, handle: FakePlayerHandle,
): PlaybackController { ): PlaybackController {
val scope = CoroutineScope(SupervisorJob() + dispatcher).also { extraScopes += it } val scope = CoroutineScope(SupervisorJob() + dispatcher).also { extraScopes += it }
return PlaybackController(sources, queues, FakePlayerConnection(handle), downloads, scope = scope) return PlaybackController(sources, queues, FakePlayerConnection(handle), downloads, prefs, scope = scope)
} }
private fun await(predicate: () -> Boolean) = awaitUntil("waiting for the expected playback state", predicate) private fun await(predicate: () -> Boolean) = awaitUntil("waiting for the expected playback state", predicate)
@@ -59,6 +59,7 @@ class ViewModelRepositoryFixture {
queueRepository, queueRepository,
FakePlayerConnection(FakePlayerHandle()), FakePlayerConnection(FakePlayerHandle()),
downloadRepository, downloadRepository,
fakeUserPreferences(),
scope = controllerScope, scope = controllerScope,
) )
val libraryRepository = val libraryRepository =
@@ -101,6 +101,7 @@ class NowPlayingRouteTest {
queues, queues,
FakePlayerConnection(handle), FakePlayerConnection(handle),
downloads, downloads,
prefs,
showMessage = {}, showMessage = {},
scope = controllerScope, scope = controllerScope,
) )
@@ -83,7 +83,15 @@ class QueueViewModelTest {
artworkFetcher = { ByteArray(0) }, artworkFetcher = { ByteArray(0) },
scope = repoScope, scope = repoScope,
).also { it.start() } ).also { it.start() }
controller = PlaybackController(sources, queues, FakePlayerConnection(FakePlayerHandle()), downloads, scope = controllerScope) controller =
PlaybackController(
sources,
queues,
FakePlayerConnection(FakePlayerHandle()),
downloads,
fakeUserPreferences(),
scope = controllerScope,
)
viewModel = QueueViewModel(queues, controller, sources, downloads) viewModel = QueueViewModel(queues, controller, sources, downloads)
} }
@@ -12,6 +12,7 @@ import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode import androidx.compose.ui.test.performScrollToNode
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.subtracks.data.prefs.SongTapBehavior
import com.subtracks.data.prefs.SortNameMode import com.subtracks.data.prefs.SortNameMode
import com.subtracks.data.prefs.StreamQuality import com.subtracks.data.prefs.StreamQuality
import com.subtracks.ui.theme.SubtracksTheme import com.subtracks.ui.theme.SubtracksTheme
@@ -82,10 +83,29 @@ class SettingsScreenTest {
assertEquals(SortNameMode.SortName, requested) assertEquals(SortNameMode.SortName, requested)
} }
@Test
fun theSongTapBehaviorRowChangesTheSetting() {
var requested: SongTapBehavior? = null
renderSettings(
keepScreenOn = false,
onKeepScreenOnChange = {},
onSongTapBehaviorChange = { requested = it },
scrollTo = "New song tap behavior",
)
composeRule.onNodeWithText("New song tap behavior").performClick()
composeRule.onNodeWithText("Play and shuffle").performClick()
composeRule.onNodeWithText("Done").performClick()
composeRule.waitForIdle()
assertEquals(SongTapBehavior.Shuffle, requested)
}
private fun renderSettings( private fun renderSettings(
keepScreenOn: Boolean, keepScreenOn: Boolean,
onKeepScreenOnChange: (Boolean) -> Unit, onKeepScreenOnChange: (Boolean) -> Unit,
onSortNameModeChange: (SortNameMode) -> Unit = {}, onSortNameModeChange: (SortNameMode) -> Unit = {},
onSongTapBehaviorChange: (SongTapBehavior) -> Unit = {},
scrollTo: String = "Keep screen on", scrollTo: String = "Keep screen on",
) { ) {
composeRule.setContent { composeRule.setContent {
@@ -110,6 +130,7 @@ class SettingsScreenTest {
onScrobblingChange = {}, onScrobblingChange = {},
onKeepScreenOnChange = onKeepScreenOnChange, onKeepScreenOnChange = onKeepScreenOnChange,
onSortNameModeChange = onSortNameModeChange, onSortNameModeChange = onSortNameModeChange,
onSongTapBehaviorChange = onSongTapBehaviorChange,
onAddServer = {}, onAddServer = {},
onOpenDownloads = {}, onOpenDownloads = {},
onOpenLicenses = {}, onOpenLicenses = {},