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 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) }
@@ -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?) =
@@ -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) }
@@ -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))
@@ -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()
}
@@ -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. */