-
{$t('player.nowPlaying')}
-
-
- ('off');
let queue = $state([]);
let queueTotalTracks = $state(0);
+ let historyTracks = $state([]);
+ let infinitePlayEnabled = $state(false);
// Toast State (from store subscription)
let toast = $state(null);
@@ -1040,6 +1042,54 @@
closeQueue();
}
+ // Toggle infinite play mode (auto-refill queue with similar tracks)
+ function handleToggleInfinitePlay() {
+ infinitePlayEnabled = !infinitePlayEnabled;
+ // Persist to localStorage
+ try {
+ localStorage.setItem('qbz-infinite-play', JSON.stringify(infinitePlayEnabled));
+ } catch {
+ // Ignore storage errors
+ }
+ showToast(infinitePlayEnabled ? 'Infinite play enabled' : 'Infinite play disabled', 'info');
+ }
+
+ // Play a track from history
+ async function handlePlayHistoryTrack(trackId: string) {
+ try {
+ // Get the full queue state to find the track in history
+ const queueState = await getBackendQueueState();
+ if (!queueState) {
+ showToast('Failed to play track', 'error');
+ return;
+ }
+
+ // Find the track in history
+ const numericId = parseInt(trackId, 10);
+ const historyTrack = queueState.history.find(t => t.id === numericId);
+ if (!historyTrack) {
+ showToast('Track not found in history', 'error');
+ return;
+ }
+
+ // Play the track directly
+ await handleTrackPlay({
+ id: historyTrack.id,
+ title: historyTrack.title,
+ artist: historyTrack.artist,
+ album: historyTrack.album,
+ duration: historyTrack.duration_secs,
+ artwork: historyTrack.artwork_url || '',
+ quality: historyTrack.hires ? 'Hi-Res' : 'CD',
+ bitDepth: historyTrack.bit_depth || 16,
+ samplingRate: historyTrack.sample_rate || 44100
+ });
+ } catch (err) {
+ console.error('Failed to play history track:', err);
+ showToast('Failed to play track', 'error');
+ }
+ }
+
// Play all tracks from album (starting from first track)
async function handlePlayAllAlbum() {
if (!selectedAlbum?.tracks?.length) return;
@@ -1827,6 +1877,16 @@
initPlaybackContextStore();
initPlaybackPreferences();
+ // Load infinite play preference
+ try {
+ const stored = localStorage.getItem('qbz-infinite-play');
+ if (stored !== null) {
+ infinitePlayEnabled = JSON.parse(stored);
+ }
+ } catch {
+ // Ignore storage errors
+ }
+
// Note: loadFavorites() is called in handleLoginSuccess after login is confirmed
// This prevents API calls before authentication is complete
@@ -2066,10 +2126,23 @@
};
});
- // Sync queue state when opening queue panel
+ // Sync queue state when opening queue panel (including history)
$effect(() => {
if (isQueueOpen) {
syncQueueState();
+ // Also fetch history from backend
+ getBackendQueueState().then((state) => {
+ if (state?.history) {
+ historyTracks = state.history.map(t => ({
+ id: String(t.id),
+ artwork: t.artwork_url || '',
+ title: t.title,
+ artist: t.artist,
+ duration: formatDuration(t.duration_secs),
+ trackId: t.id
+ }));
+ }
+ });
}
});
@@ -2093,7 +2166,8 @@
artwork: currentTrack.artwork,
title: currentTrack.title,
artist: currentTrack.artist,
- duration: formatDuration(currentTrack.duration)
+ duration: formatDuration(currentTrack.duration),
+ trackId: currentTrack.id // For favorite checking in QueuePanel
} : null);
@@ -2448,10 +2522,15 @@
onClose={closeQueue}
currentTrack={currentQueueTrack ?? undefined}
upcomingTracks={queue}
+ {historyTracks}
onPlayTrack={handleQueueTrackPlay}
+ onPlayHistoryTrack={handlePlayHistoryTrack}
onClearQueue={handleClearQueue}
onSaveAsPlaylist={handleSaveQueueAsPlaylist}
onReorderTrack={handleQueueReorder}
+ onToggleInfinitePlay={handleToggleInfinitePlay}
+ {infinitePlayEnabled}
+ {isPlaying}
/>
diff --git a/static/add-to-list.svg b/static/add-to-list.svg
new file mode 100644
index 000000000..a8a5fcf5d
--- /dev/null
+++ b/static/add-to-list.svg
@@ -0,0 +1,7 @@
+
+
+
\ No newline at end of file
diff --git a/static/trash-list.svg b/static/trash-list.svg
new file mode 100644
index 000000000..2c599711a
--- /dev/null
+++ b/static/trash-list.svg
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
-
@@ -260,34 +411,29 @@
z-index: 40;
}
- :global(.queue-panel-container) {
+ .queue-panel {
position: fixed;
- top: 32px; /* Below TitleBar */
+ top: 32px;
right: 0;
- bottom: 104px; /* Above NowPlayingBar */
+ bottom: 104px;
width: 340px;
z-index: 50;
display: flex;
flex-direction: column;
- animation: slideInRight 200ms ease-out;
+ animation: slideIn 200ms ease-out;
background-color: var(--bg-secondary);
border-left: 1px solid var(--bg-tertiary);
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
}
- /* Remove old glass panel override */
-
- @keyframes slideInRight {
- from {
- transform: translateX(100%);
- }
- to {
- transform: translateX(0);
- }
+ @keyframes slideIn {
+ from { transform: translateX(100%); }
+ to { transform: translateX(0); }
}
+ /* Header */
.header {
- padding: 16px;
+ padding: 12px 16px;
border-bottom: 1px solid var(--bg-tertiary);
display: flex;
align-items: center;
@@ -295,12 +441,65 @@
flex-shrink: 0;
}
- .header h2 {
- font-size: 16px;
+ .tabs {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .tab {
+ background: none;
+ border: none;
+ font-size: 14px;
font-weight: 600;
+ color: var(--text-muted);
+ cursor: pointer;
+ padding: 0;
+ transition: color 150ms ease;
+ }
+
+ .tab:hover {
+ color: var(--text-secondary);
+ }
+
+ .tab.active {
color: var(--text-primary);
}
+ .tab-separator {
+ color: var(--text-disabled);
+ font-size: 14px;
+ }
+
+ .header-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .header-btn {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 150ms ease;
+ }
+
+ .header-btn:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+ }
+
+ .header-btn.active {
+ color: var(--accent-primary);
+ }
+
.close-btn {
width: 28px;
height: 28px;
@@ -318,62 +517,16 @@
color: var(--text-primary);
}
- .search-container {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 12px;
- padding: 8px 10px;
- background-color: var(--bg-tertiary);
- border-radius: 6px;
- flex-shrink: 0;
- flex-grow: 0;
- }
-
- .search-container :global(.search-icon) {
- color: var(--text-muted);
- flex-shrink: 0;
- }
-
- .search-input {
- flex: 1;
- background: none;
- border: none;
- color: var(--text-primary);
- font-size: 12px;
- outline: none;
- }
-
- .search-input::placeholder {
- color: var(--text-muted);
- }
-
- .search-clear {
- background: none;
- border: none;
- color: var(--text-muted);
- cursor: pointer;
- padding: 2px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
-
- .search-clear:hover {
- color: var(--text-primary);
- }
-
+ /* Content */
.content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 12px 16px;
- padding-right: 12px; /* Less padding on right for scrollbar */
min-height: 0;
overscroll-behavior: contain;
}
- /* Thin fancy scrollbar */
.content::-webkit-scrollbar {
width: 4px;
}
@@ -391,50 +544,181 @@
background: rgba(255, 255, 255, 0.25);
}
- .content::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.4);
- }
-
+ /* Sections */
.section {
margin-bottom: 16px;
}
- .now-playing-section {
- flex-shrink: 0;
- }
-
- .next-up-section {
- display: flex;
- flex-direction: column;
- min-height: 0;
- }
-
- .section-header {
+ .section-label {
font-size: 11px;
- text-transform: uppercase;
- color: #666666;
font-weight: 600;
+ color: var(--text-muted);
letter-spacing: 0.05em;
margin-bottom: 10px;
- flex-shrink: 0;
}
+ /* Now Playing Card */
.now-playing-card {
- background-color: var(--bg-tertiary);
- border-radius: 6px;
+ background: var(--bg-tertiary);
+ border-radius: 8px;
padding: 10px;
display: flex;
align-items: center;
- gap: 10px;
+ gap: 12px;
}
- .now-playing-card img {
+ .np-artwork {
+ position: relative;
width: 48px;
height: 48px;
+ flex-shrink: 0;
+ }
+
+ .np-artwork img {
+ width: 100%;
+ height: 100%;
border-radius: 4px;
object-fit: cover;
}
+ .spectrum-bars {
+ position: absolute;
+ bottom: 4px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: flex-end;
+ gap: 2px;
+ height: 16px;
+ }
+
+ .spectrum-bars .bar {
+ width: 3px;
+ background-color: #4285F4;
+ border-radius: 1px;
+ animation: spectrum 1s ease-in-out infinite;
+ }
+
+ .spectrum-bars .bar:nth-child(1) {
+ height: 8px;
+ animation-delay: 0s;
+ }
+
+ .spectrum-bars .bar:nth-child(2) {
+ height: 12px;
+ animation-delay: 0.15s;
+ }
+
+ .spectrum-bars .bar:nth-child(3) {
+ height: 6px;
+ animation-delay: 0.3s;
+ }
+
+ .spectrum-bars .bar:nth-child(4) {
+ height: 10px;
+ animation-delay: 0.45s;
+ }
+
+ @keyframes spectrum {
+ 0%, 100% {
+ transform: scaleY(0.5);
+ opacity: 0.7;
+ }
+ 50% {
+ transform: scaleY(1);
+ opacity: 1;
+ }
+ }
+
+ .np-info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .np-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .np-artist {
+ font-size: 12px;
+ color: var(--text-muted);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .np-favorite {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 150ms ease;
+ flex-shrink: 0;
+ }
+
+ .np-favorite:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+ }
+
+ .np-favorite.active {
+ color: #ef4444;
+ }
+
+ /* Queue Tracks */
+ .tracks-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .queue-track {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 6px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background-color 150ms ease;
+ }
+
+ .queue-track:hover {
+ background: var(--bg-tertiary);
+ }
+
+ .queue-track.dragging {
+ opacity: 0.5;
+ }
+
+ .queue-track.drag-over {
+ border-top: 2px solid var(--accent-primary);
+ margin-top: -2px;
+ }
+
+ .queue-track.unavailable {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ .track-number {
+ width: 24px;
+ font-size: 13px;
+ color: var(--text-muted);
+ text-align: center;
+ flex-shrink: 0;
+ }
+
.track-info {
flex: 1;
min-width: 0;
@@ -457,95 +741,106 @@
white-space: nowrap;
}
- .tracks {
+ .track-duration {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+ font-variant-numeric: tabular-nums;
+ flex-shrink: 0;
+ }
+
+ .load-more {
+ padding: 12px;
+ text-align: center;
+ background: none;
+ border: none;
+ color: var(--accent-primary);
+ font-size: 12px;
+ cursor: pointer;
+ width: 100%;
+ border-radius: 6px;
+ transition: background 150ms ease;
+ }
+
+ .load-more:hover {
+ background: var(--bg-tertiary);
+ }
+
+ /* History */
+ .history-list {
display: flex;
flex-direction: column;
gap: 2px;
}
- .queue-track {
- height: 40px;
+ .history-track {
display: flex;
align-items: center;
- gap: 8px;
- padding: 0 6px;
+ gap: 10px;
+ padding: 6px;
border-radius: 6px;
cursor: pointer;
transition: background-color 150ms ease;
+ position: relative;
}
- .queue-track.hovered {
- background-color: var(--bg-tertiary);
+ .history-track:hover {
+ background: var(--bg-tertiary);
}
- .queue-track.dragging {
- opacity: 0.5;
- background-color: var(--bg-tertiary);
+ .history-artwork {
+ width: 40px;
+ height: 40px;
+ border-radius: 4px;
+ object-fit: cover;
+ flex-shrink: 0;
}
- .queue-track.drag-over {
- border-top: 2px solid var(--accent-primary);
- margin-top: -2px;
- }
-
- .queue-track[draggable="true"] {
- user-select: none;
- -webkit-user-select: none;
- }
-
- .drag-handle {
- color: #666666;
- opacity: 0;
- transition: opacity 150ms ease;
- cursor: grab;
- }
-
- .drag-handle:active {
- cursor: grabbing;
- }
-
- .drag-handle.visible {
- opacity: 1;
- }
-
- /* Unavailable track styles (offline mode) */
- .queue-track.unavailable {
- opacity: 0.4;
- cursor: not-allowed;
- pointer-events: none;
- }
-
- .queue-track.unavailable .track-info,
- .queue-track.unavailable .track-number,
- .queue-track.unavailable .track-duration {
- filter: grayscale(100%);
- }
-
- .unavailable-indicator {
- color: var(--text-muted);
- display: flex;
- align-items: center;
- justify-content: center;
- width: 14px;
- }
-
- .track-number {
+ .track-menu {
+ position: absolute;
+ right: 6px;
width: 28px;
+ height: 28px;
display: flex;
align-items: center;
justify-content: center;
+ background: var(--bg-secondary);
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ border-radius: 4px;
}
- .track-number span {
+ .track-menu:hover {
+ color: var(--text-primary);
+ }
+
+ /* Empty State */
+ .empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 48px 0;
+ text-align: center;
+ }
+
+ .emoji {
+ font-size: 32px;
+ margin-bottom: 12px;
+ }
+
+ .empty-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+ }
+
+ .empty-text {
font-size: 13px;
- color: #666666;
- }
-
- .track-duration {
- font-size: 12px;
- color: #666666;
- font-family: var(--font-mono);
- font-variant-numeric: tabular-nums;
+ color: var(--text-muted);
+ max-width: 200px;
}
.no-results {
@@ -555,33 +850,7 @@
font-size: 12px;
}
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 64px 0;
- text-align: center;
- }
-
- .emoji {
- font-size: 24px;
- margin-bottom: 16px;
- }
-
- .empty-title {
- font-size: 16px;
- font-weight: 500;
- color: var(--text-primary);
- margin-bottom: 8px;
- }
-
- .empty-text {
- font-size: 14px;
- color: var(--text-muted);
- max-width: 240px;
- }
-
+ /* Footer */
.footer {
padding: 12px 16px;
border-top: 1px solid var(--bg-tertiary);
@@ -591,32 +860,97 @@
flex-shrink: 0;
}
- .clear-btn {
- font-size: 12px;
- color: var(--text-muted);
+ .footer-left,
+ .footer-right {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .footer-icon-btn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
background: none;
border: none;
+ color: var(--text-muted);
cursor: pointer;
- transition: color 150ms ease;
+ border-radius: 6px;
+ transition: all 150ms ease;
}
- .clear-btn:hover {
+ .footer-icon-btn:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+ }
+
+ .footer-icon {
+ width: 18px;
+ height: 18px;
+ filter: brightness(0) saturate(100%) invert(50%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%);
+ transition: filter 150ms ease;
+ }
+
+ .footer-icon-btn:hover .footer-icon {
+ filter: brightness(0) saturate(100%) invert(100%);
+ }
+
+ /* Search Bar */
+ .search-bar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 10px;
+ background: var(--bg-tertiary);
+ border-radius: 6px;
+ width: 180px;
+ animation: expandSearch 150ms ease-out;
+ }
+
+ @keyframes expandSearch {
+ from {
+ width: 32px;
+ opacity: 0;
+ }
+ to {
+ width: 180px;
+ opacity: 1;
+ }
+ }
+
+ .search-bar :global(svg) {
+ color: var(--text-muted);
+ flex-shrink: 0;
+ }
+
+ .search-input {
+ flex: 1;
+ background: none;
+ border: none;
+ color: var(--text-primary);
+ font-size: 12px;
+ outline: none;
+ min-width: 0;
+ }
+
+ .search-input::placeholder {
+ color: var(--text-muted);
+ }
+
+ .search-close {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ padding: 2px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .search-close:hover {
color: var(--text-primary);
}
-
- .save-btn {
- padding: 6px 14px;
- border-radius: 6px;
- background-color: var(--accent-primary);
- color: white;
- font-size: 12px;
- font-weight: 500;
- border: none;
- cursor: pointer;
- transition: background-color 150ms ease;
- }
-
- .save-btn:hover {
- background-color: var(--accent-hover);
- }
diff --git a/src/lib/i18n/locales/en.json b/src/lib/i18n/locales/en.json
index 8e857ac6c..7efcf45d1 100644
--- a/src/lib/i18n/locales/en.json
+++ b/src/lib/i18n/locales/en.json
@@ -221,6 +221,11 @@
"queue": "Queue",
"upNext": "Up Next",
"history": "History",
+ "recentlyPlayed": "Recently Played",
+ "noHistoryYet": "No tracks played yet",
+ "infinitePlay": "Infinite Play",
+ "infinitePlayEnabled": "Infinite play enabled",
+ "infinitePlayDisabled": "Infinite play disabled",
"lyrics": "Lyrics",
"noLyrics": "No lyrics available",
"fetchingLyrics": "Fetching lyrics...",
@@ -299,7 +304,8 @@
"yes": "Yes",
"no": "No",
"ok": "OK",
- "loading": "Loading..."
+ "loading": "Loading...",
+ "loadMore": "Load More"
},
"settings": {
"title": "Settings",
diff --git a/src/lib/i18n/locales/es.json b/src/lib/i18n/locales/es.json
index d0730ae10..dd2028fcb 100644
--- a/src/lib/i18n/locales/es.json
+++ b/src/lib/i18n/locales/es.json
@@ -221,6 +221,11 @@
"queue": "Cola",
"upNext": "Siguiente",
"history": "Historial",
+ "recentlyPlayed": "Reproducido Recientemente",
+ "noHistoryYet": "Sin pistas reproducidas aún",
+ "infinitePlay": "Reproducción Infinita",
+ "infinitePlayEnabled": "Reproducción infinita activada",
+ "infinitePlayDisabled": "Reproducción infinita desactivada",
"lyrics": "Letras",
"noLyrics": "No hay letras disponibles",
"fetchingLyrics": "Buscando letras...",
@@ -299,7 +304,8 @@
"yes": "Sí",
"no": "No",
"ok": "OK",
- "loading": "Cargando..."
+ "loading": "Cargando...",
+ "loadMore": "Cargar Más"
},
"settings": {
"title": "Ajustes",
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 47a6f3e6a..05928c17a 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -331,6 +331,8 @@
let repeatMode = $state{currentTrack.title}
- {currentTrack.artist}
+
+
+ {#if activeTab === 'queue'}
+
+ {#if currentTrack}
+
- {/if}
+ {/if}
-
- {#if upcomingTracks.length > 0}
-
+
- {$t('player.nowPlaying').toUpperCase()}
+
+
+
+ {#if isPlaying}
+
+
+
+
+
+
+
+ {/if}
+
+
+
{currentTrack.title}
+ {currentTrack.artist}
+
-
-
- {#if searchQuery}
-
+
+ {#if upcomingTracks.length > 0}
+
-
- {#if upcomingTracks.length > 0 || currentTrack}
+
+ {#if activeTab === 'queue' && (upcomingTracks.length > 0 || currentTrack)}
+
+ {:else if !currentTrack}
+
+ {$t('player.upNext').toUpperCase()} ({displayedTracks}/{totalTracks})
+
+
+ {#each filteredTracks as track, index}
+ {@const originalIndex = upcomingTracks.findIndex(t => t.id === track.id)}
+ {@const isUnavailable = track.available === false}
+
+ handleTrackClick(track)}
+ ondragstart={(e) => handleDragStart(e, originalIndex)}
+ ondragover={(e) => handleDragOver(e, originalIndex)}
+ ondragleave={handleDragLeave}
+ ondrop={(e) => handleDrop(e, originalIndex)}
+ ondragend={handleDragEnd}
+ role="button"
+ tabindex={isUnavailable ? -1 : 0}
+ >
+ {originalIndex + 1}
+
+ {/each}
+ {#if hasMoreTracks}
+
+ {/if}
+ {#if searchQuery && filteredTracks.length === 0}
+
+
+ {track.duration}
+ {track.title}
+ {track.artist}
+ {$t('player.noTracksMatch', { values: { query: searchQuery } })}
+ {/if}
+
+
+ {/if}
+
+ {:else}
+
+ 🎵
+ {$t('player.queueEmpty')}
+ {$t('player.queueEmptyDescription')}
+
+
{/if}
-
-
- {#if upcomingTracks.length > 0}
- {$t('player.recentlyPlayed').toUpperCase()}
+ {#if historyTracks.length > 0}
+
+ {#each historyTracks.slice(0, DISPLAY_LIMIT) as track}
+
+ {:else}
+ handleHistoryTrackClick(track)}
+ onmouseenter={() => hoveredHistoryTrack = track.id}
+ onmouseleave={() => hoveredHistoryTrack = null}
+ role="button"
+ tabindex="0"
+ >
+
+
+ {/each}
+
+
+ {track.duration}
+ {#if hoveredHistoryTrack === track.id}
+
+ {/if}
+ {track.title}
+ {track.artist}
+
+
{/if}
{$t('player.noHistoryYet')}
+
-
- {/if}
-
-
- {#if upcomingTracks.length === 0 && !currentTrack}
-
- {$t('player.upNext')} ({filteredTracks.length}{searchQuery ? ` / ${upcomingTracks.length}` : ''})
-
-
- {#each filteredTracks as track, index}
- {@const originalIndex = upcomingTracks.findIndex(t => t.id === track.id)}
- {@const isUnavailable = track.available === false}
-
- (hoveredTrack = track.id)}
- onmouseleave={() => (hoveredTrack = null)}
- onclick={(e) => handleTrackClick(e, track)}
- ondragstart={(e) => handleDragStart(e, originalIndex)}
- ondragover={(e) => handleDragOver(e, originalIndex)}
- ondragleave={handleDragLeave}
- ondrop={(e) => handleDrop(e, originalIndex)}
- ondragend={handleDragEnd}
- role="button"
- tabindex={isUnavailable ? -1 : 0}
- title={isUnavailable ? $t('offline.trackNotAvailable') : undefined}
- onkeydown={(e) => e.key === 'Enter' && !isUnavailable && onPlayTrack?.(track.id)}
- >
-
- {#if isUnavailable}
-
- {/each}
- {#if searchQuery && filteredTracks.length === 0}
-
-
-
- {:else}
-
-
-
- {/if}
-
-
-
- {#if hoveredTrack === track.id && !isUnavailable}
-
- {:else}
- {originalIndex + 1}
- {/if}
-
-
-
-
-
-
-
- {track.title}
- {track.artist}
- {track.duration}
- {$t('player.noTracksMatch', { values: { query: searchQuery } })}
- {/if}
-
-
- {/if}
🎵
- {$t('player.queueEmpty')}
- {$t('player.queueEmptyDescription')}
-
-
-
+
{/if}
+
+
+
+
+ {#if searchOpen}
+
+
+
+
+
+ {:else}
+
+ {/if}
+