668 lines
21 KiB
Svelte
668 lines
21 KiB
Svelte
|
|
<script>
|
|||
|
|
import { onMount, onDestroy } from 'svelte';
|
|||
|
|
import { browser } from '$app/environment';
|
|||
|
|
import { audioPlaylist, currentTrack, hasNext, hasPrevious } from '$lib/stores/audioPlaylist';
|
|||
|
|
|
|||
|
|
let seekBar;
|
|||
|
|
let volumeBar;
|
|||
|
|
let isDraggingSeek = false;
|
|||
|
|
let isDraggingVolume = false;
|
|||
|
|
let seekPosition = 0;
|
|||
|
|
|
|||
|
|
// Format time in mm:ss
|
|||
|
|
function formatTime(seconds) {
|
|||
|
|
if (!seconds || isNaN(seconds)) return '0:00';
|
|||
|
|
const mins = Math.floor(seconds / 60);
|
|||
|
|
const secs = Math.floor(seconds % 60);
|
|||
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Seek bar handling
|
|||
|
|
function handleSeekStart(e) {
|
|||
|
|
isDraggingSeek = true;
|
|||
|
|
handleSeekMove(e);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleSeekMove(e) {
|
|||
|
|
if (!isDraggingSeek || !seekBar) return;
|
|||
|
|
const rect = seekBar.getBoundingClientRect();
|
|||
|
|
const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
|
|||
|
|
seekPosition = Math.max(0, Math.min(1, x / rect.width));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleSeekEnd() {
|
|||
|
|
if (isDraggingSeek) {
|
|||
|
|
const newTime = seekPosition * $audioPlaylist.duration;
|
|||
|
|
audioPlaylist.seek(newTime);
|
|||
|
|
}
|
|||
|
|
isDraggingSeek = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Volume bar handling
|
|||
|
|
function handleVolumeStart(e) {
|
|||
|
|
isDraggingVolume = true;
|
|||
|
|
handleVolumeMove(e);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleVolumeMove(e) {
|
|||
|
|
if (!isDraggingVolume || !volumeBar) return;
|
|||
|
|
const rect = volumeBar.getBoundingClientRect();
|
|||
|
|
const x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
|
|||
|
|
const volume = Math.max(0, Math.min(1, x / rect.width));
|
|||
|
|
audioPlaylist.setVolume(volume);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function handleVolumeEnd() {
|
|||
|
|
isDraggingVolume = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Keyboard shortcuts
|
|||
|
|
function handleKeydown(e) {
|
|||
|
|
if (!$audioPlaylist.enabled) return;
|
|||
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|||
|
|
|
|||
|
|
switch (e.code) {
|
|||
|
|
case 'Space':
|
|||
|
|
if (e.ctrlKey || e.metaKey) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
audioPlaylist.togglePlay();
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case 'ArrowRight':
|
|||
|
|
if (e.ctrlKey || e.metaKey) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
audioPlaylist.next();
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
case 'ArrowLeft':
|
|||
|
|
if (e.ctrlKey || e.metaKey) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
audioPlaylist.previous();
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMount(() => {
|
|||
|
|
if (browser) {
|
|||
|
|
window.addEventListener('mousemove', handleSeekMove);
|
|||
|
|
window.addEventListener('mouseup', handleSeekEnd);
|
|||
|
|
window.addEventListener('touchmove', handleSeekMove);
|
|||
|
|
window.addEventListener('touchend', handleSeekEnd);
|
|||
|
|
window.addEventListener('mousemove', handleVolumeMove);
|
|||
|
|
window.addEventListener('mouseup', handleVolumeEnd);
|
|||
|
|
window.addEventListener('touchmove', handleVolumeMove);
|
|||
|
|
window.addEventListener('touchend', handleVolumeEnd);
|
|||
|
|
window.addEventListener('keydown', handleKeydown);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onDestroy(() => {
|
|||
|
|
if (browser) {
|
|||
|
|
window.removeEventListener('mousemove', handleSeekMove);
|
|||
|
|
window.removeEventListener('mouseup', handleSeekEnd);
|
|||
|
|
window.removeEventListener('touchmove', handleSeekMove);
|
|||
|
|
window.removeEventListener('touchend', handleSeekEnd);
|
|||
|
|
window.removeEventListener('mousemove', handleVolumeMove);
|
|||
|
|
window.removeEventListener('mouseup', handleVolumeEnd);
|
|||
|
|
window.removeEventListener('touchmove', handleVolumeMove);
|
|||
|
|
window.removeEventListener('touchend', handleVolumeEnd);
|
|||
|
|
window.removeEventListener('keydown', handleKeydown);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Calculate progress percentage
|
|||
|
|
$: progress = $audioPlaylist.duration > 0
|
|||
|
|
? (isDraggingSeek ? seekPosition : $audioPlaylist.currentTime / $audioPlaylist.duration) * 100
|
|||
|
|
: 0;
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
{#if $audioPlaylist.enabled && ($audioPlaylist.queue.length > 0 || $audioPlaylist.nowPlaying)}
|
|||
|
|
|
|||
|
|
<div class="audio-player" class:minimized={$audioPlaylist.minimized}>
|
|||
|
|
{#if $audioPlaylist.minimized}
|
|||
|
|
<!-- Minimized view -->
|
|||
|
|
<div class="mini-player">
|
|||
|
|
<div class="mini-thumb" on:click={() => audioPlaylist.toggleMinimized()}>
|
|||
|
|
{#if $currentTrack?.thumbnailPath}
|
|||
|
|
<img src={$currentTrack.thumbnailPath} alt="" />
|
|||
|
|
{:else}
|
|||
|
|
<div class="mini-thumb-placeholder">
|
|||
|
|
<span>{$currentTrack?.title?.charAt(0) || '?'}</span>
|
|||
|
|
</div>
|
|||
|
|
{/if}
|
|||
|
|
<div class="mini-progress" style="width: {progress}%"></div>
|
|||
|
|
</div>
|
|||
|
|
<button class="mini-play" on:click={() => audioPlaylist.togglePlay()}>
|
|||
|
|
{$audioPlaylist.isPlaying ? '⏸' : '▶'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
{:else}
|
|||
|
|
<!-- Full player view -->
|
|||
|
|
<div class="player-header">
|
|||
|
|
<span class="queue-count">{$audioPlaylist.currentIndex + 1} / {$audioPlaylist.queue.length}</span>
|
|||
|
|
<div class="header-controls">
|
|||
|
|
<button class="header-btn" on:click={() => audioPlaylist.toggleMinimized()} title="Minimize">
|
|||
|
|
─
|
|||
|
|
</button>
|
|||
|
|
<button class="header-btn close" on:click={() => audioPlaylist.hide()} title="Close">
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="player-content">
|
|||
|
|
<!-- Track info -->
|
|||
|
|
<div class="track-info">
|
|||
|
|
<div class="track-thumb">
|
|||
|
|
{#if $currentTrack?.thumbnailPath}
|
|||
|
|
<img src={$currentTrack.thumbnailPath} alt="" />
|
|||
|
|
{:else}
|
|||
|
|
<div class="thumb-placeholder">
|
|||
|
|
<span>{$currentTrack?.title?.charAt(0) || '?'}</span>
|
|||
|
|
</div>
|
|||
|
|
{/if}
|
|||
|
|
</div>
|
|||
|
|
<div class="track-details">
|
|||
|
|
<div class="track-title" title={$currentTrack?.title || 'Unknown'}>
|
|||
|
|
{$currentTrack?.title || 'Unknown'}
|
|||
|
|
</div>
|
|||
|
|
<div class="track-artist">
|
|||
|
|
{$currentTrack?.username || 'Unknown Artist'}
|
|||
|
|
{#if $currentTrack?.realmName}
|
|||
|
|
<span class="track-realm">in {$currentTrack.realmName}</span>
|
|||
|
|
{/if}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Progress bar -->
|
|||
|
|
<div class="progress-container">
|
|||
|
|
<span class="time">{formatTime(isDraggingSeek ? seekPosition * $audioPlaylist.duration : $audioPlaylist.currentTime)}</span>
|
|||
|
|
<div
|
|||
|
|
class="progress-bar"
|
|||
|
|
bind:this={seekBar}
|
|||
|
|
on:mousedown={handleSeekStart}
|
|||
|
|
on:touchstart={handleSeekStart}
|
|||
|
|
>
|
|||
|
|
<div class="progress-fill" style="width: {progress}%"></div>
|
|||
|
|
<div class="progress-thumb" style="left: {progress}%"></div>
|
|||
|
|
</div>
|
|||
|
|
<span class="time">{formatTime($audioPlaylist.duration)}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Main controls -->
|
|||
|
|
<div class="main-controls">
|
|||
|
|
<button
|
|||
|
|
class="control-btn secondary"
|
|||
|
|
class:active={$audioPlaylist.shuffle}
|
|||
|
|
on:click={() => audioPlaylist.toggleShuffle()}
|
|||
|
|
title="Shuffle"
|
|||
|
|
>
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<polyline points="16 3 21 3 21 8"></polyline>
|
|||
|
|
<line x1="4" y1="20" x2="21" y2="3"></line>
|
|||
|
|
<polyline points="21 16 21 21 16 21"></polyline>
|
|||
|
|
<line x1="15" y1="15" x2="21" y2="21"></line>
|
|||
|
|
<line x1="4" y1="4" x2="9" y2="9"></line>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
class="control-btn"
|
|||
|
|
disabled={!$hasPrevious}
|
|||
|
|
on:click={() => audioPlaylist.previous()}
|
|||
|
|
title="Previous"
|
|||
|
|
>
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M6 6h2v12H6v-12zm3.5 6l8.5 6V6l-8.5 6z"/>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<button class="control-btn play" on:click={() => audioPlaylist.togglePlay()}>
|
|||
|
|
{#if $audioPlaylist.isPlaying}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|||
|
|
</svg>
|
|||
|
|
{:else}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M8 5v14l11-7z"/>
|
|||
|
|
</svg>
|
|||
|
|
{/if}
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
class="control-btn"
|
|||
|
|
disabled={!$hasNext}
|
|||
|
|
on:click={() => audioPlaylist.next()}
|
|||
|
|
title="Next"
|
|||
|
|
>
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M6 18l8.5-6L6 6v12zm10-12v12h2V6h-2z"/>
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
class="control-btn secondary"
|
|||
|
|
class:active={$audioPlaylist.repeat !== 'none'}
|
|||
|
|
on:click={() => audioPlaylist.cycleRepeat()}
|
|||
|
|
title={`Repeat: ${$audioPlaylist.repeat}`}
|
|||
|
|
>
|
|||
|
|
{#if $audioPlaylist.repeat === 'one'}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<polyline points="17 1 21 5 17 9"></polyline>
|
|||
|
|
<path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
|
|||
|
|
<polyline points="7 23 3 19 7 15"></polyline>
|
|||
|
|
<path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
|
|||
|
|
<text x="12" y="14" font-size="7" fill="currentColor" stroke="none" text-anchor="middle" dominant-baseline="middle">1</text>
|
|||
|
|
</svg>
|
|||
|
|
{:else}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<polyline points="17 1 21 5 17 9"></polyline>
|
|||
|
|
<path d="M3 11V9a4 4 0 0 1 4-4h14"></path>
|
|||
|
|
<polyline points="7 23 3 19 7 15"></polyline>
|
|||
|
|
<path d="M21 13v2a4 4 0 0 1-4 4H3"></path>
|
|||
|
|
</svg>
|
|||
|
|
{/if}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Volume control -->
|
|||
|
|
<div class="volume-container">
|
|||
|
|
<button
|
|||
|
|
class="control-btn secondary"
|
|||
|
|
on:click={() => audioPlaylist.toggleMute()}
|
|||
|
|
title={$audioPlaylist.muted ? 'Unmute' : 'Mute'}
|
|||
|
|
>
|
|||
|
|
{#if $audioPlaylist.muted || $audioPlaylist.volume === 0}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
|||
|
|
</svg>
|
|||
|
|
{:else if $audioPlaylist.volume < 0.5}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/>
|
|||
|
|
</svg>
|
|||
|
|
{:else}
|
|||
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|||
|
|
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
|||
|
|
</svg>
|
|||
|
|
{/if}
|
|||
|
|
</button>
|
|||
|
|
<div
|
|||
|
|
class="volume-bar"
|
|||
|
|
bind:this={volumeBar}
|
|||
|
|
on:mousedown={handleVolumeStart}
|
|||
|
|
on:touchstart={handleVolumeStart}
|
|||
|
|
>
|
|||
|
|
<div class="volume-fill" style="width: {$audioPlaylist.muted ? 0 : $audioPlaylist.volume * 100}%"></div>
|
|||
|
|
<div class="volume-thumb" style="left: {$audioPlaylist.muted ? 0 : $audioPlaylist.volume * 100}%"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
{/if}
|
|||
|
|
</div>
|
|||
|
|
{/if}
|
|||
|
|
|
|||
|
|
<style>
|
|||
|
|
.audio-player {
|
|||
|
|
position: fixed;
|
|||
|
|
bottom: 1rem;
|
|||
|
|
right: 1rem;
|
|||
|
|
width: 320px;
|
|||
|
|
background: #161b22;
|
|||
|
|
border: 1px solid #30363d;
|
|||
|
|
border-radius: 12px;
|
|||
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|||
|
|
z-index: 9998;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.audio-player.minimized {
|
|||
|
|
width: auto;
|
|||
|
|
border-radius: 50px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Mini player */
|
|||
|
|
.mini-player {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
padding: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-thumb {
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
overflow: hidden;
|
|||
|
|
cursor: pointer;
|
|||
|
|
position: relative;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-thumb img {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-thumb-placeholder {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background: linear-gradient(135deg, #561d5e, #8b3a92);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
color: white;
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-progress {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 0;
|
|||
|
|
left: 0;
|
|||
|
|
height: 3px;
|
|||
|
|
background: #8b3a92;
|
|||
|
|
border-radius: 0 0 0 50%;
|
|||
|
|
transition: width 0.1s linear;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-play {
|
|||
|
|
width: 36px;
|
|||
|
|
height: 36px;
|
|||
|
|
border: none;
|
|||
|
|
background: #8b3a92;
|
|||
|
|
color: white;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
transition: transform 0.15s ease, background 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.mini-play:hover {
|
|||
|
|
background: #a64daf;
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Full player */
|
|||
|
|
.player-header {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0.5rem 0.75rem;
|
|||
|
|
background: #0d1117;
|
|||
|
|
border-bottom: 1px solid #30363d;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.queue-count {
|
|||
|
|
color: #8b949e;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-controls {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-btn {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
border: none;
|
|||
|
|
background: none;
|
|||
|
|
color: #8b949e;
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
transition: all 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-btn:hover {
|
|||
|
|
background: rgba(255, 255, 255, 0.1);
|
|||
|
|
color: #c9d1d9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-btn.close:hover {
|
|||
|
|
color: #f85149;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.player-content {
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Track info */
|
|||
|
|
.track-info {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 0.75rem;
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.track-thumb {
|
|||
|
|
width: 48px;
|
|||
|
|
height: 48px;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.track-thumb img {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
object-fit: cover;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.thumb-placeholder {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
background: linear-gradient(135deg, #561d5e, #8b3a92);
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
color: white;
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 1.2rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.track-details {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.track-title {
|
|||
|
|
color: #c9d1d9;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
font-weight: 500;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.track-artist {
|
|||
|
|
color: #8b949e;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.track-realm {
|
|||
|
|
color: #6e7681;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Progress bar */
|
|||
|
|
.progress-container {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.time {
|
|||
|
|
color: #8b949e;
|
|||
|
|
font-size: 0.7rem;
|
|||
|
|
font-family: monospace;
|
|||
|
|
min-width: 32px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.time:last-child {
|
|||
|
|
text-align: right;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-bar {
|
|||
|
|
flex: 1;
|
|||
|
|
height: 4px;
|
|||
|
|
background: #30363d;
|
|||
|
|
border-radius: 2px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-fill {
|
|||
|
|
height: 100%;
|
|||
|
|
background: #8b3a92;
|
|||
|
|
border-radius: 2px;
|
|||
|
|
transition: width 0.1s linear;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-thumb {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
width: 12px;
|
|||
|
|
height: 12px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.progress-bar:hover .progress-thumb {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Main controls */
|
|||
|
|
.main-controls {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn {
|
|||
|
|
width: 32px;
|
|||
|
|
height: 32px;
|
|||
|
|
border: none;
|
|||
|
|
background: none;
|
|||
|
|
color: #c9d1d9;
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
transition: all 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn svg {
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn:hover:not(:disabled) {
|
|||
|
|
background: rgba(255, 255, 255, 0.1);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn:disabled {
|
|||
|
|
opacity: 0.3;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn.secondary {
|
|||
|
|
color: #8b949e;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn.secondary svg {
|
|||
|
|
width: 16px;
|
|||
|
|
height: 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn.secondary.active {
|
|||
|
|
color: #8b3a92;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn.play {
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
background: #8b3a92;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn.play:hover {
|
|||
|
|
background: #a64daf;
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.control-btn.play svg {
|
|||
|
|
width: 24px;
|
|||
|
|
height: 24px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* Volume control */
|
|||
|
|
.volume-container {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.volume-bar {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 100px;
|
|||
|
|
height: 4px;
|
|||
|
|
background: #30363d;
|
|||
|
|
border-radius: 2px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.volume-fill {
|
|||
|
|
height: 100%;
|
|||
|
|
background: #8b3a92;
|
|||
|
|
border-radius: 2px;
|
|||
|
|
transition: width 0.1s linear;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.volume-thumb {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 50%;
|
|||
|
|
transform: translate(-50%, -50%);
|
|||
|
|
width: 10px;
|
|||
|
|
height: 10px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.15s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.volume-bar:hover .volume-thumb {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 400px) {
|
|||
|
|
.audio-player {
|
|||
|
|
width: calc(100vw - 2rem);
|
|||
|
|
right: 1rem;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|