From dc7b9b78fa0914e218f4ab1ce9e0388b374e77c1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 22 Jul 2026 23:12:23 -0400 Subject: [PATCH] =?UTF-8?q?feat(player):=20queue=20move/remove/clear=20on?= =?UTF-8?q?=20PlayerController=20+=20VM=20=E2=80=94=20#1944?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds moveInQueue/removeFromQueue/clearQueue, each keeping the domain queueRefs snapshot in lock-step with the Media3 timeline (mirrors playNext/enqueue). Media3 onEvents rebuilds uiState so the queue view reflects reorder/removal/clear. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../minstrel/player/PlayerController.kt | 31 +++++++++++++++++++ .../minstrel/player/ui/PlayerViewModel.kt | 3 ++ 2 files changed, 34 insertions(+) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt index 6af12b3c..0a4222b6 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/PlayerController.kt @@ -288,6 +288,37 @@ class PlayerController @Inject constructor( controller.addMediaItem(track.toMediaItem(source = null)) } + /** + * Reorder the queue: move the item at [from] to [to], keeping the domain + * snapshot in lock-step with the player's MediaItem timeline. Media3 emits + * onEvents → uiState reflects the new order (and the still-playing item's + * index). No-op on bad indices or a no-move. + */ + fun moveInQueue(from: Int, to: Int) { + val controller = mediaController ?: return + if (from !in queueRefs.indices || to !in queueRefs.indices || from == to) return + queueRefs = queueRefs.toMutableList().apply { add(to, removeAt(from)) } + controller.moveMediaItem(from, to) + } + + /** + * Remove the queue item at [index]. When it's the currently-playing item + * Media3 advances to the next automatically. No-op on a bad index. + */ + fun removeFromQueue(index: Int) { + val controller = mediaController ?: return + if (index !in queueRefs.indices) return + queueRefs = queueRefs.toMutableList().apply { removeAt(index) } + controller.removeMediaItem(index) + } + + /** Empty the queue and stop playback. */ + fun clearQueue() { + val controller = mediaController ?: return + queueRefs = emptyList() + controller.clearMediaItems() + } + /** * Seed a fresh radio queue from [trackId]. The `source` tag is * "radio:" so the server-side rotation reporter can diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt index e20a3407..674728f8 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt @@ -55,6 +55,9 @@ class PlayerViewModel @Inject constructor( fun seekToIndex(index: Int) = controller.seekToIndex(index) fun toggleShuffle() = controller.toggleShuffle() fun cycleRepeat() = controller.cycleRepeat() + fun moveInQueue(from: Int, to: Int) = controller.moveInQueue(from, to) + fun removeFromQueue(index: Int) = controller.removeFromQueue(index) + fun clearQueue() = controller.clearQueue() fun toggleLikeTrack(trackId: String) { val desired = trackId !in likedTrackIds.value