feat(player): add volume sliders to FocusMode and MiniPlayer
cx
This commit is contained in:
1 parent
318e3d795a
commit
395fd165d4
3 files changed
+216
-4
No files matched your search
@@ -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<typeof setTimeout> | 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 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Spacer for balance -->
|
||||
<div class="right-spacer"></div>
|
||||
<!-- Right: Volume Control -->
|
||||
<div class="right-controls">
|
||||
<div class="volume-control">
|
||||
<div class="volume-value" class:visible={isDraggingVolume}>{volume}</div>
|
||||
<div
|
||||
class="volume-bar"
|
||||
bind:this={volumeRef}
|
||||
onmousedown={handleVolumeMouseDown}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuenow={volume}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
title={$t('player.volume')}
|
||||
>
|
||||
<div class="volume-fill" style="width: {volume}%"></div>
|
||||
<div class="volume-thumb" style="left: {volume}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/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;
|
||||
}
|
||||
|
||||
|
||||
@@ -2391,6 +2391,8 @@
|
||||
{currentTime}
|
||||
{duration}
|
||||
onSeek={handleSeek}
|
||||
{volume}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
lyricsLines={lyricsLines.map(l => ({ text: l.text }))}
|
||||
lyricsActiveIndex={lyricsActiveIndex}
|
||||
lyricsActiveProgress={lyricsActiveProgress}
|
||||
|
||||
@@ -40,10 +40,12 @@
|
||||
let repeatMode = $state<RepeatMode>('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<void> {
|
||||
e.stopPropagation();
|
||||
@@ -304,6 +331,24 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="volume-control">
|
||||
<div class="volume-value" class:visible={isDraggingVolume}>{Math.round(playerState.volume)}</div>
|
||||
<div
|
||||
class="volume-bar"
|
||||
bind:this={volumeRef}
|
||||
onmousedown={handleVolumeMouseDown}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuenow={playerState.volume}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
title="Volume"
|
||||
>
|
||||
<div class="volume-fill" style="width: {playerState.volume}%"></div>
|
||||
<div class="volume-thumb" style="left: {playerState.volume}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="window-controls">
|
||||
<button class="ctrl-btn" onclick={handleOpenQueue} title="Queue ({queueCount})">
|
||||
<ListMusic size={14} />
|
||||
@@ -479,6 +524,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.media-controls {
|
||||
@@ -493,6 +539,63 @@
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.volume-bar {
|
||||
width: 90px;
|
||||
height: 4px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.volume-fill {
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.volume-thumb {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
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: -22px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
transition: opacity 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.volume-value.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user