feat(app): add music queue management with next/prev navigation

Add queue (ShallowRef<Song[]>), currentIndex, playNext, playPrevious,
addToQueue, removeFromQueue to usePlayer. Auto-advance on song end.
Add prev/next buttons and queue count to PlayerBar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-03 20:22:51 +00:00
co-authored by Claude Opus 4.6
parent 81ccd69aae
commit 79691c9a25
2 changed files with 116 additions and 2 deletions
@@ -36,6 +36,19 @@
<div class="flex flex-col gap-1 flex-1 max-w-xl mx-4">
<div class="flex items-center gap-2">
<!-- Previous button -->
<button
class="w-8 h-8 rounded-full flex items-center justify-center shrink-0 transition-all hover:scale-105 active:scale-95"
:class="hasPrevious
? isDark ? 'text-white/70 hover:text-white/90' : 'text-gray-600 hover:text-gray-800'
: isDark ? 'text-white/20' : 'text-gray-300'"
:disabled="!hasPrevious"
@click="playPrevious"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 6h2v12H6V6zm3.5 6l8.5 6V6l-8.5 6z" />
</svg>
</button>
<button
class="w-12 h-12 rounded-full flex items-center justify-center glass-button shrink-0 transition-all hover:scale-105 active:scale-95"
@click="toggle"
@@ -51,6 +64,19 @@
<path d="M8 5v14l11-7L8 5z" />
</svg>
</button>
<!-- Next button -->
<button
class="w-8 h-8 rounded-full flex items-center justify-center shrink-0 transition-all hover:scale-105 active:scale-95"
:class="hasNext
? isDark ? 'text-white/70 hover:text-white/90' : 'text-gray-600 hover:text-gray-800'
: isDark ? 'text-white/20' : 'text-gray-300'"
:disabled="!hasNext"
@click="playNext"
>
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6h-2z" />
</svg>
</button>
<span class="text-[10px] font-mono tabular-nums"
:class="isDark ? 'text-white/40' : 'text-gray-400'">
{{ formatTime(currentTime) }}
@@ -73,6 +99,11 @@
<p v-if="error" class="text-[10px] text-red-400">{{ error }}</p>
</div>
<span
v-if="queue.length > 1"
class="text-[10px] font-mono tabular-nums shrink-0"
:class="isDark ? 'text-white/30' : 'text-gray-400'"
>{{ queue.length }} songs</span>
<button
class="w-9 h-9 rounded-xl flex items-center justify-center path-glass-button path-glass-button-sm shrink-0 transition-all hover:scale-105"
@click="clear"
@@ -102,9 +133,14 @@ const {
currentTime,
duration,
progress,
queue,
hasNext,
hasPrevious,
toggle,
seek,
clear,
playNext,
playPrevious,
setContainer,
} = usePlayer()
+80 -2
View File
@@ -1,4 +1,4 @@
import { ref, computed } from 'vue'
import { ref, shallowRef, computed } from 'vue'
import type { Song } from '@aiui/core/types/content'
import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
@@ -19,6 +19,10 @@ const error = ref<string | null>(null)
const currentTime = ref(0)
const duration = ref(0)
// Queue management
const queue = shallowRef<Song[]>([])
const currentIndex = ref(-1)
let plyrInstance: Plyr | null = null
let containerEl: HTMLDivElement | null = null
@@ -94,6 +98,10 @@ export function usePlayer() {
})
plyrInstance!.on('ended', () => {
isPlaying.value = false
// Auto-advance to next song in queue
if (currentIndex.value >= 0 && currentIndex.value < queue.value.length - 1) {
playNext()
}
})
plyrInstance!.on('playing', () => {
isPlaying.value = true
@@ -155,6 +163,58 @@ export function usePlayer() {
currentTime.value = time
}
function addToQueue(song: Song) {
const existing = queue.value.find(s => s.id === song.id)
if (!existing) {
queue.value = [...queue.value, song]
}
}
function playNext() {
if (queue.value.length === 0) return
const nextIdx = currentIndex.value + 1
if (nextIdx < queue.value.length) {
currentIndex.value = nextIdx
play(queue.value[nextIdx])
}
}
function playPrevious() {
if (queue.value.length === 0) return
const prevIdx = currentIndex.value - 1
if (prevIdx >= 0) {
currentIndex.value = prevIdx
play(queue.value[prevIdx])
}
}
function removeFromQueue(index: number) {
const newQueue = [...queue.value]
newQueue.splice(index, 1)
queue.value = newQueue
// Adjust currentIndex if needed
if (index < currentIndex.value) {
currentIndex.value--
} else if (index === currentIndex.value) {
// Removed the current track — pause
currentIndex.value = Math.min(currentIndex.value, newQueue.length - 1)
}
}
// Override play to update queue position
const originalPlay = play
async function playWithQueue(song: Song) {
// If not in queue, add it
const idx = queue.value.findIndex(s => s.id === song.id)
if (idx === -1) {
addToQueue(song)
currentIndex.value = queue.value.length - 1
} else {
currentIndex.value = idx
}
await originalPlay(song)
}
function clear() {
pause()
destroyPlayer()
@@ -165,6 +225,15 @@ export function usePlayer() {
error.value = null
}
function clearQueue() {
clear()
queue.value = []
currentIndex.value = -1
}
const hasNext = computed(() => currentIndex.value < queue.value.length - 1)
const hasPrevious = computed(() => currentIndex.value > 0)
return {
currentSong,
playableSource,
@@ -175,11 +244,20 @@ export function usePlayer() {
duration,
hasTrack,
progress,
play,
queue,
currentIndex,
hasNext,
hasPrevious,
play: playWithQueue,
pause,
toggle,
seek,
clear,
clearQueue,
addToQueue,
playNext,
playPrevious,
removeFromQueue,
setContainer,
}
}