mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-12 15:46:34 +01:00
playback: make the song tap shuffle behaviour a preference
This commit is contained in:
@@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user