fix(miniplayer): refine queue artwork overlays and inline controls
cx
This commit is contained in:
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;
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user