fix: M3.5 polish — genre splitting, radio button, search focus #24
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user