fix(miniplayer): refine queue artwork overlays and inline controls

cx
This commit is contained in:
vicrodh committed 2026-02-26 02:22:52 -06:00
1 parent e0cfc5d92b
commit d6852500bf
4 files changed
+116 -151

No files matched your search

@@ -29,6 +29,7 @@
onExpand?: () => void;
onClose?: () => void;
onStartDrag?: (event: MouseEvent) => void;
showWindowControls?: boolean;
}
let {
@@ -55,7 +56,8 @@
onTogglePin,
onExpand,
onClose,
onStartDrag
onStartDrag,
showWindowControls = false
}: Props = $props();
let seekRef: HTMLDivElement | null = $state(null);
@@ -77,7 +79,7 @@
const microTrackOffset = $derived(microTrackOverflow > 0 ? `-${microTrackOverflow + 16}px` : '0px');
const microTrackDuration = $derived(microTrackOverflow > 0 ? `${(microTrackOverflow + 16) / tickerSpeed}s` : '0s');
const canRenderInlineWindowControls = $derived(
micro && !!activeSurface && !!onSurfaceChange && !!onTogglePin && !!onExpand && !!onClose && !!onStartDrag
!!activeSurface && !!onSurfaceChange && !!onTogglePin && !!onExpand && !!onClose && !!onStartDrag
);
function getMicroTrackLine(): string {
@@ -214,20 +216,6 @@
>
<span class="micro-track-text" bind:this={microTrackTextRef}>{getMicroTrackLine()}</span>
</div>
{#if canRenderInlineWindowControls}
<div class="micro-window-controls">
<MiniPlayerWindowControls
micro
activeSurface={activeSurface!}
isPinned={isPinned}
onSurfaceChange={onSurfaceChange!}
onTogglePin={onTogglePin!}
onExpand={onExpand!}
onClose={onClose!}
onStartDrag={onStartDrag!}
/>
</div>
{/if}
</div>
{/if}
@@ -344,6 +332,21 @@
</div>
{/if}
</div>
{#if canRenderInlineWindowControls}
<div class="window-controls-anchor" class:visible={showWindowControls}>
<MiniPlayerWindowControls
micro={micro}
activeSurface={activeSurface!}
isPinned={isPinned}
onSurfaceChange={onSurfaceChange!}
onTogglePin={onTogglePin!}
onExpand={onExpand!}
onClose={onClose!}
onStartDrag={onStartDrag!}
/>
</div>
{/if}
</div>
{#if micro}
@@ -402,8 +405,8 @@
}
.micro-track {
width: calc(100% - 52px);
max-width: calc(100% - 52px);
width: 100%;
max-width: 100%;
min-width: 0;
padding: 0;
white-space: nowrap;
@@ -442,25 +445,6 @@
}
}
.micro-window-controls {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
-webkit-app-region: no-drag;
app-region: no-drag;
opacity: 0;
pointer-events: none;
contain: paint;
transition: opacity 120ms ease;
}
.micro-header:hover .micro-window-controls,
.micro-window-controls:hover {
opacity: 1;
pointer-events: auto;
}
.seek-wrapper {
position: relative;
cursor: pointer;
@@ -545,6 +529,26 @@
transform: translateY(-50%);
}
.window-controls-anchor {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
opacity: 0;
pointer-events: none;
-webkit-app-region: no-drag;
app-region: no-drag;
transition: opacity 120ms ease;
z-index: 12;
}
.window-controls-anchor.visible {
opacity: 1;
pointer-events: auto;
}
.volume-trigger {
width: 30px;
height: 30px;
@@ -25,37 +25,23 @@
<div class="active-bar" aria-hidden="true"></div>
{/if}
<div class="row-leading" aria-hidden="true">
{#if activeTrack}
{#if isPlaying}
<div class="playing-indicator">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
{:else}
<Play size={12} />
{/if}
<div class="row-artwork-wrap">
{#if queueTrack.artwork}
<img src={queueTrack.artwork} alt={queueTrack.title} class="row-artwork" />
{:else}
<div class="row-artwork placeholder" aria-hidden="true"></div>
{/if}
</div>
{#if queueTrack.artwork}
<img src={queueTrack.artwork} alt={queueTrack.title} class="row-artwork" />
{:else}
<div class="row-artwork placeholder" aria-hidden="true"></div>
{/if}
<div class="row-meta">
<div class="row-title">{queueTrack.title}</div>
<div class="row-artist">{queueTrack.artist}</div>
</div>
<div class="row-actions">
{#if queueTrack.quality}
<span class="quality">{queueTrack.quality}</span>
{#if activeTrack && isPlaying}
<div class="artwork-playing-indicator" aria-hidden="true">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
{/if}
<button
class="row-play-btn"
class="artwork-play-btn"
type="button"
title={$t('player.play')}
aria-label={$t('player.play')}
@@ -67,6 +53,15 @@
<Play size={14} />
</button>
</div>
<div class="row-meta">
<div class="row-title">{queueTrack.title}</div>
<div class="row-artist">{queueTrack.artist}</div>
</div>
{#if queueTrack.quality}
<span class="quality">{queueTrack.quality}</span>
{/if}
</div>
{/each}
</div>
@@ -126,14 +121,13 @@
background: var(--accent-primary);
}
.row-leading {
width: 12px;
height: 18px;
.row-artwork-wrap {
position: relative;
width: 36px;
height: 36px;
border-radius: 5px;
overflow: hidden;
flex-shrink: 0;
color: var(--accent-primary);
display: inline-flex;
align-items: center;
justify-content: center;
}
.row-artwork {
@@ -179,51 +173,57 @@
}
.quality {
margin-left: auto;
flex-shrink: 0;
font-size: 11px;
color: var(--text-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
}
.row-actions {
margin-left: auto;
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 8px;
}
.row-play-btn {
width: 24px;
height: 24px;
.artwork-play-btn {
position: absolute;
inset: 0;
border: none;
border-radius: 999px;
border-radius: 5px;
padding: 0;
cursor: pointer;
background: transparent;
color: var(--text-muted);
background: color-mix(in srgb, black 52%, transparent);
color: var(--text-primary);
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 120ms ease, color 120ms ease;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 120ms ease;
}
.row-play-btn:hover {
background: var(--alpha-10);
color: var(--text-primary);
.row-artwork-wrap:hover .artwork-play-btn,
.row-artwork-wrap:focus-within .artwork-play-btn {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.queue-row.active .row-play-btn {
color: var(--accent-primary);
}
.playing-indicator {
.artwork-playing-indicator {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 2px;
padding: 3px 4px;
border-radius: 999px;
background: color-mix(in srgb, black 48%, transparent);
transition: opacity 120ms ease;
}
.playing-indicator .bar {
.row-artwork-wrap:hover .artwork-playing-indicator {
opacity: 0;
}
.artwork-playing-indicator .bar {
width: 2px;
background-color: var(--accent-primary);
border-radius: 9999px;
@@ -231,16 +231,16 @@
animation: equalize 1s ease-in-out infinite;
}
.playing-indicator .bar:nth-child(1) {
.artwork-playing-indicator .bar:nth-child(1) {
height: 10px;
}
.playing-indicator .bar:nth-child(2) {
.artwork-playing-indicator .bar:nth-child(2) {
height: 13px;
animation-delay: 0.15s;
}
.playing-indicator .bar:nth-child(3) {
.artwork-playing-indicator .bar:nth-child(3) {
height: 8px;
animation-delay: 0.3s;
}
@@ -1,7 +1,6 @@
<script lang="ts">
import type { LyricsLine } from '$lib/stores/lyricsStore';
import type { MiniPlayerSurface, MiniPlayerQueueTrack } from './types';
import MiniPlayerWindowControls from './MiniPlayerWindowControls.svelte';
import MiniPlayerCompactSurface from './MiniPlayerCompactSurface.svelte';
import MiniPlayerArtworkSurface from './MiniPlayerArtworkSurface.svelte';
import MiniPlayerQueueSurface from './MiniPlayerQueueSurface.svelte';
@@ -78,29 +77,20 @@
const compactSurface = $derived(activeSurface === 'compact');
const microSurface = $derived(activeSurface === 'micro');
let windowHovered = $state(false);
</script>
<div
class="mini-player-window"
class:compact={compactSurface}
class:micro={microSurface}
onmouseenter={() => {
windowHovered = true;
}}
onmouseleave={() => {
windowHovered = false;
}}
>
{#if !microSurface}
<div class="titlebar-row">
<div class="controls-slot">
<MiniPlayerWindowControls
{activeSurface}
isPinned={isPinned}
onSurfaceChange={onSurfaceChange}
onTogglePin={onTogglePin}
onExpand={onExpand}
onClose={onClose}
onStartDrag={onStartDrag}
/>
</div>
</div>
{/if}
{#if !microSurface}
<div class="surface-area">
{#if activeSurface === 'compact'}
@@ -150,6 +140,7 @@
onExpand={onExpand}
onClose={onClose}
onStartDrag={onStartDrag}
showWindowControls={windowHovered}
/>
</div>
@@ -176,35 +167,6 @@
border-radius: 9px;
}
.titlebar-row {
position: absolute;
top: 6px;
right: 8px;
z-index: 40;
display: flex;
align-items: center;
}
.mini-player-window.compact .titlebar-row {
top: 4px;
right: 8px;
}
.controls-slot {
display: flex;
align-items: center;
opacity: 0;
pointer-events: none;
transform: translateY(-3px);
transition: opacity 140ms ease, transform 140ms ease;
}
.mini-player-window:hover .controls-slot {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.surface-area {
flex: 1;
min-height: 0;
+6 -7
View File
@@ -89,12 +89,11 @@
return surface === 'micro' || surface === 'compact';
}
function formatQualityLabel(track: PlayerState['currentTrack']): string | undefined {
if (!track) return undefined;
if (track.bitDepth && track.samplingRate) {
return `${track.bitDepth}/${Math.round(track.samplingRate / 1000)}`;
}
return track.quality || undefined;
function formatDurationLabel(totalSeconds: number): string | undefined {
if (!isFinite(totalSeconds) || totalSeconds <= 0) return undefined;
const minutes = Math.floor(totalSeconds / 60);
const seconds = Math.floor(totalSeconds % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
const activeSurface = $derived(miniState.surface);
@@ -109,7 +108,7 @@
title: playerState.currentTrack.title,
artist: playerState.currentTrack.artist,
artwork: playerState.currentTrack.artwork,
quality: formatQualityLabel(playerState.currentTrack)
quality: formatDurationLabel(playerState.duration || playerState.currentTrack.duration)
});
}