M5a frontend + M5b quarantine + M5c suggestions #30

Merged
bvandeusen merged 262 commits from dev into main 2026-05-01 07:14:20 -04:00
4 changed files with 74 additions and 1 deletions
Showing only changes of commit 03a6dc4e5d - Show all commits
@@ -78,6 +78,27 @@ class PlayerController @Inject constructor(
fun skipToNext() { mediaController?.seekToNextMediaItem() } fun skipToNext() { mediaController?.seekToNextMediaItem() }
fun skipToPrevious() { mediaController?.seekToPreviousMediaItem() } fun skipToPrevious() { mediaController?.seekToPreviousMediaItem() }
/** Flip shuffle on/off. Media3 emits onEvents → uiState reflects. */
fun toggleShuffle() {
val controller = mediaController ?: return
controller.shuffleModeEnabled = !controller.shuffleModeEnabled
}
/**
* Cycles the player's repeat mode: off → all → one → off. Mirrors
* Flutter's `cycleRepeat`. The wire order (none/all/one) matches
* what the Now Playing icon swap expects — Lucide.Repeat for off
* and all, Lucide.Repeat1 for one.
*/
fun cycleRepeat() {
val controller = mediaController ?: return
controller.repeatMode = when (controller.repeatMode) {
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
else -> Player.REPEAT_MODE_OFF
}
}
/** Jump to [index] in the current queue. No-op if index is out of range. */ /** Jump to [index] in the current queue. No-op if index is out of range. */
fun seekToIndex(index: Int) { fun seekToIndex(index: Int) {
val controller = mediaController ?: return val controller = mediaController ?: return
@@ -179,6 +200,12 @@ class PlayerController @Inject constructor(
bufferedPositionMs = player.bufferedPosition.coerceAtLeast(0), bufferedPositionMs = player.bufferedPosition.coerceAtLeast(0),
playbackError = player.playerError?.message, playbackError = player.playerError?.message,
currentSource = source, currentSource = source,
shuffleEnabled = player.shuffleModeEnabled,
repeatMode = when (player.repeatMode) {
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
Player.REPEAT_MODE_ONE -> RepeatMode.ONE
else -> RepeatMode.OFF
},
) )
} }
}, },
@@ -2,6 +2,13 @@ package com.fabledsword.minstrel.player
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
/**
* Cycle on the repeat button: off → all → one → off. Mirrors
* `AudioServiceRepeatMode` in flutter_client and maps directly to
* the three Media3 `Player.REPEAT_MODE_*` int constants.
*/
enum class RepeatMode { OFF, ALL, ONE }
/** /**
* Snapshot of what the player is doing, for the UI to read via * Snapshot of what the player is doing, for the UI to read via
* [PlayerController.uiState]. Replaces the audio_service-style * [PlayerController.uiState]. Replaces the audio_service-style
@@ -19,4 +26,6 @@ data class PlayerUiState(
val bufferedPositionMs: Long = 0, val bufferedPositionMs: Long = 0,
val playbackError: String? = null, val playbackError: String? = null,
val currentSource: String? = null, val currentSource: String? = null,
val shuffleEnabled: Boolean = false,
val repeatMode: RepeatMode = RepeatMode.OFF,
) )
@@ -43,8 +43,12 @@ import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music import com.composables.icons.lucide.Music
import com.composables.icons.lucide.Pause import com.composables.icons.lucide.Pause
import com.composables.icons.lucide.Play import com.composables.icons.lucide.Play
import com.composables.icons.lucide.Repeat
import com.composables.icons.lucide.Repeat1
import com.composables.icons.lucide.Shuffle
import com.composables.icons.lucide.SkipBack import com.composables.icons.lucide.SkipBack
import com.composables.icons.lucide.SkipForward import com.composables.icons.lucide.SkipForward
import com.fabledsword.minstrel.player.RepeatMode
import com.fabledsword.minstrel.nav.AlbumDetail import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.Queue import com.fabledsword.minstrel.nav.Queue
@@ -121,7 +125,14 @@ fun NowPlayingScreen(
onNext = viewModel::skipToNext, onNext = viewModel::skipToNext,
) )
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
BottomActionsRow(navController = navController, track = track) BottomActionsRow(
navController = navController,
track = track,
shuffleEnabled = state.shuffleEnabled,
repeatMode = state.repeatMode,
onToggleShuffle = viewModel::toggleShuffle,
onCycleRepeat = viewModel::cycleRepeat,
)
} }
} }
} }
@@ -130,11 +141,35 @@ fun NowPlayingScreen(
private fun BottomActionsRow( private fun BottomActionsRow(
navController: NavHostController, navController: NavHostController,
track: com.fabledsword.minstrel.models.TrackRef, track: com.fabledsword.minstrel.models.TrackRef,
shuffleEnabled: Boolean,
repeatMode: RepeatMode,
onToggleShuffle: () -> Unit,
onCycleRepeat: () -> Unit,
) { ) {
val accent = MaterialTheme.colorScheme.primary
val muted = MaterialTheme.colorScheme.onSurfaceVariant
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
IconButton(onClick = onToggleShuffle) {
Icon(
imageVector = Lucide.Shuffle,
contentDescription = if (shuffleEnabled) "Shuffle on" else "Shuffle off",
tint = if (shuffleEnabled) accent else muted,
)
}
IconButton(onClick = onCycleRepeat) {
Icon(
imageVector = if (repeatMode == RepeatMode.ONE) Lucide.Repeat1 else Lucide.Repeat,
contentDescription = when (repeatMode) {
RepeatMode.OFF -> "Repeat off"
RepeatMode.ALL -> "Repeat all"
RepeatMode.ONE -> "Repeat one"
},
tint = if (repeatMode == RepeatMode.OFF) muted else accent,
)
}
ViewQueueButton(onClick = { navController.navigate(Queue) }) ViewQueueButton(onClick = { navController.navigate(Queue) })
TrackActionsButton( TrackActionsButton(
track = track, track = track,
@@ -31,4 +31,6 @@ class PlayerViewModel @Inject constructor(
fun skipToNext() = controller.skipToNext() fun skipToNext() = controller.skipToNext()
fun skipToPrevious() = controller.skipToPrevious() fun skipToPrevious() = controller.skipToPrevious()
fun seekToIndex(index: Int) = controller.seekToIndex(index) fun seekToIndex(index: Int) = controller.seekToIndex(index)
fun toggleShuffle() = controller.toggleShuffle()
fun cycleRepeat() = controller.cycleRepeat()
} }