feat(immersive): add real coverflow with queue navigation
- Rename old CoverflowPanel to StaticPanel (single artwork view) - Create new CoverflowPanel with actual coverflow effect: - Shows 5 album covers (2 prev, current, 2 next) - 3D perspective rotation on side covers - Click on side covers to navigate queue - Smooth animation on track change - Add 'static' tab to focus mode (4 tabs total) - Update keyboard shortcuts: 1-4 for focus tabs
This commit is contained in:
1 parent
1aa7e9eb3a
commit
9099f94904
4 files changed
+489
-35
No files matched your search
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Disc3, Mic2, ListMusic, Music2, Info, Radio, Maximize, Minimize, ChevronDown, X, Square, Copy, Minus } from 'lucide-svelte';
|
import { Disc3, Mic2, ListMusic, Music2, Info, Radio, Maximize, Minimize, ChevronDown, X, Square, Copy, Minus, Image } from 'lucide-svelte';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
|
||||||
export type ImmersiveTab = 'lyrics' | 'trackInfo' | 'suggestions' | 'queue';
|
export type ImmersiveTab = 'lyrics' | 'trackInfo' | 'suggestions' | 'queue';
|
||||||
export type FocusTab = 'coverflow' | 'lyrics-focus' | 'queue-focus';
|
export type FocusTab = 'coverflow' | 'static' | 'lyrics-focus' | 'queue-focus';
|
||||||
export type ViewMode = 'focus' | 'split';
|
export type ViewMode = 'focus' | 'split';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -85,6 +85,7 @@
|
|||||||
// Focus mode tabs
|
// Focus mode tabs
|
||||||
const focusTabs: { id: FocusTab; label: string; icon: typeof Disc3 }[] = [
|
const focusTabs: { id: FocusTab; label: string; icon: typeof Disc3 }[] = [
|
||||||
{ id: 'coverflow', label: 'Coverflow', icon: Disc3 },
|
{ id: 'coverflow', label: 'Coverflow', icon: Disc3 },
|
||||||
|
{ id: 'static', label: 'Static', icon: Image },
|
||||||
{ id: 'lyrics-focus', label: 'Lyrics', icon: Mic2 },
|
{ id: 'lyrics-focus', label: 'Lyrics', icon: Mic2 },
|
||||||
{ id: 'queue-focus', label: 'Queue', icon: ListMusic },
|
{ id: 'queue-focus', label: 'Queue', icon: ListMusic },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import SuggestionsPanel from './panels/SuggestionsPanel.svelte';
|
import SuggestionsPanel from './panels/SuggestionsPanel.svelte';
|
||||||
import QueuePanel from './panels/QueuePanel.svelte';
|
import QueuePanel from './panels/QueuePanel.svelte';
|
||||||
import CoverflowPanel from './panels/CoverflowPanel.svelte';
|
import CoverflowPanel from './panels/CoverflowPanel.svelte';
|
||||||
|
import StaticPanel from './panels/StaticPanel.svelte';
|
||||||
import LyricsFocusPanel from './panels/LyricsFocusPanel.svelte';
|
import LyricsFocusPanel from './panels/LyricsFocusPanel.svelte';
|
||||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||||
|
|
||||||
@@ -239,9 +240,12 @@
|
|||||||
if (viewMode === 'focus') activeFocusTab = 'coverflow';
|
if (viewMode === 'focus') activeFocusTab = 'coverflow';
|
||||||
break;
|
break;
|
||||||
case '2':
|
case '2':
|
||||||
if (viewMode === 'focus') activeFocusTab = 'lyrics-focus';
|
if (viewMode === 'focus') activeFocusTab = 'static';
|
||||||
break;
|
break;
|
||||||
case '3':
|
case '3':
|
||||||
|
if (viewMode === 'focus') activeFocusTab = 'lyrics-focus';
|
||||||
|
break;
|
||||||
|
case '4':
|
||||||
if (viewMode === 'focus') activeFocusTab = 'queue-focus';
|
if (viewMode === 'focus') activeFocusTab = 'queue-focus';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -306,7 +310,7 @@
|
|||||||
{#if viewMode === 'focus'}
|
{#if viewMode === 'focus'}
|
||||||
<!-- Focus Mode Views -->
|
<!-- Focus Mode Views -->
|
||||||
{#if activeFocusTab === 'coverflow'}
|
{#if activeFocusTab === 'coverflow'}
|
||||||
<!-- Coverflow: Centered artwork -->
|
<!-- Coverflow: Animated carousel of album covers -->
|
||||||
<CoverflowPanel
|
<CoverflowPanel
|
||||||
{artwork}
|
{artwork}
|
||||||
{trackTitle}
|
{trackTitle}
|
||||||
@@ -316,6 +320,21 @@
|
|||||||
{quality}
|
{quality}
|
||||||
{bitDepth}
|
{bitDepth}
|
||||||
{samplingRate}
|
{samplingRate}
|
||||||
|
{queueTracks}
|
||||||
|
{queueCurrentIndex}
|
||||||
|
onNavigate={(index) => onQueuePlayTrack?.(index)}
|
||||||
|
/>
|
||||||
|
{:else if activeFocusTab === 'static'}
|
||||||
|
<!-- Static: Single centered artwork -->
|
||||||
|
<StaticPanel
|
||||||
|
{artwork}
|
||||||
|
{trackTitle}
|
||||||
|
{artist}
|
||||||
|
{album}
|
||||||
|
{isPlaying}
|
||||||
|
{quality}
|
||||||
|
{bitDepth}
|
||||||
|
{samplingRate}
|
||||||
/>
|
/>
|
||||||
{:else if activeFocusTab === 'lyrics-focus'}
|
{:else if activeFocusTab === 'lyrics-focus'}
|
||||||
<!-- Lyrics Focus: Single line, large, centered -->
|
<!-- Lyrics Focus: Single line, large, centered -->
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||||
|
|
||||||
|
interface QueueTrack {
|
||||||
|
id: string | number;
|
||||||
|
title: string;
|
||||||
|
artist: string;
|
||||||
|
artwork: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
artwork: string;
|
artwork: string;
|
||||||
trackTitle: string;
|
trackTitle: string;
|
||||||
@@ -10,6 +17,10 @@
|
|||||||
quality?: string;
|
quality?: string;
|
||||||
bitDepth?: number;
|
bitDepth?: number;
|
||||||
samplingRate?: number;
|
samplingRate?: number;
|
||||||
|
// Queue for coverflow
|
||||||
|
queueTracks?: QueueTrack[];
|
||||||
|
queueCurrentIndex?: number;
|
||||||
|
onNavigate?: (index: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -20,15 +31,87 @@
|
|||||||
isPlaying = false,
|
isPlaying = false,
|
||||||
quality,
|
quality,
|
||||||
bitDepth,
|
bitDepth,
|
||||||
samplingRate
|
samplingRate,
|
||||||
|
queueTracks = [],
|
||||||
|
queueCurrentIndex = 0,
|
||||||
|
onNavigate
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
// Build coverflow items: 2 prev + current + 2 next
|
||||||
|
const coverflowItems = $derived.by(() => {
|
||||||
|
const items: { index: number; artwork: string; title: string; artist: string; position: number }[] = [];
|
||||||
|
|
||||||
|
// Position: -2, -1, 0 (center), 1, 2
|
||||||
|
for (let offset = -2; offset <= 2; offset++) {
|
||||||
|
const queueIndex = queueCurrentIndex + offset;
|
||||||
|
|
||||||
|
if (queueIndex >= 0 && queueIndex < queueTracks.length) {
|
||||||
|
const track = queueTracks[queueIndex];
|
||||||
|
items.push({
|
||||||
|
index: queueIndex,
|
||||||
|
artwork: track.artwork,
|
||||||
|
title: track.title,
|
||||||
|
artist: track.artist,
|
||||||
|
position: offset
|
||||||
|
});
|
||||||
|
} else if (offset === 0) {
|
||||||
|
// Always show current even if queue is empty
|
||||||
|
items.push({
|
||||||
|
index: queueCurrentIndex,
|
||||||
|
artwork,
|
||||||
|
title: trackTitle,
|
||||||
|
artist,
|
||||||
|
position: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track previous index for animation direction
|
||||||
|
let previousIndex = $state(queueCurrentIndex);
|
||||||
|
let animationDirection = $state<'left' | 'right' | null>(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (queueCurrentIndex !== previousIndex) {
|
||||||
|
animationDirection = queueCurrentIndex > previousIndex ? 'left' : 'right';
|
||||||
|
previousIndex = queueCurrentIndex;
|
||||||
|
|
||||||
|
// Reset animation direction after animation completes
|
||||||
|
setTimeout(() => {
|
||||||
|
animationDirection = null;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleCoverClick(index: number) {
|
||||||
|
if (index !== queueCurrentIndex && onNavigate) {
|
||||||
|
onNavigate(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="coverflow-panel">
|
<div class="coverflow-panel">
|
||||||
<div class="artwork-wrapper">
|
<div class="coverflow-container" class:animate-left={animationDirection === 'left'} class:animate-right={animationDirection === 'right'}>
|
||||||
<div class="artwork-container" class:playing={isPlaying}>
|
{#each coverflowItems as item (item.index)}
|
||||||
<img src={artwork} alt={trackTitle} class="artwork" />
|
<button
|
||||||
</div>
|
class="coverflow-item"
|
||||||
|
class:center={item.position === 0}
|
||||||
|
class:left-1={item.position === -1}
|
||||||
|
class:left-2={item.position === -2}
|
||||||
|
class:right-1={item.position === 1}
|
||||||
|
class:right-2={item.position === 2}
|
||||||
|
onclick={() => handleCoverClick(item.index)}
|
||||||
|
disabled={item.position === 0}
|
||||||
|
title={item.position === 0 ? undefined : `${item.title} - ${item.artist}`}
|
||||||
|
>
|
||||||
|
<div class="cover-wrapper">
|
||||||
|
<img src={item.artwork} alt={item.title} class="cover-image" />
|
||||||
|
<div class="cover-reflection"></div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
@@ -62,46 +145,150 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 20px;
|
gap: 24px;
|
||||||
/* Offset for header (70px) and controls (120px) to achieve true visual center */
|
|
||||||
padding-top: 70px;
|
padding-top: 70px;
|
||||||
padding-bottom: 120px;
|
padding-bottom: 120px;
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
padding-right: 40px;
|
padding-right: 40px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
|
perspective: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork-wrapper {
|
.coverflow-container {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.artwork-container {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(45vh, 360px);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
height: min(45vh, 360px);
|
height: min(45vh, 360px);
|
||||||
|
perspective: 1200px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item {
|
||||||
|
position: absolute;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item:not(:disabled):hover {
|
||||||
|
transform: translateX(var(--hover-x, 0)) rotateY(var(--hover-rotate, 0)) scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: min(38vh, 300px);
|
||||||
|
height: min(38vh, 300px);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
box-shadow:
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
0 8px 32px rgba(0, 0, 0, 0.5),
|
transition: box-shadow 300ms ease;
|
||||||
0 20px 60px rgba(0, 0, 0, 0.3);
|
|
||||||
transition: transform 300ms ease, box-shadow 300ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork-container:hover {
|
.coverflow-item:not(:disabled):hover .cover-wrapper {
|
||||||
transform: scale(1.02) translateY(-4px);
|
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
|
||||||
box-shadow:
|
|
||||||
0 12px 40px rgba(0, 0, 0, 0.5),
|
|
||||||
0 28px 80px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork {
|
.cover-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cover-reflection {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -60%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 60%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
transform: scaleY(-1);
|
||||||
|
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, transparent 50%);
|
||||||
|
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.3) 0%, transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Center item */
|
||||||
|
.coverflow-item.center {
|
||||||
|
z-index: 5;
|
||||||
|
transform: translateX(0) rotateY(0deg) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.center .cover-wrapper {
|
||||||
|
width: min(45vh, 360px);
|
||||||
|
height: min(45vh, 360px);
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.5),
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Left items */
|
||||||
|
.coverflow-item.left-1 {
|
||||||
|
z-index: 4;
|
||||||
|
transform: translateX(-180px) rotateY(45deg) scale(0.85);
|
||||||
|
--hover-x: -170px;
|
||||||
|
--hover-rotate: 40deg;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.left-2 {
|
||||||
|
z-index: 3;
|
||||||
|
transform: translateX(-300px) rotateY(55deg) scale(0.7);
|
||||||
|
--hover-x: -290px;
|
||||||
|
--hover-rotate: 50deg;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right items */
|
||||||
|
.coverflow-item.right-1 {
|
||||||
|
z-index: 4;
|
||||||
|
transform: translateX(180px) rotateY(-45deg) scale(0.85);
|
||||||
|
--hover-x: 170px;
|
||||||
|
--hover-rotate: -40deg;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.right-2 {
|
||||||
|
z-index: 3;
|
||||||
|
transform: translateX(300px) rotateY(-55deg) scale(0.7);
|
||||||
|
--hover-x: 290px;
|
||||||
|
--hover-rotate: -50deg;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation classes */
|
||||||
|
.coverflow-container.animate-left .coverflow-item {
|
||||||
|
animation: slideLeft 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-container.animate-right .coverflow-item {
|
||||||
|
animation: slideRight 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
0% { transform: translateX(calc(var(--translate-x, 0px) + 100px)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRight {
|
||||||
|
0% { transform: translateX(calc(var(--translate-x, 0px) - 100px)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Track info */
|
||||||
.track-info {
|
.track-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -174,20 +361,68 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.coverflow-item.left-1 {
|
||||||
|
transform: translateX(-140px) rotateY(45deg) scale(0.8);
|
||||||
|
--hover-x: -130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.left-2 {
|
||||||
|
transform: translateX(-230px) rotateY(55deg) scale(0.65);
|
||||||
|
--hover-x: -220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.right-1 {
|
||||||
|
transform: translateX(140px) rotateY(-45deg) scale(0.8);
|
||||||
|
--hover-x: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.right-2 {
|
||||||
|
transform: translateX(230px) rotateY(-55deg) scale(0.65);
|
||||||
|
--hover-x: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.coverflow-panel {
|
.coverflow-panel {
|
||||||
padding: 70px 24px 130px;
|
padding: 70px 16px 130px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artwork-container {
|
.cover-wrapper {
|
||||||
width: min(55vw, 280px);
|
width: min(30vh, 200px);
|
||||||
height: min(55vw, 280px);
|
height: min(30vh, 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.center .cover-wrapper {
|
||||||
|
width: min(38vh, 260px);
|
||||||
|
height: min(38vh, 260px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.left-1 {
|
||||||
|
transform: translateX(-100px) rotateY(45deg) scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.left-2 {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.right-1 {
|
||||||
|
transform: translateX(100px) rotateY(-45deg) scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.right-2 {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 600px) {
|
@media (max-height: 600px) {
|
||||||
.artwork-container {
|
.cover-wrapper {
|
||||||
|
width: min(28vh, 180px);
|
||||||
|
height: min(28vh, 180px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coverflow-item.center .cover-wrapper {
|
||||||
width: min(32vh, 220px);
|
width: min(32vh, 220px);
|
||||||
height: min(32vh, 220px);
|
height: min(32vh, 220px);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import QualityBadge from '$lib/components/QualityBadge.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
artwork: string;
|
||||||
|
trackTitle: string;
|
||||||
|
artist: string;
|
||||||
|
album?: string;
|
||||||
|
isPlaying?: boolean;
|
||||||
|
quality?: string;
|
||||||
|
bitDepth?: number;
|
||||||
|
samplingRate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
artwork,
|
||||||
|
trackTitle,
|
||||||
|
artist,
|
||||||
|
album,
|
||||||
|
isPlaying = false,
|
||||||
|
quality,
|
||||||
|
bitDepth,
|
||||||
|
samplingRate
|
||||||
|
}: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="static-panel">
|
||||||
|
<div class="artwork-wrapper">
|
||||||
|
<div class="artwork-container" class:playing={isPlaying}>
|
||||||
|
<img src={artwork} alt={trackTitle} class="artwork" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="track-info">
|
||||||
|
{#if isPlaying}
|
||||||
|
<div class="now-playing-indicator">
|
||||||
|
<div class="equalizer">
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<span class="bar"></span>
|
||||||
|
</div>
|
||||||
|
<span>Now Playing</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<h1 class="track-title">{trackTitle}</h1>
|
||||||
|
<p class="track-artist">{artist}</p>
|
||||||
|
{#if album}
|
||||||
|
<p class="track-album">{album}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="quality-badge-wrapper">
|
||||||
|
<QualityBadge {quality} {bitDepth} {samplingRate} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.static-panel {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
/* Offset for header (70px) and controls (120px) to achieve true visual center */
|
||||||
|
padding-top: 70px;
|
||||||
|
padding-bottom: 120px;
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 40px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-container {
|
||||||
|
position: relative;
|
||||||
|
width: min(45vh, 360px);
|
||||||
|
height: min(45vh, 360px);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 32px rgba(0, 0, 0, 0.5),
|
||||||
|
0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: transform 300ms ease, box-shadow 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-container:hover {
|
||||||
|
transform: scale(1.02) translateY(-4px);
|
||||||
|
box-shadow:
|
||||||
|
0 12px 40px rgba(0, 0, 0, 0.5),
|
||||||
|
0 28px 80px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 6px;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-playing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--accent-primary, #7c3aed);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equalizer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equalizer .bar {
|
||||||
|
width: 3px;
|
||||||
|
background: var(--accent-primary, #7c3aed);
|
||||||
|
border-radius: 1px;
|
||||||
|
animation: equalize 0.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equalizer .bar:nth-child(1) { animation-delay: 0s; height: 60%; }
|
||||||
|
.equalizer .bar:nth-child(2) { animation-delay: 0.2s; height: 100%; }
|
||||||
|
.equalizer .bar:nth-child(3) { animation-delay: 0.1s; height: 40%; }
|
||||||
|
.equalizer .bar:nth-child(4) { animation-delay: 0.3s; height: 80%; }
|
||||||
|
|
||||||
|
@keyframes equalize {
|
||||||
|
0%, 100% { transform: scaleY(0.3); }
|
||||||
|
50% { transform: scaleY(1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-title {
|
||||||
|
font-size: clamp(20px, 3vw, 28px);
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary, white);
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-artist {
|
||||||
|
font-size: clamp(14px, 2vw, 18px);
|
||||||
|
color: var(--alpha-70, rgba(255, 255, 255, 0.7));
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-album {
|
||||||
|
font-size: clamp(12px, 1.5vw, 14px);
|
||||||
|
color: var(--alpha-50, rgba(255, 255, 255, 0.5));
|
||||||
|
margin: 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-badge-wrapper {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.static-panel {
|
||||||
|
padding: 70px 24px 130px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork-container {
|
||||||
|
width: min(55vw, 280px);
|
||||||
|
height: min(55vw, 280px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.artwork-container {
|
||||||
|
width: min(32vh, 220px);
|
||||||
|
height: min(32vh, 220px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user