Files
dupe/client/src/routes/Music.svelte
wisecolt 69e8fb47ed feat(music): implement music player with playback controls and dual view
Müzik çalar özellikleri eklenmiş ve görüntüleme modları geliştirilmiştir:

- Tam kapsamlı müzik çalar implementasyonu (play, pause, next, previous)
- İlerleme çubuğu ve süre göstergesi
- Ses kontrolü ve sessiz alma özelliği
- Liste ve ızgara (grid) görünüm modları
- Oynatma göstergeleri ve animasyonlar
- Medya süresi bilgisi için sunucu desteği
2025-12-24 20:47:02 +03:00

1303 lines
28 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onMount } from "svelte";
import { API, apiFetch, withToken } from "../utils/api.js";
import { musicCount } from "../stores/musicStore.js";
import { cleanFileName } from "../utils/filename.js";
let items = [];
let loading = true;
let error = null;
// Player state
let currentTrack = null;
let isPlaying = false;
let currentTime = 0;
let duration = 0;
let volume = 0.8;
let isMuted = false;
let previousVolume = 0.8;
let audioEl;
let progressInterval;
// View mode
let viewMode = "list"; // "list" or "grid"
async function loadMusic() {
loading = true;
error = null;
try {
const resp = await apiFetch("/api/music");
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data?.error || `HTTP ${resp.status}`);
}
const list = await resp.json();
items = Array.isArray(list) ? list : [];
musicCount.set(items.length);
} catch (err) {
console.error("Music load error:", err);
items = [];
musicCount.set(0);
error = err?.message || "Music listesi yüklenemedi.";
} finally {
loading = false;
}
}
function streamURL(item) {
const base = `${API}/stream/${item.infoHash}?index=${item.fileIndex || 0}`;
return withToken(base);
}
function thumbnailURL(item) {
if (!item.thumbnail) return null;
const token = localStorage.getItem("token");
const separator = item.thumbnail.includes("?") ? "&" : "?";
return `${API}${item.thumbnail}${separator}token=${token}`;
}
function formatDuration(seconds) {
if (!Number.isFinite(seconds) || seconds <= 0) return "0:00";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${String(secs).padStart(2, "0")}`;
}
function formatTime(seconds) {
if (!Number.isFinite(seconds) || seconds <= 0) return "0:00";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${String(secs).padStart(2, "0")}`;
}
function sourceLabel(item) {
if (item.tracker === "youtube" || item.thumbnail) return "YouTube";
return "Music";
}
// Player functions
function playTrack(item, index) {
if (currentTrack?.id === item.id) {
togglePlay();
return;
}
currentTrack = { ...item, index };
currentTime = 0;
duration = item.mediaInfo?.format?.duration || 0;
isPlaying = true;
if (audioEl) {
audioEl.src = streamURL(item);
audioEl.play().catch((err) => {
console.error("Play error:", err);
isPlaying = false;
});
}
}
function togglePlay() {
if (!audioEl) return;
if (isPlaying) {
audioEl.pause();
} else {
audioEl.play().catch((err) => {
console.error("Play error:", err);
});
}
isPlaying = !isPlaying;
}
function playNext() {
if (!currentTrack || items.length === 0) return;
const currentIndex = items.findIndex((i) => i.id === currentTrack.id);
const nextIndex = (currentIndex + 1) % items.length;
playTrack(items[nextIndex], nextIndex);
}
function playPrevious() {
if (!currentTrack || items.length === 0) return;
const currentIndex = items.findIndex((i) => i.id === currentTrack.id);
const prevIndex = (currentIndex - 1 + items.length) % items.length;
playTrack(items[prevIndex], prevIndex);
}
function seek(e) {
if (!audioEl || !duration) return;
const percent = parseFloat(e.target.value);
currentTime = (percent / 100) * duration;
audioEl.currentTime = currentTime;
}
function setVolume(e) {
volume = parseFloat(e.target.value);
if (audioEl) {
audioEl.volume = volume;
}
isMuted = volume === 0;
}
function toggleMute() {
if (isMuted) {
volume = previousVolume;
isMuted = false;
} else {
previousVolume = volume;
volume = 0;
isMuted = true;
}
if (audioEl) {
audioEl.volume = volume;
}
}
function handleTimeUpdate() {
if (audioEl) {
currentTime = audioEl.currentTime;
duration = audioEl.duration || 0;
}
}
function handleLoadedMetadata() {
if (audioEl) {
duration = audioEl.duration || 0;
}
}
function handleEnded() {
playNext();
}
function handlePlay() {
isPlaying = true;
}
function handlePause() {
isPlaying = false;
}
function startProgressInterval() {
if (progressInterval) clearInterval(progressInterval);
progressInterval = setInterval(() => {
if (isPlaying && audioEl) {
currentTime = audioEl.currentTime;
}
}, 100);
}
onMount(() => {
loadMusic();
startProgressInterval();
return () => {
if (progressInterval) clearInterval(progressInterval);
if (audioEl) {
audioEl.pause();
audioEl.src = "";
}
};
});
</script>
<section class="music-page">
<!-- Header -->
<div class="music-header">
<div class="header-left">
<h2>Music</h2>
{#if !loading && !error}
<span class="song-count">{items.length} Songs</span>
{/if}
</div>
<div class="header-right">
<div class="view-toggle">
<button
class="view-btn {viewMode === 'list' ? 'active' : ''}"
on:click={() => viewMode = 'list'}
title="Liste görünümü"
>
<i class="fa-solid fa-list"></i>
</button>
<button
class="view-btn {viewMode === 'grid' ? 'active' : ''}"
on:click={() => viewMode = 'grid'}
title="Grid görünümü"
>
<i class="fa-solid fa-border-all"></i>
</button>
</div>
<button class="refresh-btn" on:click={loadMusic} disabled={loading}>
<i class="fa-solid fa-rotate"></i>
</button>
</div>
</div>
<!-- Content -->
<div class="music-content">
{#if loading}
<div class="music-empty">
<div class="loading-spinner"></div>
<p>Yükleniyor…</p>
</div>
{:else if error}
<div class="music-empty error">
<i class="fa-solid fa-triangle-exclamation"></i>
<p>{error}</p>
</div>
{:else if !items.length}
<div class="music-empty">
<i class="fa-solid fa-music"></i>
<p>Henüz müzik dosyası yok.</p>
</div>
{:else if viewMode === 'list'}
<!-- List View -->
<div class="music-list">
<div class="list-header">
<div class="header-index">#</div>
<div class="header-thumb"></div>
<div class="header-title">Başlık</div>
<div class="header-album">Kaynak</div>
<div class="header-time"><i class="fa-regular fa-clock"></i></div>
<div class="header-actions"></div>
</div>
{#each items as item, idx (item.id)}
<div
class="music-row {currentTrack?.id === item.id ? 'playing' : ''}"
on:click={() => playTrack(item, idx)}
on:dblclick={() => {
if (currentTrack?.id === item.id) togglePlay();
}}
>
<div class="row-index">
{#if currentTrack?.id === item.id && isPlaying}
<div class="playing-indicator">
<span class="bar bar-1"></span>
<span class="bar bar-2"></span>
<span class="bar bar-3"></span>
</div>
{:else}
<span>{String(idx + 1).padStart(2, "0")}</span>
{/if}
</div>
<div class="row-thumb">
{#if thumbnailURL(item)}
<img src={thumbnailURL(item)} alt={item.title} />
{:else}
<div class="thumb-placeholder">
<i class="fa-solid fa-music"></i>
</div>
{/if}
</div>
<div class="row-info">
<div class="row-title">{cleanFileName(item.title)}</div>
<div class="row-source">{sourceLabel(item)}</div>
</div>
<div class="row-time">
{formatDuration(item.mediaInfo?.format?.duration || item.duration)}
</div>
<div class="row-actions" on:click|stopPropagation>
<button
class="action-btn play-btn-small"
title="Oynat"
on:click={() => playTrack(item, idx)}
>
<i class="fa-solid fa-play"></i>
</button>
<a
class="action-btn folder-btn"
href={`/files?path=${encodeURIComponent(item.folder)}`}
title="Klasöre git"
>
<i class="fa-solid fa-folder-open"></i>
</a>
</div>
</div>
{/each}
</div>
{:else}
<!-- Grid View -->
<div class="music-grid">
{#each items as item, idx (item.id)}
<div
class="music-card {currentTrack?.id === item.id ? 'playing' : ''}"
on:click={() => playTrack(item, idx)}
>
<div class="card-thumb">
{#if thumbnailURL(item)}
<img src={thumbnailURL(item)} alt={item.title} />
{:else}
<div class="thumb-placeholder">
<i class="fa-solid fa-music"></i>
</div>
{/if}
<div class="card-overlay">
<button class="card-play-btn">
<i class="fa-solid fa-play"></i>
</button>
</div>
{#if currentTrack?.id === item.id && isPlaying}
<div class="playing-badge">
<i class="fa-solid fa-volume-high"></i>
</div>
{/if}
</div>
<div class="card-info">
<div class="card-title">{cleanFileName(item.title)}</div>
<div class="card-source">{sourceLabel(item)}</div>
<div class="card-duration">
{formatDuration(item.mediaInfo?.format?.duration || item.duration)}
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
<!-- Bottom Player Bar -->
{#if currentTrack}
<div class="player-bar">
<audio
bind:this={audioEl}
on:timeupdate={handleTimeUpdate}
on:loadedmetadata={handleLoadedMetadata}
on:ended={handleEnded}
on:play={handlePlay}
on:pause={handlePause}
></audio>
<div class="player-content">
<!-- Track Info -->
<div class="player-track">
<div class="track-thumb">
{#if thumbnailURL(currentTrack)}
<img src={thumbnailURL(currentTrack)} alt={currentTrack.title} />
{:else}
<div class="thumb-placeholder">
<i class="fa-solid fa-music"></i>
</div>
{/if}
</div>
<div class="track-info">
<div class="track-name">{cleanFileName(currentTrack.title)}</div>
<div class="track-source">{sourceLabel(currentTrack)}</div>
</div>
<button class="like-btn" title="Beğen">
<i class="fa-regular fa-heart"></i>
</button>
</div>
<!-- Player Controls -->
<div class="player-controls">
<button class="control-btn" on:click={playPrevious} title="Önceki">
<i class="fa-solid fa-backward-step"></i>
</button>
<button
class="control-btn play-pause-btn"
on:click={togglePlay}
title={isPlaying ? "Durdaklat" : "Oynat"}
>
{#if isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
{/if}
</button>
<button class="control-btn" on:click={playNext} title="Sonraki">
<i class="fa-solid fa-forward-step"></i>
</button>
</div>
<!-- Progress -->
<div class="player-progress">
<span class="time-current">{formatTime(currentTime)}</span>
<div class="progress-container">
<input
type="range"
min="0"
max="100"
value={(currentTime / duration) * 100 || 0}
on:input={seek}
class="progress-slider"
/>
</div>
<span class="time-total">{formatTime(duration)}</span>
</div>
<!-- Volume & Extra -->
<div class="player-extra">
<div class="volume-wrapper">
<button class="control-btn volume-icon" on:click={toggleMute} title="Ses">
{#if isMuted}
<i class="fa-solid fa-volume-xmark"></i>
{:else if volume < 0.3}
<i class="fa-solid fa-volume-off"></i>
{:else if volume < 0.7}
<i class="fa-solid fa-volume-low"></i>
{:else}
<i class="fa-solid fa-volume-high"></i>
{/if}
</button>
<div class="volume-slider-container">
<input
type="range"
min="0"
max="1"
step="0.01"
bind:value={volume}
on:input={setVolume}
class="volume-slider"
style="--fill: {volume * 100}%"
/>
</div>
</div>
<a
class="control-btn download-btn"
href={streamURL(currentTrack)}
download={currentTrack.title}
title="İndir"
>
<i class="fa-solid fa-download"></i>
</a>
</div>
</div>
</div>
{/if}
</section>
<style>
/* === Project Color Scheme === */
.music-page {
min-height: calc(100vh - 140px);
background: #fff;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Header */
.music-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.header-left h2 {
margin: 0;
font-size: 1.75rem;
font-weight: 700;
color: #222;
}
.song-count {
color: #666;
font-weight: 500;
font-size: 0.9rem;
background: #f4f4f4;
padding: 0.25rem 0.75rem;
border-radius: 20px;
border: 1px solid #e5e5e5;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.view-toggle {
display: flex;
gap: 0.25rem;
background: #f8f8f8;
border-radius: 8px;
padding: 0.25rem;
border: 1px solid #e5e5e5;
}
.view-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: #666;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.view-btn:hover {
background: #e5e5e5;
color: #222;
}
.view-btn.active {
background: var(--yellow);
color: #222;
}
.refresh-btn {
width: 36px;
height: 36px;
border: none;
background: #f8f8f8;
color: #666;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
border: 1px solid #e5e5e5;
}
.refresh-btn:hover {
background: var(--yellow-dark);
color: #222;
transform: rotate(180deg);
}
.refresh-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Content */
.music-content {
flex: 1;
overflow-y: auto;
}
/* Empty State */
.music-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: #666;
text-align: center;
}
.music-empty i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.music-empty p {
font-size: 1rem;
margin: 0;
}
.music-empty.error {
color: var(--yellow-dark);
}
.music-empty.error i {
color: var(--yellow-dark);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #e5e5e5;
border-top-color: var(--yellow);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* === List View === */
.music-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.list-header {
display: grid;
grid-template-columns: 50px 60px 1fr 120px 80px 100px;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-bottom: 2px solid var(--border);
color: #666;
font-size: 0.85rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
background: #f8f8f8;
}
.header-time {
text-align: right;
}
.music-row {
display: grid;
grid-template-columns: 50px 60px 1fr 120px 80px 100px;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #222;
border: 1px solid transparent;
}
.music-row:hover {
background: #f8f8f8;
border-color: #e5e5e5;
}
.music-row.playing {
background: #fff8e1;
border-color: var(--yellow);
}
.row-index {
font-weight: 600;
font-size: 0.9rem;
}
.playing-indicator {
display: flex;
align-items: center;
gap: 2px;
height: 16px;
}
.playing-indicator .bar {
width: 3px;
background: var(--yellow);
border-radius: 2px;
animation: equalizer 0.8s ease-in-out infinite;
}
.playing-indicator .bar-1 { animation-delay: 0s; height: 8px; }
.playing-indicator .bar-2 { animation-delay: 0.2s; height: 12px; }
.playing-indicator .bar-3 { animation-delay: 0.4s; height: 6px; }
@keyframes equalizer {
0%, 100% { transform: scaleY(0.5); }
50% { transform: scaleY(1); }
}
.row-thumb {
width: 56px;
height: 56px;
border-radius: 6px;
overflow: hidden;
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
display: flex;
align-items: center;
justify-content: center;
}
.row-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.thumb-placeholder {
color: #999;
font-size: 1.5rem;
}
.row-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.row-title {
font-weight: 600;
color: inherit;
font-size: 0.95rem;
}
.row-source {
font-size: 0.85rem;
color: #999;
}
.row-time {
text-align: right;
font-weight: 500;
font-size: 0.9rem;
color: #666;
}
.row-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
opacity: 0;
transition: opacity 0.2s ease;
}
.music-row:hover .row-actions {
opacity: 1;
}
.action-btn {
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: #f4f4f4;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
text-decoration: none;
}
.action-btn:hover {
background: var(--yellow);
color: #222;
}
.folder-btn {
color: #666;
}
.folder-btn:hover {
background: #e5e5e5;
color: #222;
}
/* === Grid View === */
.music-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1.5rem;
}
.music-card {
background: #fff;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
border: 1px solid #e5e5e5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.music-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.music-card.playing {
border-color: var(--yellow);
box-shadow: 0 0 0 3px rgba(245, 179, 51, 0.3);
}
.card-thumb {
position: relative;
aspect-ratio: 1;
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
overflow: hidden;
}
.card-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.music-card:hover .card-overlay {
opacity: 1;
}
.card-play-btn {
width: 56px;
height: 56px;
border: none;
border-radius: 50%;
background: var(--yellow);
color: #222;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
transition: all 0.2s ease;
}
.card-play-btn:hover {
transform: scale(1.1);
background: var(--yellow-dark);
}
.playing-badge {
position: absolute;
top: 0.75rem;
right: 0.75rem;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--yellow);
color: #222;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.9rem;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 179, 51, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(245, 179, 51, 0); }
}
.card-info {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.card-title {
font-weight: 600;
color: #222;
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-source {
font-size: 0.8rem;
color: #999;
}
.card-duration {
font-size: 0.8rem;
color: #666;
}
/* === Player Bar === */
.player-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 90px;
background: #fff;
border-top: 2px solid var(--border);
padding: 0 1.5rem;
z-index: 1000;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
}
.player-content {
display: grid;
grid-template-columns: 280px 1fr 300px 200px;
align-items: center;
gap: 1.5rem;
height: 100%;
max-width: 1600px;
margin: 0 auto;
}
/* Track Info */
.player-track {
display: flex;
align-items: center;
gap: 1rem;
}
.track-thumb {
width: 56px;
height: 56px;
border-radius: 6px;
overflow: hidden;
background: linear-gradient(135deg, #f0f0f0, #e0e0e0);
flex-shrink: 0;
border: 1px solid #e5e5e5;
}
.track-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.track-info {
flex: 1;
min-width: 0;
}
.track-name {
font-weight: 600;
color: #222;
font-size: 0.9rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.track-source {
font-size: 0.8rem;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.like-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: #999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
flex-shrink: 0;
}
.like-btn:hover {
color: var(--yellow-dark);
}
/* Player Controls */
.player-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.control-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: #666;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all 0.2s ease;
text-decoration: none;
}
.control-btn:hover {
color: #222;
background: #f4f4f4;
}
.play-pause-btn {
width: 40px;
height: 40px;
background: var(--yellow);
color: #222;
font-size: 1rem;
}
.play-pause-btn:hover {
transform: scale(1.05);
background: var(--yellow-dark);
}
/* Progress */
.player-progress {
display: flex;
align-items: center;
gap: 0.75rem;
}
.time-current,
.time-total {
font-size: 0.75rem;
color: #666;
font-weight: 500;
min-width: 40px;
}
.progress-container {
flex: 1;
height: 4px;
position: relative;
}
.progress-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
background: #e5e5e5;
border-radius: 2px;
cursor: pointer;
}
.progress-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--yellow);
cursor: pointer;
transition: all 0.2s ease;
}
.progress-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.progress-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--yellow);
cursor: pointer;
border: none;
transition: all 0.2s ease;
}
.progress-slider::-moz-range-thumb:hover {
transform: scale(1.2);
}
/* Extra */
.player-extra {
display: flex;
align-items: center;
gap: 0.75rem;
}
.volume-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.volume-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.volume-slider-container {
flex: 1;
position: relative;
height: 4px;
display: flex;
align-items: center;
}
.volume-slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
border-radius: 2px;
cursor: pointer;
background: linear-gradient(to right, var(--yellow) var(--fill, 100%), #e5e5e5 var(--fill, 100%));
position: relative;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--yellow);
cursor: pointer;
position: relative;
z-index: 2;
margin-top: 0;
opacity: 1;
transition: all 0.2s ease;
}
.volume-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
border-color: var(--yellow-dark);
}
.volume-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--yellow);
cursor: pointer;
position: relative;
z-index: 2;
margin-top: 0;
opacity: 1;
transition: all 0.2s ease;
}
.volume-slider::-moz-range-thumb:hover {
transform: scale(1.1);
border-color: var(--yellow-dark);
}
.download-btn:hover {
color: var(--yellow-dark) !important;
background: #f4f4f4 !important;
}
/* Responsive */
@media (max-width: 1024px) {
.music-list,
.music-row,
.list-header {
grid-template-columns: 40px 50px 1fr 80px 70px;
}
.row-actions {
opacity: 1;
}
.player-content {
grid-template-columns: 1fr auto;
gap: 1rem;
}
.player-extra {
display: none;
}
}
@media (max-width: 768px) {
.music-page {
padding: 1rem;
padding-bottom: 100px;
}
.music-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.header-right {
width: 100%;
justify-content: space-between;
}
.music-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 1rem;
}
.music-list,
.music-row,
.list-header {
grid-template-columns: 40px 45px 1fr 60px;
}
.header-time,
.row-time {
display: none;
}
.player-bar {
height: 120px;
padding: 0.75rem 1rem;
}
.player-content {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
gap: 0.75rem;
}
.player-track {
justify-content: center;
}
.player-progress {
order: 3;
}
.player-controls {
order: 2;
}
.volume-wrapper {
width: 100%;
}
.volume-slider-container {
max-width: 200px;
margin: 0 auto;
}
}
@media (max-width: 480px) {
.music-grid {
grid-template-columns: repeat(2, 1fr);
}
.card-info {
padding: 0.75rem;
}
.card-title {
font-size: 0.85rem;
}
}
/* Scrollbar Styling */
.music-content::-webkit-scrollbar {
width: 8px;
}
.music-content::-webkit-scrollbar-track {
background: transparent;
}
.music-content::-webkit-scrollbar-thumb {
background: #e5e5e5;
border-radius: 4px;
}
.music-content::-webkit-scrollbar-thumb:hover {
background: #ccc;
}
</style>