Sİdebar'da music kategorisi oluşturuldu
This commit is contained in:
284
client/src/routes/Music.svelte
Normal file
284
client/src/routes/Music.svelte
Normal file
@@ -0,0 +1,284 @@
|
||||
<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;
|
||||
|
||||
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}&v=${Date.now()}`;
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
if (!Number.isFinite(seconds) || seconds <= 0) return "";
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function sourceLabel(item) {
|
||||
if (item.tracker === "youtube" || item.thumbnail) return "YouTube";
|
||||
return "Music";
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadMusic();
|
||||
});
|
||||
</script>
|
||||
|
||||
<section class="music-page">
|
||||
<div class="music-header">
|
||||
<div class="music-title-wrap">
|
||||
<h2>Music</h2>
|
||||
{#if !loading && !error}
|
||||
<span class="song-count">{items.length} Songs</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="refresh-btn" on:click={loadMusic} disabled={loading}>
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
Yenile
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="music-empty">Yükleniyor…</div>
|
||||
{:else if error}
|
||||
<div class="music-empty error">{error}</div>
|
||||
{:else if !items.length}
|
||||
<div class="music-empty">Henüz müzik videosu yok.</div>
|
||||
{:else}
|
||||
<div class="music-list">
|
||||
{#each items as item, idx (item.id)}
|
||||
<div class="music-row">
|
||||
<div class="index">{String(idx + 1).padStart(2, "0")}</div>
|
||||
<div class="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="track-info">
|
||||
<div class="name">{cleanFileName(item.title)}</div>
|
||||
<div class="meta">{sourceLabel(item)}</div>
|
||||
</div>
|
||||
<div class="duration">
|
||||
{formatDuration(item.mediaInfo?.format?.duration || item.duration)}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a
|
||||
class="play-btn"
|
||||
href={streamURL(item)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Oynat"
|
||||
>
|
||||
<i class="fa-solid fa-play"></i>
|
||||
</a>
|
||||
<a
|
||||
class="open-btn"
|
||||
href={`/files?path=${encodeURIComponent(item.folder)}`}
|
||||
title="Klasöre git"
|
||||
>
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.music-page {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.music-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.music-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.song-count {
|
||||
color: #8a8fa3;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.music-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.music-row {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 56px 1fr 70px 90px;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
background: #f6f6f6;
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1rem;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.index {
|
||||
font-weight: 700;
|
||||
color: #8a8fa3;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #d2d8ff, #f0f3ff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumb-placeholder {
|
||||
color: #6f7ba3;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.track-info .name {
|
||||
font-weight: 700;
|
||||
color: #1c2440;
|
||||
}
|
||||
|
||||
.track-info .meta {
|
||||
color: #8a8fa3;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.duration {
|
||||
text-align: right;
|
||||
color: #8a8fa3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.play-btn,
|
||||
.open-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1f8a70;
|
||||
background: #e8f5f1;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.open-btn {
|
||||
color: #5f6f92;
|
||||
background: #eef2f9;
|
||||
}
|
||||
|
||||
.play-btn:hover,
|
||||
.open-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.music-empty {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
border: 1px dashed #ddd;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.music-empty.error {
|
||||
border-color: #eb5757;
|
||||
color: #eb5757;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
border: none;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: #2c3e50;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user