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
+54
View File
@@ -822,6 +822,7 @@ document.addEventListener('alpine:init', () => {
_playLocal(track, options = {}) {
this.currentTrack = track;
Alpine.store('queue')?.syncCurrentIndexToTrack(track);
this._localSourceTrackId = track.id;
this._historyRecorded = false;
this._resetPlaybackTracking();
@@ -1138,6 +1139,7 @@ document.addEventListener('alpine:init', () => {
_mirrorRemoteTrack(track, playing, positionSeconds = null) {
if (!track) return;
this.currentTrack = track;
Alpine.store('queue')?.syncCurrentIndexToTrack(track);
this.isPlaying = !!playing;
if (positionSeconds !== null) this.currentTime = Math.max(0, Number(positionSeconds || 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;
if (track) {
this.currentTrack = track;
queue?.syncCurrentIndexToTrack(track);
}
this.shuffle = !!state.shuffle;
this.repeatMode = state.repeat_mode || 'off';
@@ -1359,6 +1362,7 @@ document.addEventListener('alpine:init', () => {
: tracks[idx];
if (currentTrack) {
this.currentTrack = currentTrack;
queue.syncCurrentIndexToTrack(currentTrack);
this._localSourceTrackId = currentTrack.id;
this._historyRecorded = false;
this._resetPlaybackTracking();
@@ -1976,6 +1980,56 @@ document.addEventListener('alpine:init', () => {
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) {
const items = this._tracksForQueueAdd(tracks);
if (!items.length) return;
+34 -33
View File
@@ -959,42 +959,43 @@
</div>
</div>
<div class="queue-tracks">
<template x-if="$store.queue.tracks.length === 0">
<template x-if="$store.queue.displayItems().length === 0">
<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>
<p>{{ t.player_queue_empty }}</p>
</div>
</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"
:data-queue-index="idx"
:class="{ active: idx === $store.queue.currentIndex, dragging: $store.queue._dragIdx === idx, 'foreign-jam-track': $store.queue.isForeignJamTrack(track) }"
:style="$store.queue.isForeignJamTrack(track) ? $store.queue.contributorStyle(track) : ''"
@click="$store.queue.playFromIndex(idx)"
draggable="true"
@dragstart="$store.queue._dragIdx = idx; $event.dataTransfer.effectAllowed = 'move'"
:data-queue-index="item.index"
: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(item.track) ? $store.queue.contributorStyle(item.track) : ''"
@click="item.index >= 0 ? $store.queue.playFromIndex(item.index) : $store.player.play(item.track)"
:draggable="!item.synthetic"
@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'))"
@dragover.prevent="$event.dataTransfer.dropEffect = 'move'; $event.currentTarget.classList.add('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"
x-show="!item.synthetic"
@mousedown.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>
</div>
<div class="queue-track-cover">
<template x-if="track.cover_url">
<img :src="track.cover_url" :alt="track.title" loading="lazy">
<template x-if="item.track.cover_url">
<img :src="item.track.cover_url" :alt="item.track.title" loading="lazy">
</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>
</template>
</div>
<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">
<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>
<template x-if="artistIdx > 0"><span>, </span></template>
<a class="artist-link" @click.stop="$store.library.openArtist(artist.id)" x-text="artist.label"></a>
@@ -1004,15 +1005,15 @@
</div>
<div class="queue-track-actions">
<button class="queue-track-remove info-btn popularity-info-btn"
:class="{ 'has-popularity': $store.library.hasPopularity(track), 'no-popularity': !$store.library.hasPopularity(track) }"
:style="$store.library.popularityStyle(track)"
@click.stop="$store.library.openTrackInfo(track)"
:title="$store.library.trackInfoTitle(track)"
:class="{ 'has-popularity': $store.library.hasPopularity(item.track), 'no-popularity': !$store.library.hasPopularity(item.track) }"
:style="$store.library.popularityStyle(item.track)"
@click.stop="$store.library.openTrackInfo(item.track)"
:title="$store.library.trackInfoTitle(item.track)"
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(track)" class="info-letter">i</span>
<span x-show="$store.library.hasPopularity(item.track)" x-text="$store.library.popularityLabel(item.track)"></span>
<span x-show="!$store.library.hasPopularity(item.track)" class="info-letter">i</span>
</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>
</button>
</div>
@@ -1292,27 +1293,27 @@
</div>
<div class="mobile-expanded-queue">
<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>
</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"
:class="{ 'foreign-jam-track': $store.queue.isForeignJamTrack(track) }"
:style="$store.queue.isForeignJamTrack(track) ? $store.queue.contributorStyle(track) : ''"
:class="{ current: item.state === 'current', played: item.state === 'played', 'foreign-jam-track': $store.queue.isForeignJamTrack(item.track) }"
:style="$store.queue.isForeignJamTrack(item.track) ? $store.queue.contributorStyle(item.track) : ''"
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">
<template x-if="track.cover_url">
<img :src="track.cover_url" :alt="track.title" loading="lazy">
<template x-if="item.track.cover_url">
<img :src="item.track.cover_url" :alt="item.track.title" loading="lazy">
</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>
</template>
</div>
<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">
<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>
<template x-if="artistIdx > 0"><span>, </span></template>
<span x-text="artist.label"></span>
@@ -1320,7 +1321,7 @@
</template>
</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>
</template>
</div>
+41 -2
View File
@@ -1224,7 +1224,22 @@ button.user-stat: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 {
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;
}
.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 {
font-size: 11px;
@@ -4925,6 +4942,28 @@ button.user-stat: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 {
background: linear-gradient(90deg, var(--jam-contributor-bg, rgba(82,145,255,0.12)), transparent 82%);
}