main #3
@@ -3,6 +3,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import Sidebar from "./components/Sidebar.svelte";
|
||||
import Topbar from "./components/Topbar.svelte";
|
||||
import MiniPlayer from "./components/MiniPlayer.svelte";
|
||||
import Files from "./routes/Files.svelte";
|
||||
import Transfers from "./routes/Transfers.svelte";
|
||||
import Trash from "./routes/Trash.svelte";
|
||||
@@ -156,6 +157,8 @@
|
||||
<Route path="/trash" component={Trash} />
|
||||
</div>
|
||||
|
||||
<MiniPlayer />
|
||||
|
||||
<!-- Sidebar dışına tıklayınca kapanma -->
|
||||
{#if menuOpen}
|
||||
<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>
|
||||
import { onMount, tick } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { API, apiFetch, withToken } from "../utils/api.js";
|
||||
import { musicCount } from "../stores/musicStore.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 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
|
||||
const VIEW_MODE_STORAGE_KEY = "musicViewMode";
|
||||
let viewMode = "list"; // "list" or "grid"
|
||||
@@ -48,6 +49,7 @@
|
||||
const list = await resp.json();
|
||||
items = Array.isArray(list) ? list : [];
|
||||
musicCount.set(items.length);
|
||||
setQueue(items);
|
||||
} catch (err) {
|
||||
console.error("Music load error:", err);
|
||||
items = [];
|
||||
@@ -90,113 +92,23 @@
|
||||
}
|
||||
|
||||
// Player functions
|
||||
async function playTrack(item, index) {
|
||||
if (currentTrack?.id === item.id) {
|
||||
function playTrack(item, index) {
|
||||
if (!item) return;
|
||||
const current = $musicPlayer.currentTrack;
|
||||
if (current?.id === item.id) {
|
||||
togglePlay();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
playFromStore(item, index, items);
|
||||
}
|
||||
|
||||
function seek(e) {
|
||||
if (!audioEl || !duration) return;
|
||||
const percent = parseFloat(e.target.value);
|
||||
currentTime = (percent / 100) * duration;
|
||||
audioEl.currentTime = currentTime;
|
||||
seekToPercent(percent);
|
||||
}
|
||||
|
||||
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);
|
||||
function setPlayerVolume(e) {
|
||||
setVolume(e.target.value);
|
||||
}
|
||||
|
||||
function setViewMode(mode) {
|
||||
@@ -207,15 +119,6 @@
|
||||
onMount(() => {
|
||||
loadViewMode();
|
||||
loadMusic();
|
||||
startProgressInterval();
|
||||
|
||||
return () => {
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
if (audioEl) {
|
||||
audioEl.pause();
|
||||
audioEl.src = "";
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -281,14 +184,14 @@
|
||||
</div>
|
||||
{#each items as item, idx (item.id)}
|
||||
<div
|
||||
class="music-row {currentTrack?.id === item.id ? 'playing' : ''}"
|
||||
class="music-row {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
|
||||
on:click={() => playTrack(item, idx)}
|
||||
on:dblclick={() => {
|
||||
if (currentTrack?.id === item.id) togglePlay();
|
||||
if ($musicPlayer.currentTrack?.id === item.id) togglePlay();
|
||||
}}
|
||||
>
|
||||
<div class="row-index">
|
||||
{#if currentTrack?.id === item.id && isPlaying}
|
||||
{#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
|
||||
<div class="playing-indicator">
|
||||
<span class="bar bar-1"></span>
|
||||
<span class="bar bar-2"></span>
|
||||
@@ -340,7 +243,7 @@
|
||||
<div class="music-grid">
|
||||
{#each items as item, idx (item.id)}
|
||||
<div
|
||||
class="music-card {currentTrack?.id === item.id ? 'playing' : ''}"
|
||||
class="music-card {$musicPlayer.currentTrack?.id === item.id ? 'playing' : ''}"
|
||||
on:click={() => playTrack(item, idx)}
|
||||
>
|
||||
<div class="card-thumb">
|
||||
@@ -356,7 +259,7 @@
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</button>
|
||||
</div>
|
||||
{#if currentTrack?.id === item.id && isPlaying}
|
||||
{#if $musicPlayer.currentTrack?.id === item.id && $musicPlayer.isPlaying}
|
||||
<div class="playing-badge">
|
||||
<i class="fa-solid fa-volume-high"></i>
|
||||
</div>
|
||||
@@ -378,23 +281,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Bottom Player Bar -->
|
||||
{#if currentTrack}
|
||||
{#if $musicPlayer.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} />
|
||||
{#if thumbnailURL($musicPlayer.currentTrack)}
|
||||
<img
|
||||
src={thumbnailURL($musicPlayer.currentTrack)}
|
||||
alt={$musicPlayer.currentTrack.title}
|
||||
/>
|
||||
{:else}
|
||||
<div class="thumb-placeholder">
|
||||
<i class="fa-solid fa-music"></i>
|
||||
@@ -402,11 +299,15 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="track-info">
|
||||
<div class="track-name">{cleanFileName(currentTrack.title)}</div>
|
||||
<div class="track-source">{sourceLabel(currentTrack)}</div>
|
||||
<div class="track-name">
|
||||
{cleanFileName($musicPlayer.currentTrack.title)}
|
||||
</div>
|
||||
<div class="track-source">
|
||||
{sourceLabel($musicPlayer.currentTrack)}
|
||||
</div>
|
||||
</div>
|
||||
<button class="like-btn" title="Beğen">
|
||||
<i class="fa-regular fa-heart"></i>
|
||||
<button class="like-btn" title="Kapat" on:click={stopPlayback}>
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -418,9 +319,9 @@
|
||||
<button
|
||||
class="control-btn play-pause-btn"
|
||||
on:click={togglePlay}
|
||||
title={isPlaying ? "Durdaklat" : "Oynat"}
|
||||
title={$musicPlayer.isPlaying ? "Durdaklat" : "Oynat"}
|
||||
>
|
||||
{#if isPlaying}
|
||||
{#if $musicPlayer.isPlaying}
|
||||
<i class="fa-solid fa-pause"></i>
|
||||
{:else}
|
||||
<i class="fa-solid fa-play"></i>
|
||||
@@ -433,18 +334,22 @@
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="player-progress">
|
||||
<span class="time-current">{formatTime(currentTime)}</span>
|
||||
<span class="time-current">
|
||||
{formatTime($musicPlayer.currentTime)}
|
||||
</span>
|
||||
<div class="progress-container">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={(currentTime / duration) * 100 || 0}
|
||||
value={
|
||||
($musicPlayer.currentTime / $musicPlayer.duration) * 100 || 0
|
||||
}
|
||||
on:input={seek}
|
||||
class="progress-slider"
|
||||
/>
|
||||
</div>
|
||||
<span class="time-total">{formatTime(duration)}</span>
|
||||
<span class="time-total">{formatTime($musicPlayer.duration)}</span>
|
||||
</div>
|
||||
|
||||
<!-- Volume & Extra -->
|
||||
@@ -455,11 +360,11 @@
|
||||
on:click={toggleMute}
|
||||
title="Ses"
|
||||
>
|
||||
{#if isMuted}
|
||||
{#if $musicPlayer.isMuted}
|
||||
<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>
|
||||
{:else if volume < 0.7}
|
||||
{:else if $musicPlayer.volume < 0.7}
|
||||
<i class="fa-solid fa-volume-low"></i>
|
||||
{:else}
|
||||
<i class="fa-solid fa-volume-high"></i>
|
||||
@@ -471,17 +376,17 @@
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
bind:value={volume}
|
||||
on:input={setVolume}
|
||||
value={$musicPlayer.volume}
|
||||
on:input={setPlayerVolume}
|
||||
class="volume-slider"
|
||||
style="--fill: {volume * 100}%"
|
||||
style="--fill: {$musicPlayer.volume * 100}%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="control-btn download-btn"
|
||||
href={streamURL(currentTrack)}
|
||||
download={currentTrack.title}
|
||||
href={streamURL($musicPlayer.currentTrack)}
|
||||
download={$musicPlayer.currentTrack.title}
|
||||
title="İndir"
|
||||
>
|
||||
<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