diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdClient.kt b/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdClient.kt index 237fbff..855b2b1 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdClient.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdClient.kt @@ -161,6 +161,7 @@ class MpdClient( suspend fun playPos(pos: Int) = run(MpdCommands.playPos(pos)) suspend fun playId(songId: Int) = run(MpdCommands.playId(songId)) suspend fun stop() = run(MpdCommands.stop()) + suspend fun clearQueue() = run(MpdCommands.clear()) suspend fun next() = run(MpdCommands.next()) suspend fun previous() = run(MpdCommands.previous()) suspend fun pause(paused: Boolean) = run(MpdCommands.pause(paused)) @@ -195,6 +196,21 @@ class MpdClient( conn.execute(MpdCommands.play()) } + /** Append an album's tracks to the end of the queue; playback is untouched. */ + suspend fun queueAlbum(album: String, albumArtist: String?) = withCommand { conn -> + conn.execute(MpdCommands.findAddAlbum(album, albumArtist)) + } + + /** + * Insert an album right after the current track so it plays next. Falls back + * to a plain append when nothing is playing — there is no current song for the + * relative `"+0"` position to anchor to. + */ + suspend fun playAlbumNext(album: String, albumArtist: String?) = withCommand { conn -> + val playing = MpdStatus.from(conn.execute(MpdCommands.status()).toMap()).song != null + conn.execute(MpdCommands.findAddAlbum(album, albumArtist, position = if (playing) "+0" else null)) + } + /** Cover art bytes for a specific song URI, or null if the server has none. */ suspend fun songArt(uri: String): ByteArray? = withArtConnection { conn -> readArt(conn, uri) } diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdCommands.kt b/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdCommands.kt index 15ac01b..bb0b30a 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdCommands.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/mpd/MpdCommands.kt @@ -72,13 +72,22 @@ object MpdCommands { /** Every album, grouped by album artist (`Album`/`AlbumArtist` lines). */ fun listAlbums() = MpdProtocol.command("list", "album", "group", "albumartist") - /** Append every track of an album to the queue (optionally scoped to an artist). */ - fun findAddAlbum(album: String, albumArtist: String?) = - if (albumArtist.isNullOrEmpty()) { - MpdProtocol.command("findadd", "album", album) - } else { - MpdProtocol.command("findadd", "album", album, "albumartist", albumArtist) + /** + * Append every track of an album to the queue (optionally scoped to an artist). + * + * When [position] is given it is passed through as `findadd`'s `position` + * argument, controlling where the tracks land. MPD accepts an absolute index + * or a relative one — `"+0"` inserts right after the currently playing song + * (i.e. "play next"). Omit it to append to the end of the queue. + */ + fun findAddAlbum(album: String, albumArtist: String?, position: String? = null): String { + val args = buildList { + add("album"); add(album) + if (!albumArtist.isNullOrEmpty()) { add("albumartist"); add(albumArtist) } + if (position != null) { add("position"); add(position) } } + return MpdProtocol.command("findadd", *args.toTypedArray()) + } /** First track of an album — used to resolve a URI for album-art lookup. */ fun findFirstTrack(album: String, albumArtist: String?) = diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/playback/MpdConnectionManager.kt b/app/src/main/kotlin/ca/ksamad/musicremote/playback/MpdConnectionManager.kt index 2550ea4..bb5b598 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/playback/MpdConnectionManager.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/playback/MpdConnectionManager.kt @@ -121,6 +121,7 @@ class MpdConnectionManager(context: Context) { fun resume() = fire { pause(false) } fun pause() = fire { pause(true) } fun stop() = fire { stop() } + fun clearQueue() = fire { clearQueue() } fun next() = fire { next() } fun previous() = fire { previous() } fun togglePlayPause() = fire { togglePause() } @@ -135,6 +136,12 @@ class MpdConnectionManager(context: Context) { fun playAlbum(album: String, albumArtist: String?) = fire { playAlbum(album, albumArtist) } + /** Append an album to the end of the queue without changing playback. */ + fun queueAlbum(album: String, albumArtist: String?) = fire { queueAlbum(album, albumArtist) } + + /** Insert an album right after the current track so it plays next. */ + fun playAlbumNext(album: String, albumArtist: String?) = fire { playAlbumNext(album, albumArtist) } + /** Jump to a queue entry by its stable song id. */ fun playQueueItem(songId: Int) = fire { playId(songId) } diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/ui/AlbumsScreen.kt b/app/src/main/kotlin/ca/ksamad/musicremote/ui/AlbumsScreen.kt index df9e27d..75c173b 100644 Binary files a/app/src/main/kotlin/ca/ksamad/musicremote/ui/AlbumsScreen.kt and b/app/src/main/kotlin/ca/ksamad/musicremote/ui/AlbumsScreen.kt differ diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt b/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt index 971dd75..6ede084 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt @@ -159,7 +159,7 @@ fun NowPlayingScreen( Spacer(Modifier.height(24.dp)) - CastIndicator(host = serverHost) + // CastIndicator(host = serverHost) VolumeControl(volume = status?.volume, onSetVolume = { vm.setVolume(it) }) Spacer(Modifier.height(16.dp)) diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/ui/PlayerViewModel.kt b/app/src/main/kotlin/ca/ksamad/musicremote/ui/PlayerViewModel.kt index f897c2b..58a98fb 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/ui/PlayerViewModel.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/ui/PlayerViewModel.kt @@ -70,8 +70,11 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application) fun setConsume(on: Boolean) = manager.setConsume(on) fun playAlbum(album: String, albumArtist: String?) = manager.playAlbum(album, albumArtist) + fun queueAlbum(album: String, albumArtist: String?) = manager.queueAlbum(album, albumArtist) + fun playAlbumNext(album: String, albumArtist: String?) = manager.playAlbumNext(album, albumArtist) suspend fun loadAlbums() = manager.loadAlbums() fun playQueueItem(songId: Int) = manager.playQueueItem(songId) + fun clearQueue() = manager.clearQueue() suspend fun loadQueue() = manager.loadQueue() } diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/ui/QueueScreen.kt b/app/src/main/kotlin/ca/ksamad/musicremote/ui/QueueScreen.kt index 4331b48..04c03f5 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/ui/QueueScreen.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/ui/QueueScreen.kt @@ -11,7 +11,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.DeleteSweep import androidx.compose.material.icons.filled.VolumeUp +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -21,10 +23,14 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -63,6 +69,10 @@ fun QueueScreen(vm: PlayerViewModel, onBack: () -> Unit) { } val hasCurrent = currentPos != null && (fullQueue?.indices?.contains(currentPos) == true) + // Guards the destructive "clear queue" action behind a confirmation dialog. + var confirmClear by remember { mutableStateOf(false) } + val queueNotEmpty = fullQueue?.isNotEmpty() == true + Scaffold( topBar = { TopAppBar( @@ -72,6 +82,13 @@ fun QueueScreen(vm: PlayerViewModel, onBack: () -> Unit) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") } }, + actions = { + if (queueNotEmpty) { + IconButton(onClick = { confirmClear = true }) { + Icon(Icons.Filled.DeleteSweep, contentDescription = "Clear queue") + } + } + }, ) }, ) { innerPadding -> @@ -137,6 +154,23 @@ fun QueueScreen(vm: PlayerViewModel, onBack: () -> Unit) { } } } + + if (confirmClear) { + AlertDialog( + onDismissRequest = { confirmClear = false }, + title = { Text("Clear the queue?") }, + text = { Text("This removes every track from the queue and stops playback.") }, + confirmButton = { + TextButton(onClick = { + vm.clearQueue() + confirmClear = false + }) { Text("Clear") } + }, + dismissButton = { + TextButton(onClick = { confirmClear = false }) { Text("Cancel") } + }, + ) + } } /** Explains what will actually happen at the end of the list, given the modes. */