feat(music): mini player ekle
Müzik çalar durumunu yönetmek için global store oluştur. Özel bir mini player bileşeni ile çalma listesi ve kontrolleri ekle. Müzik çaların uygulama genelinde kalıcı olmasını sağla.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import Sidebar from "./components/Sidebar.svelte";
|
import Sidebar from "./components/Sidebar.svelte";
|
||||||
import Topbar from "./components/Topbar.svelte";
|
import Topbar from "./components/Topbar.svelte";
|
||||||
|
import MiniPlayer from "./components/MiniPlayer.svelte";
|
||||||
import Files from "./routes/Files.svelte";
|
import Files from "./routes/Files.svelte";
|
||||||
import Transfers from "./routes/Transfers.svelte";
|
import Transfers from "./routes/Transfers.svelte";
|
||||||
import Trash from "./routes/Trash.svelte";
|
import Trash from "./routes/Trash.svelte";
|
||||||
@@ -156,6 +157,8 @@
|
|||||||
<Route path="/trash" component={Trash} />
|
<Route path="/trash" component={Trash} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MiniPlayer />
|
||||||
|
|
||||||
<!-- Sidebar dışına tıklayınca kapanma -->
|
<!-- Sidebar dışına tıklayınca kapanma -->
|
||||||
{#if menuOpen}
|
{#if menuOpen}
|
||||||
<div
|
<div
|
||||||
|
|||||||
246
client/src/components/MiniPlayer.svelte
Normal file
246
client/src/components/MiniPlayer.svelte
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<script>
|
||||||
|
import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js";
|
||||||
|
import { API } from "../utils/api.js";
|
||||||
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
|
||||||
|
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 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 remainingTime(current, total) {
|
||||||
|
if (!Number.isFinite(total) || total <= 0) return "-0:00";
|
||||||
|
const remaining = Math.max(total - (current || 0), 0);
|
||||||
|
return `-${formatTime(remaining)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(item) {
|
||||||
|
if (item?.tracker === "youtube" || item?.thumbnail) return "YouTube";
|
||||||
|
return "Music";
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $musicPlayer.currentTrack}
|
||||||
|
<div class="mini-player" aria-label="Mini music player">
|
||||||
|
<div class="mini-top">
|
||||||
|
<div class="mini-user-meta">
|
||||||
|
<div class="mini-user-name">
|
||||||
|
{cleanFileName($musicPlayer.currentTrack.title)}
|
||||||
|
</div>
|
||||||
|
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-actions">
|
||||||
|
<button class="mini-icon-btn" title="Paylaş">
|
||||||
|
<i class="fa-solid fa-arrow-up-from-bracket"></i>
|
||||||
|
</button>
|
||||||
|
<button class="mini-icon-btn" title="Beğen">
|
||||||
|
<i class="fa-regular fa-heart"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-cover">
|
||||||
|
{#if thumbnailURL($musicPlayer.currentTrack)}
|
||||||
|
<img
|
||||||
|
src={thumbnailURL($musicPlayer.currentTrack)}
|
||||||
|
alt={$musicPlayer.currentTrack.title}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="mini-cover-placeholder">
|
||||||
|
<i class="fa-solid fa-music"></i>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-progress">
|
||||||
|
<span class="mini-time">{formatTime($musicPlayer.currentTime)}</span>
|
||||||
|
<div class="mini-bar">
|
||||||
|
<div
|
||||||
|
class="mini-bar-fill"
|
||||||
|
style="width: {($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="mini-time">
|
||||||
|
{remainingTime($musicPlayer.currentTime, $musicPlayer.duration)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-controls">
|
||||||
|
<button class="mini-btn" on:click={playPrevious} title="Önceki">
|
||||||
|
<i class="fa-solid fa-backward-step"></i>
|
||||||
|
</button>
|
||||||
|
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur">
|
||||||
|
{#if $musicPlayer.isPlaying}
|
||||||
|
<i class="fa-solid fa-pause"></i>
|
||||||
|
{:else}
|
||||||
|
<i class="fa-solid fa-play"></i>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button class="mini-btn" on:click={playNext} title="Sonraki">
|
||||||
|
<i class="fa-solid fa-forward-step"></i>
|
||||||
|
</button>
|
||||||
|
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mini-player {
|
||||||
|
position: fixed;
|
||||||
|
right: 24px;
|
||||||
|
bottom: 24px;
|
||||||
|
width: 240px;
|
||||||
|
padding: 16px;
|
||||||
|
background: rgba(12, 12, 12, 0.72);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 26px;
|
||||||
|
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35);
|
||||||
|
color: #f6f6f6;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-user-meta {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-user-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-user-handle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-icon-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: #f6f6f6;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-cover {
|
||||||
|
margin: 14px auto 12px;
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-cover-placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.45);
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-progress {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-bar {
|
||||||
|
height: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--yellow);
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-btn {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: none;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
color: #f6f6f6;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-btn.main {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--yellow);
|
||||||
|
color: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-btn.ghost {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.mini-player {
|
||||||
|
right: 16px;
|
||||||
|
bottom: 16px;
|
||||||
|
width: 220px;
|
||||||
|
}
|
||||||
|
.mini-cover {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,24 +1,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount, tick } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { API, apiFetch, withToken } from "../utils/api.js";
|
import { API, apiFetch, withToken } from "../utils/api.js";
|
||||||
import { musicCount } from "../stores/musicStore.js";
|
import { musicCount } from "../stores/musicStore.js";
|
||||||
import { cleanFileName } from "../utils/filename.js";
|
import { cleanFileName } from "../utils/filename.js";
|
||||||
|
import {
|
||||||
|
musicPlayer,
|
||||||
|
setQueue,
|
||||||
|
playTrack as playFromStore,
|
||||||
|
togglePlay,
|
||||||
|
playNext,
|
||||||
|
playPrevious,
|
||||||
|
seekToPercent,
|
||||||
|
setVolume,
|
||||||
|
toggleMute,
|
||||||
|
stopPlayback
|
||||||
|
} from "../stores/musicPlayerStore.js";
|
||||||
|
|
||||||
let items = [];
|
let items = [];
|
||||||
let loading = true;
|
let loading = true;
|
||||||
let error = null;
|
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
|
// View mode
|
||||||
const VIEW_MODE_STORAGE_KEY = "musicViewMode";
|
const VIEW_MODE_STORAGE_KEY = "musicViewMode";
|
||||||
let viewMode = "list"; // "list" or "grid"
|
let viewMode = "list"; // "list" or "grid"
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
const list = await resp.json();
|
const list = await resp.json();
|
||||||
items = Array.isArray(list) ? list : [];
|
items = Array.isArray(list) ? list : [];
|
||||||
musicCount.set(items.length);
|
musicCount.set(items.length);
|
||||||
|
setQueue(items);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Music load error:", err);
|
console.error("Music load error:", err);
|
||||||
items = [];
|
items = [];
|
||||||
@@ -90,113 +92,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Player functions
|
// Player functions
|
||||||
async function playTrack(item, index) {
|
function playTrack(item, index) {
|
||||||
if (currentTrack?.id === item.id) {
|
if (!item) return;
|
||||||
|
const current = $musicPlayer.currentTrack;
|
||||||
|
if (current?.id === item.id) {
|
||||||
togglePlay();
|
togglePlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
playFromStore(item, index, items);
|
||||||
currentTrack = { ...item, index };
|
|
||||||
currentTime = 0;
|
|
||||||
duration = item.mediaInfo?.format?.duration || 0;
|
|
||||||
isPlaying = true;
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
if (!audioEl) return;
|
|
||||||
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) {
|
function seek(e) {
|
||||||
if (!audioEl || !duration) return;
|
|
||||||
const percent = parseFloat(e.target.value);
|
const percent = parseFloat(e.target.value);
|
||||||
currentTime = (percent / 100) * duration;
|
seekToPercent(percent);
|
||||||
audioEl.currentTime = currentTime;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setVolume(e) {
|
function setPlayerVolume(e) {
|
||||||
volume = parseFloat(e.target.value);
|
setVolume(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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setViewMode(mode) {
|
function setViewMode(mode) {
|
||||||
@@ -207,15 +119,6 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadViewMode();
|
loadViewMode();
|
||||||
loadMusic();
|
loadMusic();
|
||||||
startProgressInterval();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (progressInterval) clearInterval(progressInterval);
|
|
||||||
if (audioEl) {
|
|
||||||
audioEl.pause();
|
|
||||||
audioEl.src = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -281,14 +184,14 @@
|
|||||||
</div>
|
</div>
|
||||||
{#each items as item, idx (item.id)}
|
{#each items as item, idx (item.id)}
|
||||||
<div
|
<div
|
||||||
class="music-row {currentTrack?.id === item.id ? 'playing' : ''}"
|
class="music-row {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
|
||||||
on:click={() => playTrack(item, idx)}
|
on:click={() => playTrack(item, idx)}
|
||||||
on:dblclick={() => {
|
on:dblclick={() => {
|
||||||
if (currentTrack?.id === item.id) togglePlay();
|
if ($musicPlayer.currentTrack?.id === item.id) togglePlay();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div class="row-index">
|
<div class="row-index">
|
||||||
{#if currentTrack?.id === item.id && isPlaying}
|
{#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
|
||||||
<div class="playing-indicator">
|
<div class="playing-indicator">
|
||||||
<span class="bar bar-1"></span>
|
<span class="bar bar-1"></span>
|
||||||
<span class="bar bar-2"></span>
|
<span class="bar bar-2"></span>
|
||||||
@@ -340,7 +243,7 @@
|
|||||||
<div class="music-grid">
|
<div class="music-grid">
|
||||||
{#each items as item, idx (item.id)}
|
{#each items as item, idx (item.id)}
|
||||||
<div
|
<div
|
||||||
class="music-card {currentTrack?.id === item.id ? 'playing' : ''}"
|
class="music-card {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
|
||||||
on:click={() => playTrack(item, idx)}
|
on:click={() => playTrack(item, idx)}
|
||||||
>
|
>
|
||||||
<div class="card-thumb">
|
<div class="card-thumb">
|
||||||
@@ -356,7 +259,7 @@
|
|||||||
<i class="fa-solid fa-play"></i>
|
<i class="fa-solid fa-play"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if currentTrack?.id === item.id && isPlaying}
|
{#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
|
||||||
<div class="playing-badge">
|
<div class="playing-badge">
|
||||||
<i class="fa-solid fa-volume-high"></i>
|
<i class="fa-solid fa-volume-high"></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,23 +281,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Player Bar -->
|
<!-- Bottom Player Bar -->
|
||||||
{#if currentTrack}
|
{#if $musicPlayer.currentTrack}
|
||||||
<div class="player-bar">
|
<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">
|
<div class="player-content">
|
||||||
<!-- Track Info -->
|
<!-- Track Info -->
|
||||||
<div class="player-track">
|
<div class="player-track">
|
||||||
<div class="track-thumb">
|
<div class="track-thumb">
|
||||||
{#if thumbnailURL(currentTrack)}
|
{#if thumbnailURL($musicPlayer.currentTrack)}
|
||||||
<img src={thumbnailURL(currentTrack)} alt={currentTrack.title} />
|
<img
|
||||||
|
src={thumbnailURL($musicPlayer.currentTrack)}
|
||||||
|
alt={$musicPlayer.currentTrack.title}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="thumb-placeholder">
|
<div class="thumb-placeholder">
|
||||||
<i class="fa-solid fa-music"></i>
|
<i class="fa-solid fa-music"></i>
|
||||||
@@ -402,11 +299,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="track-info">
|
<div class="track-info">
|
||||||
<div class="track-name">{cleanFileName(currentTrack.title)}</div>
|
<div class="track-name">
|
||||||
<div class="track-source">{sourceLabel(currentTrack)}</div>
|
{cleanFileName($musicPlayer.currentTrack.title)}
|
||||||
</div>
|
</div>
|
||||||
<button class="like-btn" title="Beğen">
|
<div class="track-source">
|
||||||
<i class="fa-regular fa-heart"></i>
|
{sourceLabel($musicPlayer.currentTrack)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="like-btn" title="Kapat" on:click={stopPlayback}>
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -418,9 +319,9 @@
|
|||||||
<button
|
<button
|
||||||
class="control-btn play-pause-btn"
|
class="control-btn play-pause-btn"
|
||||||
on:click={togglePlay}
|
on:click={togglePlay}
|
||||||
title={isPlaying ? "Durdaklat" : "Oynat"}
|
title={$musicPlayer.isPlaying ? "Durdaklat" : "Oynat"}
|
||||||
>
|
>
|
||||||
{#if isPlaying}
|
{#if $musicPlayer.isPlaying}
|
||||||
<i class="fa-solid fa-pause"></i>
|
<i class="fa-solid fa-pause"></i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa-solid fa-play"></i>
|
<i class="fa-solid fa-play"></i>
|
||||||
@@ -433,18 +334,22 @@
|
|||||||
|
|
||||||
<!-- Progress -->
|
<!-- Progress -->
|
||||||
<div class="player-progress">
|
<div class="player-progress">
|
||||||
<span class="time-current">{formatTime(currentTime)}</span>
|
<span class="time-current">
|
||||||
|
{formatTime($musicPlayer.currentTime)}
|
||||||
|
</span>
|
||||||
<div class="progress-container">
|
<div class="progress-container">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="100"
|
||||||
value={(currentTime / duration) * 100 || 0}
|
value={
|
||||||
|
($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0
|
||||||
|
}
|
||||||
on:input={seek}
|
on:input={seek}
|
||||||
class="progress-slider"
|
class="progress-slider"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="time-total">{formatTime(duration)}</span>
|
<span class="time-total">{formatTime($musicPlayer.duration)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Volume & Extra -->
|
<!-- Volume & Extra -->
|
||||||
@@ -455,11 +360,11 @@
|
|||||||
on:click={toggleMute}
|
on:click={toggleMute}
|
||||||
title="Ses"
|
title="Ses"
|
||||||
>
|
>
|
||||||
{#if isMuted}
|
{#if $musicPlayer.isMuted}
|
||||||
<i class="fa-solid fa-volume-xmark"></i>
|
<i class="fa-solid fa-volume-xmark"></i>
|
||||||
{:else if volume < 0.3}
|
{:else if $musicPlayer.volume < 0.3}
|
||||||
<i class="fa-solid fa-volume-off"></i>
|
<i class="fa-solid fa-volume-off"></i>
|
||||||
{:else if volume < 0.7}
|
{:else if $musicPlayer.volume < 0.7}
|
||||||
<i class="fa-solid fa-volume-low"></i>
|
<i class="fa-solid fa-volume-low"></i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa-solid fa-volume-high"></i>
|
<i class="fa-solid fa-volume-high"></i>
|
||||||
@@ -471,17 +376,17 @@
|
|||||||
min="0"
|
min="0"
|
||||||
max="1"
|
max="1"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
bind:value={volume}
|
value={$musicPlayer.volume}
|
||||||
on:input={setVolume}
|
on:input={setPlayerVolume}
|
||||||
class="volume-slider"
|
class="volume-slider"
|
||||||
style="--fill: {volume * 100}%"
|
style="--fill: {$musicPlayer.volume * 100}%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
class="control-btn download-btn"
|
class="control-btn download-btn"
|
||||||
href={streamURL(currentTrack)}
|
href={streamURL($musicPlayer.currentTrack)}
|
||||||
download={currentTrack.title}
|
download={$musicPlayer.currentTrack.title}
|
||||||
title="İndir"
|
title="İndir"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-download"></i>
|
<i class="fa-solid fa-download"></i>
|
||||||
|
|||||||
203
client/src/stores/musicPlayerStore.js
Normal file
203
client/src/stores/musicPlayerStore.js
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import { writable, get } from "svelte/store";
|
||||||
|
import { API, withToken } from "../utils/api.js";
|
||||||
|
|
||||||
|
const INITIAL_STATE = {
|
||||||
|
currentTrack: null,
|
||||||
|
currentIndex: -1,
|
||||||
|
queue: [],
|
||||||
|
isPlaying: false,
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 0,
|
||||||
|
volume: 0.8,
|
||||||
|
isMuted: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const { subscribe, set, update } = writable({ ...INITIAL_STATE });
|
||||||
|
|
||||||
|
let audio = null;
|
||||||
|
let previousVolume = INITIAL_STATE.volume;
|
||||||
|
|
||||||
|
function ensureAudio() {
|
||||||
|
if (audio || typeof Audio === "undefined") return;
|
||||||
|
audio = new Audio();
|
||||||
|
audio.preload = "metadata";
|
||||||
|
audio.volume = INITIAL_STATE.volume;
|
||||||
|
|
||||||
|
audio.addEventListener("timeupdate", () => {
|
||||||
|
update((state) => ({
|
||||||
|
...state,
|
||||||
|
currentTime: audio.currentTime || 0,
|
||||||
|
duration: Number.isFinite(audio.duration) ? audio.duration : state.duration
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("loadedmetadata", () => {
|
||||||
|
update((state) => ({
|
||||||
|
...state,
|
||||||
|
duration: Number.isFinite(audio.duration) ? audio.duration : 0
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("play", () => {
|
||||||
|
update((state) => ({ ...state, isPlaying: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("pause", () => {
|
||||||
|
update((state) => ({ ...state, isPlaying: false }));
|
||||||
|
});
|
||||||
|
|
||||||
|
audio.addEventListener("ended", () => {
|
||||||
|
playNext();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStreamURL(item) {
|
||||||
|
const base = `${API}/stream/${item.infoHash}?index=${item.fileIndex || 0}`;
|
||||||
|
return withToken(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQueue(items = []) {
|
||||||
|
update((state) => ({ ...state, queue: Array.isArray(items) ? items : [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function playTrack(item, index, items = null) {
|
||||||
|
if (!item) return;
|
||||||
|
ensureAudio();
|
||||||
|
if (items) setQueue(items);
|
||||||
|
const state = get({ subscribe });
|
||||||
|
const nextIndex =
|
||||||
|
Number.isFinite(index) && index >= 0
|
||||||
|
? index
|
||||||
|
: state.queue.findIndex((entry) => entry.id === item.id);
|
||||||
|
|
||||||
|
update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
currentTrack: item,
|
||||||
|
currentIndex: nextIndex,
|
||||||
|
currentTime: 0,
|
||||||
|
duration: item.mediaInfo?.format?.duration || 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!audio) return;
|
||||||
|
audio.src = buildStreamURL(item);
|
||||||
|
audio
|
||||||
|
.play()
|
||||||
|
.catch(() => update((prev) => ({ ...prev, isPlaying: false })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function playByIndex(index) {
|
||||||
|
ensureAudio();
|
||||||
|
const state = get({ subscribe });
|
||||||
|
if (!state.queue.length || index < 0 || index >= state.queue.length) return;
|
||||||
|
const item = state.queue[index];
|
||||||
|
update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
currentTrack: item,
|
||||||
|
currentIndex: index,
|
||||||
|
currentTime: 0,
|
||||||
|
duration: item.mediaInfo?.format?.duration || 0
|
||||||
|
}));
|
||||||
|
if (!audio) return;
|
||||||
|
audio.src = buildStreamURL(item);
|
||||||
|
audio
|
||||||
|
.play()
|
||||||
|
.catch(() => update((prev) => ({ ...prev, isPlaying: false })));
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePlay() {
|
||||||
|
ensureAudio();
|
||||||
|
const state = get({ subscribe });
|
||||||
|
if (!audio || !state.currentTrack) return;
|
||||||
|
if (state.isPlaying) {
|
||||||
|
audio.pause();
|
||||||
|
} else {
|
||||||
|
audio
|
||||||
|
.play()
|
||||||
|
.catch(() => update((prev) => ({ ...prev, isPlaying: false })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playNext() {
|
||||||
|
const state = get({ subscribe });
|
||||||
|
if (!state.queue.length) return;
|
||||||
|
const nextIndex =
|
||||||
|
state.currentIndex >= 0
|
||||||
|
? (state.currentIndex + 1) % state.queue.length
|
||||||
|
: 0;
|
||||||
|
playByIndex(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPrevious() {
|
||||||
|
const state = get({ subscribe });
|
||||||
|
if (!state.queue.length) return;
|
||||||
|
const prevIndex =
|
||||||
|
state.currentIndex > 0
|
||||||
|
? state.currentIndex - 1
|
||||||
|
: state.queue.length - 1;
|
||||||
|
playByIndex(prevIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seekToPercent(percent) {
|
||||||
|
ensureAudio();
|
||||||
|
if (!audio) return;
|
||||||
|
const state = get({ subscribe });
|
||||||
|
if (!state.duration) return;
|
||||||
|
const nextTime = (Number(percent) / 100) * state.duration;
|
||||||
|
audio.currentTime = nextTime;
|
||||||
|
update((prev) => ({ ...prev, currentTime: nextTime }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVolume(value) {
|
||||||
|
ensureAudio();
|
||||||
|
const volume = Math.min(Math.max(Number(value) || 0, 0), 1);
|
||||||
|
if (audio) audio.volume = volume;
|
||||||
|
update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
volume,
|
||||||
|
isMuted: volume === 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMute() {
|
||||||
|
ensureAudio();
|
||||||
|
const state = get({ subscribe });
|
||||||
|
if (!audio) return;
|
||||||
|
if (state.isMuted || state.volume === 0) {
|
||||||
|
const nextVolume = previousVolume || 0.8;
|
||||||
|
audio.volume = nextVolume;
|
||||||
|
update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
volume: nextVolume,
|
||||||
|
isMuted: false
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
previousVolume = state.volume;
|
||||||
|
audio.volume = 0;
|
||||||
|
update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
volume: 0,
|
||||||
|
isMuted: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPlayback() {
|
||||||
|
if (audio) {
|
||||||
|
audio.pause();
|
||||||
|
audio.src = "";
|
||||||
|
}
|
||||||
|
set({ ...INITIAL_STATE });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const musicPlayer = { subscribe };
|
||||||
|
export {
|
||||||
|
setQueue,
|
||||||
|
playTrack,
|
||||||
|
togglePlay,
|
||||||
|
playNext,
|
||||||
|
playPrevious,
|
||||||
|
seekToPercent,
|
||||||
|
setVolume,
|
||||||
|
toggleMute,
|
||||||
|
stopPlayback
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user