diff --git a/src/lib/components/FocusMode.svelte b/src/lib/components/FocusMode.svelte index 1d7deda8f..b8e6d8fe0 100644 --- a/src/lib/components/FocusMode.svelte +++ b/src/lib/components/FocusMode.svelte @@ -22,6 +22,8 @@ currentTime: number; duration: number; onSeek: (time: number) => void; + volume: number; + onVolumeChange: (volume: number) => void; onContextClick?: () => void; // Lyrics props lyricsLines?: LyricsLine[]; @@ -45,6 +47,8 @@ currentTime, duration, onSeek, + volume, + onVolumeChange, onContextClick, lyricsLines = [], lyricsActiveIndex = -1, @@ -67,6 +71,8 @@ let showControls = $state(true); let hideTimeout: ReturnType | null = null; let progressRef: HTMLDivElement | null = $state(null); + let volumeRef: HTMLDivElement | null = $state(null); + let isDraggingVolume = $state(false); const progress = $derived((currentTime / duration) * 100 || 0); const hasLyrics = $derived(lyricsLines.length > 0); @@ -96,6 +102,31 @@ } } + function handleVolumeMouseDown(e: MouseEvent) { + e.stopPropagation(); + isDraggingVolume = true; + updateVolume(e); + document.addEventListener('mousemove', handleVolumeMouseMove); + document.addEventListener('mouseup', handleVolumeMouseUp); + } + + function handleVolumeMouseMove(e: MouseEvent) { + if (isDraggingVolume) updateVolume(e); + } + + function handleVolumeMouseUp() { + isDraggingVolume = false; + document.removeEventListener('mousemove', handleVolumeMouseMove); + document.removeEventListener('mouseup', handleVolumeMouseUp); + } + + function updateVolume(e: MouseEvent) { + if (!volumeRef) return; + const rect = volumeRef.getBoundingClientRect(); + const percentage = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); + onVolumeChange(Math.round(percentage)); + } + function showControlsTemporarily() { showControls = true; if (hideTimeout) clearTimeout(hideTimeout); @@ -239,8 +270,26 @@ - -
+ +
+
+
{volume}
+
+
+
+
+
+
{/if} @@ -620,8 +669,66 @@ opacity: 1; } - .right-spacer { + .right-controls { min-width: 200px; + display: flex; + justify-content: flex-end; + } + + .volume-control { + position: relative; + display: flex; + align-items: center; + } + + .volume-bar { + width: 140px; + height: 6px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.15); + cursor: pointer; + position: relative; + } + + .volume-fill { + height: 100%; + border-radius: 999px; + background: rgba(255, 255, 255, 0.7); + } + + .volume-thumb { + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 12px; + height: 12px; + border-radius: 50%; + background: #fff; + opacity: 0; + transition: opacity 150ms ease; + } + + .volume-bar:hover .volume-thumb { + opacity: 1; + } + + .volume-value { + position: absolute; + right: 0; + top: -26px; + padding: 4px 8px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.5); + color: #fff; + font-size: 12px; + opacity: 0; + transform: translateY(4px); + transition: opacity 150ms ease, transform 150ms ease; + } + + .volume-value.visible { + opacity: 1; + transform: translateY(0); } /* Responsive Breakpoints */ @@ -667,7 +774,7 @@ display: none; } - .right-spacer { + .right-controls { display: none; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f5c2bd566..b23f5220b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2391,6 +2391,8 @@ {currentTime} {duration} onSeek={handleSeek} + {volume} + onVolumeChange={handleVolumeChange} lyricsLines={lyricsLines.map(l => ({ text: l.text }))} lyricsActiveIndex={lyricsActiveIndex} lyricsActiveProgress={lyricsActiveProgress} diff --git a/src/routes/miniplayer/+page.svelte b/src/routes/miniplayer/+page.svelte index 9b392fea4..bb8d7cf14 100644 --- a/src/routes/miniplayer/+page.svelte +++ b/src/routes/miniplayer/+page.svelte @@ -40,10 +40,12 @@ let repeatMode = $state('off'); let isDragging = $state(false); let isDraggingProgress = $state(false); + let isDraggingVolume = $state(false); let queueCount = $state(0); // Refs let progressRef: HTMLDivElement; + let volumeRef: HTMLDivElement | null = null; // Derived state const progress = $derived(playerState.duration > 0 ? (playerState.currentTime / playerState.duration) * 100 : 0); @@ -164,6 +166,31 @@ } } + function handleVolumeMouseDown(e: MouseEvent): void { + e.stopPropagation(); + isDraggingVolume = true; + updateVolume(e); + document.addEventListener('mousemove', handleVolumeMouseMove); + document.addEventListener('mouseup', handleVolumeMouseUp); + } + + function handleVolumeMouseMove(e: MouseEvent): void { + if (isDraggingVolume) updateVolume(e); + } + + function handleVolumeMouseUp(): void { + isDraggingVolume = false; + document.removeEventListener('mousemove', handleVolumeMouseMove); + document.removeEventListener('mouseup', handleVolumeMouseUp); + } + + function updateVolume(e: MouseEvent): void { + if (!volumeRef) return; + const rect = volumeRef.getBoundingClientRect(); + const percentage = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); + playerSetVolume(Math.round(percentage)); + } + // Window controls - stop propagation to prevent drag from capturing async function handleRestore(e: MouseEvent): Promise { e.stopPropagation(); @@ -304,6 +331,24 @@ +
+
{Math.round(playerState.volume)}
+
+
+
+
+
+