feat(ui): add stack icon to all 4 players

- Create StackIcon component with reactive context subscription
- Display Layers icon before track title when context exists
- Show tooltip with origin info on hover
- Add to: NowPlayingBar, ExpandedPlayer, FocusMode, MiniPlayer
- Icon hidden when no playback context (single track)
- Responsive sizing per player (14px-18px)

cp
This commit is contained in:
vicrodh committed 2026-01-18 00:39:25 -06:00
1 parent 49fe9e6b11
commit cf5be434e3
5 files changed
+128 -5

No files matched your search

+19 -2
View File
@@ -2,6 +2,7 @@
import { invoke } from '@tauri-apps/api/core';
import { X, Shuffle, SkipBack, Play, Pause, SkipForward, Repeat, Heart, List, Maximize2, MoreHorizontal, Cast } from 'lucide-svelte';
import QualityBadge from './QualityBadge.svelte';
import StackIcon from './StackIcon.svelte';
import LyricsLines from './lyrics/LyricsLines.svelte';
import { startActiveLineUpdates } from '$lib/stores/lyricsStore';
@@ -244,7 +245,10 @@
</div>
<div class="track-info">
<h1 class="title">{trackTitle}</h1>
<div class="title-row">
<StackIcon size={18} class="stack-icon" />
<h1 class="title">{trackTitle}</h1>
</div>
<h2 class="artist">{artist}</h2>
<h3 class="album">{album}</h3>
<div class="quality-info">
@@ -508,14 +512,27 @@
width: 100%;
}
.title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.title-row :global(.stack-icon) {
flex-shrink: 0;
}
.title {
font-size: 22px;
font-weight: 600;
color: white;
margin: 0 0 6px;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
}
.artist {
+19 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { SkipBack, Play, Pause, SkipForward, ChevronDown } from 'lucide-svelte';
import StackIcon from './StackIcon.svelte';
import LyricsLines from './lyrics/LyricsLines.svelte';
import { startActiveLineUpdates, stopActiveLineUpdates } from '$lib/stores/lyricsStore';
import { t } from '$lib/i18n';
@@ -184,7 +185,10 @@
<div class="track-info">
<img src={artwork} alt="" class="mini-artwork" />
<div class="track-meta">
<div class="track-title">{trackTitle}</div>
<div class="track-title-row">
<StackIcon size={14} class="stack-icon" />
<div class="track-title">{trackTitle}</div>
</div>
<div class="track-artist">{artist}</div>
</div>
</div>
@@ -464,6 +468,18 @@
flex: 1;
}
.track-title-row {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
}
.track-title-row :global(.stack-icon) {
flex-shrink: 0;
}
.track-title {
font-size: 14px;
font-weight: 600;
@@ -471,6 +487,8 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.track-artist {
+21 -1
View File
@@ -20,6 +20,7 @@
} from 'lucide-svelte';
import QualityBadge from './QualityBadge.svelte';
import AudioOutputBadges from './AudioOutputBadges.svelte';
import StackIcon from './StackIcon.svelte';
import { t } from '$lib/i18n';
import {
subscribe as subscribeOffline,
@@ -294,7 +295,10 @@
</button>
<div class="song-info">
<span class="song-title" title={trackTitle}>{trackTitle}</span>
<div class="song-title-row">
<StackIcon size={14} class="stack-icon" />
<span class="song-title" title={trackTitle}>{trackTitle}</span>
</div>
<div class="song-meta">
{#if artist}
<button class="meta-link" onclick={onArtistClick} title={$t('actions.goToArtist')}>
@@ -676,6 +680,20 @@
align-self: center;
}
.song-title-row {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
width: 100%;
}
.song-title-row :global(.stack-icon) {
flex-shrink: 0;
margin-top: 1px;
}
.song-title {
font-size: 13px;
font-weight: 500;
@@ -683,6 +701,8 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
}
.song-meta {
+50
View File
@@ -0,0 +1,50 @@
<script lang="ts">
import { Layers } from 'lucide-svelte';
import { subscribe, getCurrentContext, getContextDisplayInfo } from '$lib/stores/playbackContextStore';
import { onMount, onDestroy } from 'svelte';
interface Props {
size?: number;
class?: string;
}
let { size = 16, class: className = '' }: Props = $props();
let context = $state(getCurrentContext());
let displayInfo = $state(context ? getContextDisplayInfo() : null);
let unsubscribe: (() => void) | null = null;
onMount(() => {
unsubscribe = subscribe(() => {
context = getCurrentContext();
displayInfo = context ? getContextDisplayInfo() : null;
});
});
onDestroy(() => {
unsubscribe?.();
});
</script>
{#if context && displayInfo}
<div class="stack-icon-wrapper {className}" title="Playing from: {displayInfo}">
<Layers size={size} strokeWidth={2} />
</div>
{/if}
<style>
.stack-icon-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
opacity: 0.7;
transition: opacity 0.2s;
flex-shrink: 0;
}
.stack-icon-wrapper:hover {
opacity: 1;
}
</style>
+19 -1
View File
@@ -12,6 +12,7 @@
Maximize2,
ListMusic
} from 'lucide-svelte';
import StackIcon from '$lib/components/StackIcon.svelte';
import {
subscribe as subscribePlayer,
getPlayerState,
@@ -230,7 +231,10 @@
<Maximize2 size={14} />
</button>
<div class="track-info">
<div class="title">{playerState.currentTrack?.title ?? 'No track'}</div>
<div class="title-row">
<StackIcon size={14} class="stack-icon" />
<div class="title">{playerState.currentTrack?.title ?? 'No track'}</div>
</div>
<div class="artist-album">
{playerState.currentTrack?.artist ?? '—'}
{#if playerState.currentTrack?.album}
@@ -408,6 +412,18 @@
padding-right: 30px;
}
.title-row {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
overflow: hidden;
}
.title-row :global(.stack-icon) {
flex-shrink: 0;
}
.title {
font-weight: 600;
font-size: 14px;
@@ -416,6 +432,8 @@
text-overflow: ellipsis;
color: #fff;
line-height: 1.4;
flex: 1;
min-width: 0;
}
.artist-album {