feat(ui): queue panel improvements and playlist album column

Queue Panel:
- Fix scroll by constraining panel between TitleBar and NowPlayingBar
- Add search bar to filter entire queue (not just first 20)
- Reduce font sizes by 1px for compact layout
- Reduce item height from 48px to 40px

TrackRow:
- Add album prop to display album column

PlaylistDetailView:
- Pass album data to TrackRow for album column display
This commit is contained in:
vicrodh committed 2026-01-11 18:51:38 -06:00
1 parent 4d2fdce1aa
commit f9cf4f06cd
3 files changed
+153 -39

No files matched your search

+135 -39
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { X, GripVertical, Play } from 'lucide-svelte'; import { X, GripVertical, Play, Search } from 'lucide-svelte';
import GlassSurface from './glass/GlassSurface.svelte'; import GlassSurface from './glass/GlassSurface.svelte';
interface QueueTrack { interface QueueTrack {
@@ -31,6 +31,17 @@
}: Props = $props(); }: Props = $props();
let hoveredTrack = $state<string | null>(null); let hoveredTrack = $state<string | null>(null);
let searchQuery = $state('');
// Filter tracks based on search query (searches entire queue)
const filteredTracks = $derived.by(() => {
if (!searchQuery.trim()) return upcomingTracks;
const query = searchQuery.toLowerCase();
return upcomingTracks.filter(track =>
track.title.toLowerCase().includes(query) ||
track.artist.toLowerCase().includes(query)
);
});
</script> </script>
{#if isOpen} {#if isOpen}
@@ -43,10 +54,28 @@
<div class="header"> <div class="header">
<h2>Queue</h2> <h2>Queue</h2>
<button class="close-btn" onclick={onClose}> <button class="close-btn" onclick={onClose}>
<X size={24} /> <X size={20} />
</button> </button>
</div> </div>
<!-- Search Bar -->
{#if upcomingTracks.length > 0}
<div class="search-container">
<Search size={14} class="search-icon" />
<input
type="text"
placeholder="Search in queue..."
bind:value={searchQuery}
class="search-input"
/>
{#if searchQuery}
<button class="search-clear" onclick={() => searchQuery = ''}>
<X size={12} />
</button>
{/if}
</div>
{/if}
<!-- Content --> <!-- Content -->
<div class="content"> <div class="content">
<!-- Now Playing --> <!-- Now Playing -->
@@ -65,10 +94,13 @@
<!-- Next Up --> <!-- Next Up -->
{#if upcomingTracks.length > 0} {#if upcomingTracks.length > 0}
<div class="section"> <div class="section next-up-section">
<div class="section-header">Next Up ({upcomingTracks.length})</div> <div class="section-header">
Next Up ({filteredTracks.length}{searchQuery ? ` / ${upcomingTracks.length}` : ''})
</div>
<div class="tracks"> <div class="tracks">
{#each upcomingTracks as track, index} {#each filteredTracks as track, index}
{@const originalIndex = upcomingTracks.findIndex(t => t.id === track.id)}
<div <div
class="queue-track" class="queue-track"
class:hovered={hoveredTrack === track.id} class:hovered={hoveredTrack === track.id}
@@ -81,15 +113,15 @@
> >
<!-- Drag Handle --> <!-- Drag Handle -->
<div class="drag-handle" class:visible={hoveredTrack === track.id}> <div class="drag-handle" class:visible={hoveredTrack === track.id}>
<GripVertical size={16} /> <GripVertical size={14} />
</div> </div>
<!-- Track Number / Play Icon --> <!-- Track Number / Play Icon -->
<div class="track-number"> <div class="track-number">
{#if hoveredTrack === track.id} {#if hoveredTrack === track.id}
<Play size={14} fill="white" color="white" /> <Play size={12} fill="white" color="white" />
{:else} {:else}
<span>{index + 1}</span> <span>{originalIndex + 1}</span>
{/if} {/if}
</div> </div>
@@ -103,6 +135,9 @@
<div class="track-duration">{track.duration}</div> <div class="track-duration">{track.duration}</div>
</div> </div>
{/each} {/each}
{#if searchQuery && filteredTracks.length === 0}
<div class="no-results">No tracks match "{searchQuery}"</div>
{/if}
</div> </div>
</div> </div>
{/if} {/if}
@@ -137,10 +172,10 @@
:global(.queue-panel) { :global(.queue-panel) {
position: fixed; position: fixed;
top: 0; top: 32px; /* Below TitleBar */
right: 0; right: 0;
bottom: 0; bottom: 104px; /* Above NowPlayingBar */
width: 360px; width: 340px;
z-index: 50; z-index: 50;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -162,22 +197,23 @@
} }
.header { .header {
padding: 20px; padding: 16px;
border-bottom: 1px solid var(--bg-tertiary); border-bottom: 1px solid var(--bg-tertiary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-shrink: 0;
} }
.header h2 { .header h2 {
font-size: 18px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
} }
.close-btn { .close-btn {
width: 32px; width: 28px;
height: 32px; height: 28px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -192,37 +228,89 @@
color: var(--text-primary); color: var(--text-primary);
} }
.search-container {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 16px;
padding: 8px 10px;
background-color: var(--bg-tertiary);
border-radius: 6px;
flex-shrink: 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; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 16px 20px; padding: 12px 16px;
min-height: 0;
} }
.section { .section {
margin-bottom: 24px; margin-bottom: 20px;
}
.next-up-section {
display: flex;
flex-direction: column;
min-height: 0;
} }
.section-header { .section-header {
font-size: 12px; font-size: 11px;
text-transform: uppercase; text-transform: uppercase;
color: #666666; color: #666666;
font-weight: 600; font-weight: 600;
letter-spacing: 0.05em; letter-spacing: 0.05em;
margin-bottom: 12px; margin-bottom: 10px;
flex-shrink: 0;
} }
.now-playing-card { .now-playing-card {
background-color: var(--bg-tertiary); background-color: var(--bg-tertiary);
border-radius: 8px; border-radius: 6px;
padding: 12px; padding: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
} }
.now-playing-card img { .now-playing-card img {
width: 56px; width: 48px;
height: 56px; height: 48px;
border-radius: 4px; border-radius: 4px;
object-fit: cover; object-fit: cover;
} }
@@ -233,7 +321,7 @@
} }
.track-title { .track-title {
font-size: 14px; font-size: 13px;
font-weight: 500; font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
overflow: hidden; overflow: hidden;
@@ -242,7 +330,7 @@
} }
.track-artist { .track-artist {
font-size: 13px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -252,16 +340,16 @@
.tracks { .tracks {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 2px;
} }
.queue-track { .queue-track {
height: 48px; height: 40px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 8px;
padding: 0 8px; padding: 0 6px;
border-radius: 8px; border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: background-color 150ms ease; transition: background-color 150ms ease;
} }
@@ -281,24 +369,31 @@
} }
.track-number { .track-number {
width: 32px; width: 28px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.track-number span { .track-number span {
font-size: 14px; font-size: 13px;
color: #666666; color: #666666;
} }
.track-duration { .track-duration {
font-size: 13px; font-size: 12px;
color: #666666; color: #666666;
font-family: var(--font-mono); font-family: var(--font-mono);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.no-results {
padding: 24px;
text-align: center;
color: var(--text-muted);
font-size: 12px;
}
.empty-state { .empty-state {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -327,15 +422,16 @@
} }
.footer { .footer {
padding: 16px; padding: 12px 16px;
border-top: 1px solid var(--bg-tertiary); border-top: 1px solid var(--bg-tertiary);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
flex-shrink: 0;
} }
.clear-btn { .clear-btn {
font-size: 14px; font-size: 12px;
color: var(--text-muted); color: var(--text-muted);
background: none; background: none;
border: none; border: none;
@@ -348,11 +444,11 @@
} }
.save-btn { .save-btn {
padding: 8px 16px; padding: 6px 14px;
border-radius: 8px; border-radius: 6px;
background-color: var(--accent-primary); background-color: var(--accent-primary);
color: white; color: white;
font-size: 14px; font-size: 12px;
font-weight: 500; font-weight: 500;
border: none; border: none;
cursor: pointer; cursor: pointer;
+17
View File
@@ -16,6 +16,7 @@
number: number; number: number;
title: string; title: string;
artist?: string; artist?: string;
album?: string;
duration: string; duration: string;
quality?: string; quality?: string;
isPlaying?: boolean; isPlaying?: boolean;
@@ -47,6 +48,7 @@
number, number,
title, title,
artist, artist,
album,
duration, duration,
quality, quality,
isPlaying = false, isPlaying = false,
@@ -129,6 +131,11 @@
<div class="track-artist-column">{artist}</div> <div class="track-artist-column">{artist}</div>
{/if} {/if}
<!-- Album Column -->
{#if album}
<div class="track-album">{album}</div>
{/if}
<!-- Duration --> <!-- Duration -->
<div class="track-duration">{duration}</div> <div class="track-duration">{duration}</div>
@@ -302,6 +309,16 @@
white-space: nowrap; white-space: nowrap;
} }
.track-album {
flex: 1;
min-width: 0;
font-size: 13px;
color: var(--text-muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.track-duration { .track-duration {
font-size: 14px; font-size: 14px;
color: var(--text-muted); color: var(--text-muted);
@@ -644,6 +644,7 @@
number={idx + 1} number={idx + 1}
title={track.title} title={track.title}
artist={track.artist} artist={track.artist}
album={track.album}
duration={track.duration} duration={track.duration}
quality={track.hires ? 'Hi-Res' : undefined} quality={track.hires ? 'Hi-Res' : undefined}
hideFavorite={track.isLocal} hideFavorite={track.isLocal}