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:
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>
|
||||
Reference in new issue
Block a user