polish(immersive): improve panel styling and add canvas visualizer

- Replace emoji icons with Lucide icons in placeholder panels
- Add styled "Coming Soon" badges
- Create animated canvas visualizer (reacts to isPlaying state)
- Visualizer shows frequency bars with smooth animation
This commit is contained in:
vicrodh committed 2026-01-30 17:07:19 -06:00
1 parent 9ea9d66681
commit 4fea0dee17
3 files changed
+179 -43

No files matched your search

@@ -1,5 +1,5 @@
<script lang="ts">
import { t } from '$lib/i18n';
import { Info } from 'lucide-svelte';
interface Props {
trackId?: number;
@@ -7,16 +7,18 @@
let { trackId }: Props = $props();
// TODO: Implement credits fetching in Phase 3
// TODO: Implement credits fetching - needs albumId and MusicBrainz integration
let isLoading = $state(false);
let credits = $state<null>(null);
</script>
<div class="credits-panel">
<div class="placeholder">
<span class="placeholder-icon">ℹ️</span>
<span class="placeholder-text">{$t('player.creditsComingSoon') || 'Credits coming soon'}</span>
<div class="placeholder-icon">
<Info size={40} strokeWidth={1.5} />
</div>
<span class="placeholder-text">Credits</span>
<span class="placeholder-hint">Producer, Composer, Musicians...</span>
<span class="coming-soon">Coming Soon</span>
</div>
</div>
@@ -35,24 +37,36 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
gap: 16px;
height: 100%;
min-height: 200px;
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
}
.placeholder-icon {
font-size: 32px;
opacity: 0.5;
opacity: 0.4;
}
.placeholder-text {
font-size: 16px;
font-weight: 500;
font-size: 18px;
font-weight: 600;
color: var(--alpha-50, rgba(255, 255, 255, 0.5));
}
.placeholder-hint {
font-size: 13px;
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
}
.coming-soon {
margin-top: 8px;
padding: 6px 14px;
background: var(--alpha-10, rgba(255, 255, 255, 0.1));
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
}
</style>
@@ -1,5 +1,5 @@
<script lang="ts">
import { t } from '$lib/i18n';
import { Radio } from 'lucide-svelte';
interface Props {
trackId?: number;
@@ -8,15 +8,18 @@
let { trackId, artistId }: Props = $props();
// TODO: Implement suggestions fetching in Phase 4
// TODO: Implement suggestions - Artist Radio, Track Radio, Similar Tracks
let isLoading = $state(false);
</script>
<div class="suggestions-panel">
<div class="placeholder">
<span class="placeholder-icon">📻</span>
<span class="placeholder-text">{$t('player.suggestionsComingSoon') || 'Suggestions coming soon'}</span>
<div class="placeholder-icon">
<Radio size={40} strokeWidth={1.5} />
</div>
<span class="placeholder-text">Suggestions</span>
<span class="placeholder-hint">Artist Radio, Track Radio, Similar Tracks...</span>
<span class="coming-soon">Coming Soon</span>
</div>
</div>
@@ -35,24 +38,36 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
gap: 16px;
height: 100%;
min-height: 200px;
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
}
.placeholder-icon {
font-size: 32px;
opacity: 0.5;
opacity: 0.4;
}
.placeholder-text {
font-size: 16px;
font-weight: 500;
font-size: 18px;
font-weight: 600;
color: var(--alpha-50, rgba(255, 255, 255, 0.5));
}
.placeholder-hint {
font-size: 13px;
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
}
.coming-soon {
margin-top: 8px;
padding: 6px 14px;
background: var(--alpha-10, rgba(255, 255, 255, 0.1));
border-radius: 20px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
}
</style>
@@ -1,5 +1,5 @@
<script lang="ts">
import { t } from '$lib/i18n';
import { onMount } from 'svelte';
interface Props {
isPlaying?: boolean;
@@ -7,15 +7,113 @@
let { isPlaying = false }: Props = $props();
// TODO: Implement visualizer in Phase 5
// Will use spectrum-analyzer crate in Rust + Canvas rendering
let canvas: HTMLCanvasElement | null = $state(null);
let animationId: number | null = null;
let bars: number[] = [];
const BAR_COUNT = 64;
const BAR_WIDTH = 4;
const BAR_GAP = 2;
// Initialize bars with random heights
function initBars() {
bars = Array(BAR_COUNT).fill(0).map(() => Math.random() * 0.3 + 0.1);
}
// Animate bars
function animate() {
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const totalWidth = BAR_COUNT * (BAR_WIDTH + BAR_GAP) - BAR_GAP;
const startX = centerX - totalWidth / 2;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Update bar heights with smooth animation
bars = bars.map((bar, i) => {
if (isPlaying) {
// Random movement when playing
const target = Math.random() * 0.8 + 0.2;
return bar + (target - bar) * 0.15;
} else {
// Slowly decay to idle state
const idle = Math.sin(Date.now() / 1000 + i * 0.2) * 0.1 + 0.15;
return bar + (idle - bar) * 0.05;
}
});
// Draw bars
const gradient = ctx.createLinearGradient(0, height, 0, 0);
gradient.addColorStop(0, 'rgba(124, 58, 237, 0.8)'); // Purple
gradient.addColorStop(0.5, 'rgba(139, 92, 246, 0.6)');
gradient.addColorStop(1, 'rgba(167, 139, 250, 0.4)');
ctx.fillStyle = gradient;
bars.forEach((bar, i) => {
const x = startX + i * (BAR_WIDTH + BAR_GAP);
const barHeight = bar * height * 0.7;
const y = (height - barHeight) / 2;
// Rounded rectangle
const radius = BAR_WIDTH / 2;
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + BAR_WIDTH - radius, y);
ctx.quadraticCurveTo(x + BAR_WIDTH, y, x + BAR_WIDTH, y + radius);
ctx.lineTo(x + BAR_WIDTH, y + barHeight - radius);
ctx.quadraticCurveTo(x + BAR_WIDTH, y + barHeight, x + BAR_WIDTH - radius, y + barHeight);
ctx.lineTo(x + radius, y + barHeight);
ctx.quadraticCurveTo(x, y + barHeight, x, y + barHeight - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
ctx.fill();
});
animationId = requestAnimationFrame(animate);
}
// Handle canvas resize
function resizeCanvas() {
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
}
onMount(() => {
initBars();
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
if (animationId) {
cancelAnimationFrame(animationId);
}
};
});
</script>
<div class="visualizer-panel">
<div class="placeholder">
<span class="placeholder-icon">📊</span>
<span class="placeholder-text">{$t('player.visualizerComingSoon') || 'Visualizer coming soon'}</span>
<span class="placeholder-hint">Frequency Bars, Waveform, Circular...</span>
<canvas bind:this={canvas} class="visualizer-canvas"></canvas>
<div class="visualizer-label">
<span class="label-text">Audio Visualizer</span>
{#if !isPlaying}
<span class="label-hint">Play a track to see the visualization</span>
{/if}
</div>
</div>
@@ -28,31 +126,40 @@
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.placeholder {
.visualizer-canvas {
width: 100%;
height: 200px;
max-width: 500px;
}
.visualizer-label {
position: absolute;
bottom: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
height: 100%;
min-height: 200px;
gap: 4px;
opacity: 0.5;
transition: opacity 200ms ease;
}
.visualizer-panel:hover .visualizer-label {
opacity: 0.3;
}
.label-text {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
}
.placeholder-icon {
font-size: 32px;
opacity: 0.5;
}
.placeholder-text {
font-size: 16px;
font-weight: 500;
}
.placeholder-hint {
font-size: 13px;
.label-hint {
font-size: 11px;
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
}
</style>