Initial commit - realms platform
This commit is contained in:
parent
c590ab6d18
commit
c717c3751c
234 changed files with 74103 additions and 15231 deletions
667
frontend/src/lib/components/AudioPlayer.svelte
Normal file
667
frontend/src/lib/components/AudioPlayer.svelte
Normal file
|
|
@ -0,0 +1,667 @@
|
|||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue