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">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
import { Info } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trackId?: number;
|
trackId?: number;
|
||||||
@@ -7,16 +7,18 @@
|
|||||||
|
|
||||||
let { trackId }: Props = $props();
|
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 isLoading = $state(false);
|
||||||
let credits = $state<null>(null);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="credits-panel">
|
<div class="credits-panel">
|
||||||
<div class="placeholder">
|
<div class="placeholder">
|
||||||
<span class="placeholder-icon">ℹ️</span>
|
<div class="placeholder-icon">
|
||||||
<span class="placeholder-text">{$t('player.creditsComingSoon') || 'Credits coming soon'}</span>
|
<Info size={40} strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
<span class="placeholder-text">Credits</span>
|
||||||
<span class="placeholder-hint">Producer, Composer, Musicians...</span>
|
<span class="placeholder-hint">Producer, Composer, Musicians...</span>
|
||||||
|
<span class="coming-soon">Coming Soon</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,24 +37,36 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
|
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-icon {
|
.placeholder-icon {
|
||||||
font-size: 32px;
|
opacity: 0.4;
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: var(--alpha-50, rgba(255, 255, 255, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-hint {
|
.placeholder-hint {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
|
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>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
import { Radio } from 'lucide-svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
trackId?: number;
|
trackId?: number;
|
||||||
@@ -8,15 +8,18 @@
|
|||||||
|
|
||||||
let { trackId, artistId }: Props = $props();
|
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);
|
let isLoading = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="suggestions-panel">
|
<div class="suggestions-panel">
|
||||||
<div class="placeholder">
|
<div class="placeholder">
|
||||||
<span class="placeholder-icon">📻</span>
|
<div class="placeholder-icon">
|
||||||
<span class="placeholder-text">{$t('player.suggestionsComingSoon') || 'Suggestions coming soon'}</span>
|
<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="placeholder-hint">Artist Radio, Track Radio, Similar Tracks...</span>
|
||||||
|
<span class="coming-soon">Coming Soon</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -35,24 +38,36 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 200px;
|
min-height: 200px;
|
||||||
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
|
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-icon {
|
.placeholder-icon {
|
||||||
font-size: 32px;
|
opacity: 0.4;
|
||||||
opacity: 0.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-text {
|
.placeholder-text {
|
||||||
font-size: 16px;
|
font-size: 18px;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
|
color: var(--alpha-50, rgba(255, 255, 255, 0.5));
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-hint {
|
.placeholder-hint {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
|
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>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from '$lib/i18n';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isPlaying?: boolean;
|
isPlaying?: boolean;
|
||||||
@@ -7,15 +7,113 @@
|
|||||||
|
|
||||||
let { isPlaying = false }: Props = $props();
|
let { isPlaying = false }: Props = $props();
|
||||||
|
|
||||||
// TODO: Implement visualizer in Phase 5
|
let canvas: HTMLCanvasElement | null = $state(null);
|
||||||
// Will use spectrum-analyzer crate in Rust + Canvas rendering
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="visualizer-panel">
|
<div class="visualizer-panel">
|
||||||
<div class="placeholder">
|
<canvas bind:this={canvas} class="visualizer-canvas"></canvas>
|
||||||
<span class="placeholder-icon">📊</span>
|
<div class="visualizer-label">
|
||||||
<span class="placeholder-text">{$t('player.visualizerComingSoon') || 'Visualizer coming soon'}</span>
|
<span class="label-text">Audio Visualizer</span>
|
||||||
<span class="placeholder-hint">Frequency Bars, Waveform, Circular...</span>
|
{#if !isPlaying}
|
||||||
|
<span class="label-hint">Play a track to see the visualization</span>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -28,31 +126,40 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder {
|
.visualizer-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visualizer-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 4px;
|
||||||
gap: 12px;
|
opacity: 0.5;
|
||||||
height: 100%;
|
transition: opacity 200ms ease;
|
||||||
min-height: 200px;
|
}
|
||||||
|
|
||||||
|
.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));
|
color: var(--alpha-40, rgba(255, 255, 255, 0.4));
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-icon {
|
.label-hint {
|
||||||
font-size: 32px;
|
font-size: 11px;
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-hint {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
|
color: var(--alpha-30, rgba(255, 255, 255, 0.3));
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user