Fix queue display

This commit is contained in:
Ultradesu
2026-06-08 17:59:46 +01:00
parent 8fa06038fe
commit 1c54782dd7
4 changed files with 130 additions and 36 deletions
Generated
+1 -1
View File
@@ -1418,7 +1418,7 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]] [[package]]
name = "furumusic" name = "furumusic"
version = "0.4.1" version = "0.4.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
+54
View File
@@ -822,6 +822,7 @@ document.addEventListener('alpine:init', () => {
_playLocal(track, options = {}) { _playLocal(track, options = {}) {
this.currentTrack = track; this.currentTrack = track;
Alpine.store('queue')?.syncCurrentIndexToTrack(track);
this._localSourceTrackId = track.id; this._localSourceTrackId = track.id;
this._historyRecorded = false; this._historyRecorded = false;
this._resetPlaybackTracking(); this._resetPlaybackTracking();
@@ -1138,6 +1139,7 @@ document.addEventListener('alpine:init', () => {
_mirrorRemoteTrack(track, playing, positionSeconds = null) { _mirrorRemoteTrack(track, playing, positionSeconds = null) {
if (!track) return; if (!track) return;
this.currentTrack = track; this.currentTrack = track;
Alpine.store('queue')?.syncCurrentIndexToTrack(track);
this.isPlaying = !!playing; this.isPlaying = !!playing;
if (positionSeconds !== null) this.currentTime = Math.max(0, Number(positionSeconds || 0)); if (positionSeconds !== null) this.currentTime = Math.max(0, Number(positionSeconds || 0));
this.duration = Number(track.duration_seconds || this.duration || 0); this.duration = Number(track.duration_seconds || this.duration || 0);
@@ -1158,6 +1160,7 @@ document.addEventListener('alpine:init', () => {
const track = state.track || queue?.tracks?.[queue.currentIndex] || null; const track = state.track || queue?.tracks?.[queue.currentIndex] || null;
if (track) { if (track) {
this.currentTrack = track; this.currentTrack = track;
queue?.syncCurrentIndexToTrack(track);
} }
this.shuffle = !!state.shuffle; this.shuffle = !!state.shuffle;
this.repeatMode = state.repeat_mode || 'off'; this.repeatMode = state.repeat_mode || 'off';
@@ -1359,6 +1362,7 @@ document.addEventListener('alpine:init', () => {
: tracks[idx]; : tracks[idx];
if (currentTrack) { if (currentTrack) {
this.currentTrack = currentTrack; this.currentTrack = currentTrack;
queue.syncCurrentIndexToTrack(currentTrack);
this._localSourceTrackId = currentTrack.id; this._localSourceTrackId = currentTrack.id;
this._historyRecorded = false; this._historyRecorded = false;
this._resetPlaybackTracking(); this._resetPlaybackTracking();
@@ -1976,6 +1980,56 @@ document.addEventListener('alpine:init', () => {
return this.tracks.slice(start, start + limit); return this.tracks.slice(start, start + limit);
}, },
effectiveCurrentIndex() {
const currentTrack = Alpine.store('player')?.currentTrack || null;
if (currentTrack?.id) {
return this.tracks.findIndex(track => Number(track?.id) === Number(currentTrack.id));
}
if (!this.tracks.length) return -1;
return Math.max(0, Math.min(Number(this.currentIndex || 0), this.tracks.length - 1));
},
queueItemState(index) {
const current = this.effectiveCurrentIndex();
if (current < 0) return 'upcoming';
if (index < current) return 'played';
if (index === current) return 'current';
return 'upcoming';
},
displayItems() {
const current = this.effectiveCurrentIndex();
const playerTrack = Alpine.store('player')?.currentTrack || null;
const items = this.tracks.map((track, index) => ({
track,
index,
key: `${index}-${track?.id || 'track'}`,
state: current >= 0
? (index < current ? 'played' : (index === current ? 'current' : 'upcoming'))
: 'upcoming',
synthetic: false,
}));
if (playerTrack?.id && current < 0) {
items.unshift({
track: playerTrack,
index: -1,
key: `current-${playerTrack.id}`,
state: 'current',
synthetic: true,
});
}
return items;
},
syncCurrentIndexToTrack(track) {
if (!track?.id || !this.tracks.length) return -1;
const index = this.tracks.findIndex(item => Number(item?.id) === Number(track.id));
if (index >= 0) this.currentIndex = index;
return index;
},
addToEnd(tracks) { addToEnd(tracks) {
const items = this._tracksForQueueAdd(tracks); const items = this._tracksForQueueAdd(tracks);
if (!items.length) return; if (!items.length) return;
+34 -33
View File
@@ -959,42 +959,43 @@
</div> </div>
</div> </div>
<div class="queue-tracks"> <div class="queue-tracks">
<template x-if="$store.queue.tracks.length === 0"> <template x-if="$store.queue.displayItems().length === 0">
<div class="empty-state"> <div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
<p>{{ t.player_queue_empty }}</p> <p>{{ t.player_queue_empty }}</p>
</div> </div>
</template> </template>
<template x-for="(track, idx) in $store.queue.tracks" :key="idx + '-' + track.id"> <template x-for="item in $store.queue.displayItems()" :key="item.key">
<div class="queue-track" <div class="queue-track"
:data-queue-index="idx" :data-queue-index="item.index"
:class="{ active: idx === $store.queue.currentIndex, dragging: $store.queue._dragIdx === idx, 'foreign-jam-track': $store.queue.isForeignJamTrack(track) }" :class="{ active: item.state === 'current', current: item.state === 'current', played: item.state === 'played', dragging: $store.queue._dragIdx === item.index, synthetic: item.synthetic, 'foreign-jam-track': $store.queue.isForeignJamTrack(item.track) }"
:style="$store.queue.isForeignJamTrack(track) ? $store.queue.contributorStyle(track) : ''" :style="$store.queue.isForeignJamTrack(item.track) ? $store.queue.contributorStyle(item.track) : ''"
@click="$store.queue.playFromIndex(idx)" @click="item.index >= 0 ? $store.queue.playFromIndex(item.index) : $store.player.play(item.track)"
draggable="true" :draggable="!item.synthetic"
@dragstart="$store.queue._dragIdx = idx; $event.dataTransfer.effectAllowed = 'move'" @dragstart="if (item.synthetic) { $event.preventDefault(); } else { $store.queue._dragIdx = item.index; $event.dataTransfer.effectAllowed = 'move'; }"
@dragend="$store.queue._dragIdx = null; document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'))" @dragend="$store.queue._dragIdx = null; document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'))"
@dragover.prevent="$event.dataTransfer.dropEffect = 'move'; $event.currentTarget.classList.add('drag-over')" @dragover.prevent="$event.dataTransfer.dropEffect = 'move'; $event.currentTarget.classList.add('drag-over')"
@dragleave="$event.currentTarget.classList.remove('drag-over')" @dragleave="$event.currentTarget.classList.remove('drag-over')"
@drop.prevent="$event.currentTarget.classList.remove('drag-over'); if ($store.queue._dragIdx !== null) { $store.queue.moveTrack($store.queue._dragIdx, idx); $store.queue._dragIdx = null; }"> @drop.prevent="$event.currentTarget.classList.remove('drag-over'); if (!item.synthetic && $store.queue._dragIdx !== null) { $store.queue.moveTrack($store.queue._dragIdx, item.index); $store.queue._dragIdx = null; }">
<div class="queue-drag-handle" <div class="queue-drag-handle"
x-show="!item.synthetic"
@mousedown.stop @mousedown.stop
@click.stop @click.stop
@pointerdown.stop="$store.queue.startPointerReorder($event, idx)"> @pointerdown.stop="$store.queue.startPointerReorder($event, item.index)">
<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg> <svg viewBox="0 0 24 24" fill="currentColor"><circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>
</div> </div>
<div class="queue-track-cover"> <div class="queue-track-cover">
<template x-if="track.cover_url"> <template x-if="item.track.cover_url">
<img :src="track.cover_url" :alt="track.title" loading="lazy"> <img :src="item.track.cover_url" :alt="item.track.title" loading="lazy">
</template> </template>
<template x-if="!track.cover_url"> <template x-if="!item.track.cover_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</template> </template>
</div> </div>
<div class="queue-track-info"> <div class="queue-track-info">
<div class="queue-track-title" x-text="track.title"></div> <div class="queue-track-title" x-text="item.track.title"></div>
<div class="queue-track-artist"> <div class="queue-track-artist">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx"> <template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(item.track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span> <span>
<template x-if="artistIdx > 0"><span>, </span></template> <template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a> <a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
@@ -1004,15 +1005,15 @@
</div> </div>
<div class="queue-track-actions"> <div class="queue-track-actions">
<button class="queue-track-remove info-btn popularity-info-btn" <button class="queue-track-remove info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }" :class="{ 'has-popularity': $store.library.hasPopularity(item.track), 'no-popularity': !$store.library.hasPopularity(item.track) }"
:style="$store.library.popularityStyle(track)" :style="$store.library.popularityStyle(item.track)"
@click.stop="$store.library.openTrackInfo(track)" @click.stop="$store.library.openTrackInfo(item.track)"
:title="$store.library.trackInfoTitle(track)" :title="$store.library.trackInfoTitle(item.track)"
aria-label="{{ t.player_track_info }}"> aria-label="{{ t.player_track_info }}">
<span x-show="$store.library.hasPopularity(track)" x-text="$store.library.popularityLabel(track)"></span> <span x-show="$store.library.hasPopularity(item.track)" x-text="$store.library.popularityLabel(item.track)"></span>
<span x-show="!$store.library.hasPopularity(track)" class="info-letter">i</span> <span x-show="!$store.library.hasPopularity(item.track)" class="info-letter">i</span>
</button> </button>
<button class="queue-track-remove" @click.stop="$store.queue.remove(idx)" title="{{ t.player_remove }}"> <button class="queue-track-remove" x-show="!item.synthetic" @click.stop="$store.queue.remove(item.index)" title="{{ t.player_remove }}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button> </button>
</div> </div>
@@ -1292,27 +1293,27 @@
</div> </div>
<div class="mobile-expanded-queue"> <div class="mobile-expanded-queue">
<div class="mobile-expanded-queue-title">{{ t.player_queue }}</div> <div class="mobile-expanded-queue-title">{{ t.player_queue }}</div>
<template x-if="$store.queue.upcoming().length === 0"> <template x-if="$store.queue.displayItems().length === 0">
<div class="mobile-expanded-queue-empty">{{ t.player_queue_empty }}</div> <div class="mobile-expanded-queue-empty">{{ t.player_queue_empty }}</div>
</template> </template>
<template x-for="(track, idx) in $store.queue.upcoming()" :key="'mobile-expanded-queue-' + track.id + '-' + idx"> <template x-for="item in $store.queue.displayItems()" :key="'mobile-expanded-queue-' + item.key">
<button class="mobile-expanded-queue-row" <button class="mobile-expanded-queue-row"
:class="{ 'foreign-jam-track': $store.queue.isForeignJamTrack(track) }" :class="{ current: item.state === 'current', played: item.state === 'played', 'foreign-jam-track': $store.queue.isForeignJamTrack(item.track) }"
:style="$store.queue.isForeignJamTrack(track) ? $store.queue.contributorStyle(track) : ''" :style="$store.queue.isForeignJamTrack(item.track) ? $store.queue.contributorStyle(item.track) : ''"
type="button" type="button"
@click="$store.queue.playFromIndex($store.queue.currentIndex + idx + 1)"> @click="item.index >= 0 ? $store.queue.playFromIndex(item.index) : $store.player.play(item.track)">
<div class="mobile-expanded-queue-cover"> <div class="mobile-expanded-queue-cover">
<template x-if="track.cover_url"> <template x-if="item.track.cover_url">
<img :src="track.cover_url" :alt="track.title" loading="lazy"> <img :src="item.track.cover_url" :alt="item.track.title" loading="lazy">
</template> </template>
<template x-if="!track.cover_url"> <template x-if="!item.track.cover_url">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
</template> </template>
</div> </div>
<div class="mobile-expanded-queue-info"> <div class="mobile-expanded-queue-info">
<div class="mobile-expanded-queue-name" x-text="track.title"></div> <div class="mobile-expanded-queue-name" x-text="item.track.title"></div>
<div class="mobile-expanded-queue-artist"> <div class="mobile-expanded-queue-artist">
<template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(track)" :key="artist.label + '-' + artist.id + '-' + artistIdx"> <template x-for="(artist, artistIdx) in $store.library.trackArtistLinks(item.track)" :key="artist.label + '-' + artist.id + '-' + artistIdx">
<span> <span>
<template x-if="artistIdx > 0"><span>, </span></template> <template x-if="artistIdx > 0"><span>, </span></template>
<span x-text="artist.label"></span> <span x-text="artist.label"></span>
@@ -1320,7 +1321,7 @@
</template> </template>
</div> </div>
</div> </div>
<span class="mobile-expanded-queue-time" x-text="formatTime(track.duration_seconds)"></span> <span class="mobile-expanded-queue-time" x-text="formatTime(item.track.duration_seconds)"></span>
</button> </button>
</template> </template>
</div> </div>
+41 -2
View File
@@ -1224,7 +1224,22 @@ button.user-stat:hover {
} }
.queue-track:hover { background: var(--bg-hover); } .queue-track:hover { background: var(--bg-hover); }
.queue-track.active { background: var(--bg-active); } .queue-track.active,
.queue-track.current { background: var(--bg-active); }
.queue-track.played {
color: var(--text-subdued);
opacity: 0.58;
}
.queue-track.played:hover {
opacity: 0.78;
}
.queue-track.played .queue-track-cover {
filter: grayscale(1);
opacity: 0.72;
}
.queue-track.synthetic .queue-drag-handle {
display: none;
}
.queue-track.foreign-jam-track { .queue-track.foreign-jam-track {
background: linear-gradient(90deg, var(--jam-contributor-bg, rgba(82,145,255,0.12)), transparent 78%); background: linear-gradient(90deg, var(--jam-contributor-bg, rgba(82,145,255,0.12)), transparent 78%);
} }
@@ -1259,7 +1274,9 @@ button.user-stat:hover {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.queue-track.active .queue-track-title { color: var(--accent); } .queue-track.active .queue-track-title,
.queue-track.current .queue-track-title { color: var(--accent); }
.queue-track.played .queue-track-title { color: var(--text-subdued); }
.queue-track-artist { .queue-track-artist {
font-size: 11px; font-size: 11px;
@@ -4925,6 +4942,28 @@ button.user-stat:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
.mobile-expanded-queue-row.current {
background: var(--bg-active);
}
.mobile-expanded-queue-row.current .mobile-expanded-queue-name {
color: var(--accent);
}
.mobile-expanded-queue-row.played {
color: var(--text-subdued);
opacity: 0.56;
}
.mobile-expanded-queue-row.played:active {
opacity: 0.74;
}
.mobile-expanded-queue-row.played .mobile-expanded-queue-cover {
filter: grayscale(1);
opacity: 0.72;
}
.mobile-expanded-queue-row.foreign-jam-track { .mobile-expanded-queue-row.foreign-jam-track {
background: linear-gradient(90deg, var(--jam-contributor-bg, rgba(82,145,255,0.12)), transparent 82%); background: linear-gradient(90deg, var(--jam-contributor-bg, rgba(82,145,255,0.12)), transparent 82%);
} }