feat: clear the queue

This commit is contained in:
2026-07-30 00:47:30 -04:00
parent 01cb164c9e
commit 795673aff8
7 changed files with 76 additions and 7 deletions
@@ -161,6 +161,7 @@ class MpdClient(
suspend fun playPos(pos: Int) = run(MpdCommands.playPos(pos)) suspend fun playPos(pos: Int) = run(MpdCommands.playPos(pos))
suspend fun playId(songId: Int) = run(MpdCommands.playId(songId)) suspend fun playId(songId: Int) = run(MpdCommands.playId(songId))
suspend fun stop() = run(MpdCommands.stop()) suspend fun stop() = run(MpdCommands.stop())
suspend fun clearQueue() = run(MpdCommands.clear())
suspend fun next() = run(MpdCommands.next()) suspend fun next() = run(MpdCommands.next())
suspend fun previous() = run(MpdCommands.previous()) suspend fun previous() = run(MpdCommands.previous())
suspend fun pause(paused: Boolean) = run(MpdCommands.pause(paused)) suspend fun pause(paused: Boolean) = run(MpdCommands.pause(paused))
@@ -195,6 +196,21 @@ class MpdClient(
conn.execute(MpdCommands.play()) 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. */ /** 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) } suspend fun songArt(uri: String): ByteArray? = withArtConnection { conn -> readArt(conn, uri) }
@@ -72,12 +72,21 @@ object MpdCommands {
/** Every album, grouped by album artist (`Album`/`AlbumArtist` lines). */ /** Every album, grouped by album artist (`Album`/`AlbumArtist` lines). */
fun listAlbums() = MpdProtocol.command("list", "album", "group", "albumartist") 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?) = * Append every track of an album to the queue (optionally scoped to an artist).
if (albumArtist.isNullOrEmpty()) { *
MpdProtocol.command("findadd", "album", album) * When [position] is given it is passed through as `findadd`'s `position`
} else { * argument, controlling where the tracks land. MPD accepts an absolute index
MpdProtocol.command("findadd", "album", album, "albumartist", albumArtist) * 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. */ /** First track of an album — used to resolve a URI for album-art lookup. */
@@ -121,6 +121,7 @@ class MpdConnectionManager(context: Context) {
fun resume() = fire { pause(false) } fun resume() = fire { pause(false) }
fun pause() = fire { pause(true) } fun pause() = fire { pause(true) }
fun stop() = fire { stop() } fun stop() = fire { stop() }
fun clearQueue() = fire { clearQueue() }
fun next() = fire { next() } fun next() = fire { next() }
fun previous() = fire { previous() } fun previous() = fire { previous() }
fun togglePlayPause() = fire { togglePause() } fun togglePlayPause() = fire { togglePause() }
@@ -135,6 +136,12 @@ class MpdConnectionManager(context: Context) {
fun playAlbum(album: String, albumArtist: String?) = fire { playAlbum(album, albumArtist) } 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. */ /** Jump to a queue entry by its stable song id. */
fun playQueueItem(songId: Int) = fire { playId(songId) } fun playQueueItem(songId: Int) = fire { playId(songId) }
@@ -159,7 +159,7 @@ fun NowPlayingScreen(
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
CastIndicator(host = serverHost) // CastIndicator(host = serverHost)
VolumeControl(volume = status?.volume, onSetVolume = { vm.setVolume(it) }) VolumeControl(volume = status?.volume, onSetVolume = { vm.setVolume(it) })
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@@ -70,8 +70,11 @@ class PlayerViewModel(application: Application) : AndroidViewModel(application)
fun setConsume(on: Boolean) = manager.setConsume(on) fun setConsume(on: Boolean) = manager.setConsume(on)
fun playAlbum(album: String, albumArtist: String?) = manager.playAlbum(album, albumArtist) 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() suspend fun loadAlbums() = manager.loadAlbums()
fun playQueueItem(songId: Int) = manager.playQueueItem(songId) fun playQueueItem(songId: Int) = manager.playQueueItem(songId)
fun clearQueue() = manager.clearQueue()
suspend fun loadQueue() = manager.loadQueue() suspend fun loadQueue() = manager.loadQueue()
} }
@@ -11,7 +11,9 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack 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.material.icons.filled.VolumeUp
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -21,10 +23,14 @@ import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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) 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( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -72,6 +82,13 @@ fun QueueScreen(vm: PlayerViewModel, onBack: () -> Unit) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = {
if (queueNotEmpty) {
IconButton(onClick = { confirmClear = true }) {
Icon(Icons.Filled.DeleteSweep, contentDescription = "Clear queue")
}
}
},
) )
}, },
) { innerPadding -> ) { 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. */ /** Explains what will actually happen at the end of the list, given the modes. */