fix(client-server): rclone ve miniplayer düzeltmeleri

This commit is contained in:
2026-02-04 00:36:46 +03:00
parent 42447a63f3
commit 0255a44120
5 changed files with 340 additions and 61 deletions

View File

@@ -1,10 +1,23 @@
<script>
import { onMount, afterUpdate, tick } from "svelte";
import { musicPlayer, togglePlay, playNext, playPrevious, stopPlayback } from "../stores/musicPlayerStore.js";
import { useLocation } from "svelte-routing";
import { API, withToken } from "../utils/api.js";
import { cleanFileName } from "../utils/filename.js";
let videoEl = null;
let titleWrap = null;
let titleInner = null;
let marqueeShift = "0px";
let marqueeDuration = "0s";
let marqueeEnabled = false;
let dragX = 0;
let dragY = 0;
let dragging = false;
let dragStartX = 0;
let dragStartY = 0;
let originX = 0;
let originY = 0;
const location = useLocation();
$: isMusicRoute = ($location?.pathname || "").startsWith("/music");
@@ -39,22 +52,96 @@
return withToken(`${API}/stream/${item.infoHash}?index=${index}`);
}
const HARD_SYNC_THRESHOLD = 0.2;
const SOFT_SYNC_THRESHOLD = 0.05;
$: if (videoEl && $musicPlayer.currentTrack && $musicPlayer.isPlaying) {
const target = $musicPlayer.currentTime || 0;
if (Number.isFinite(target) && Math.abs(videoEl.currentTime - target) > 0.6) {
videoEl.currentTime = target;
if (!Number.isFinite(target)) {
videoEl.playbackRate = 1;
} else {
const delta = target - (videoEl.currentTime || 0);
if (Math.abs(delta) > HARD_SYNC_THRESHOLD) {
videoEl.currentTime = target;
} else if (Math.abs(delta) > SOFT_SYNC_THRESHOLD) {
videoEl.playbackRate = delta > 0 ? 1.02 : 0.98;
} else {
videoEl.playbackRate = 1;
}
}
}
async function updateMarquee() {
if (!titleWrap || !titleInner) return;
await tick();
const wrapW = titleWrap.clientWidth || 0;
const textW = titleInner.scrollWidth || 0;
if (textW > wrapW + 2) {
const shift = Math.max(textW - wrapW, 0);
marqueeShift = `${shift}px`;
marqueeDuration = `${Math.max(8, shift / 25)}s`;
marqueeEnabled = true;
} else {
marqueeShift = "0px";
marqueeDuration = "0s";
marqueeEnabled = false;
}
}
onMount(() => {
updateMarquee();
});
afterUpdate(() => {
updateMarquee();
});
$: if ($musicPlayer.currentTrack?.id) {
setTimeout(updateMarquee, 0);
}
$: if (videoEl && $musicPlayer.isPlaying) {
videoEl.play().catch(() => undefined);
} else if (videoEl) {
videoEl.pause();
}
function shouldIgnoreDrag(target) {
if (!target) return false;
return Boolean(target.closest("button, a, input, textarea, select, [data-no-drag]"));
}
function startDrag(event) {
if (shouldIgnoreDrag(event.target)) return;
dragging = true;
dragStartX = event.clientX;
dragStartY = event.clientY;
originX = dragX;
originY = dragY;
window.addEventListener("pointermove", onDrag);
window.addEventListener("pointerup", endDrag, { once: true });
}
function onDrag(event) {
if (!dragging) return;
dragX = originX + (event.clientX - dragStartX);
dragY = originY + (event.clientY - dragStartY);
}
function endDrag() {
dragging = false;
window.removeEventListener("pointermove", onDrag);
}
</script>
{#if $musicPlayer.currentTrack && !isMusicRoute}
<div class="mini-player" aria-label="Mini music player">
<div
class="mini-player {dragging ? 'dragging' : ''}"
aria-label="Mini music player"
on:pointerdown={startDrag}
style="transform: translate({dragX}px, {dragY}px);"
>
<div class="mini-top"></div>
<div class="mini-cover">
@@ -78,8 +165,16 @@
{/if}
</div>
<div class="mini-meta">
<div class="mini-user-name">
{cleanFileName($musicPlayer.currentTrack.title)}
<div class="mini-user-name" bind:this={titleWrap}>
{#key $musicPlayer.currentTrack?.id}
<span
class="marquee {marqueeEnabled ? 'active' : ''}"
bind:this={titleInner}
style="--marquee-shift: {marqueeShift}; --marquee-duration: {marqueeDuration};"
>
{cleanFileName($musicPlayer.currentTrack.title)}
</span>
{/key}
</div>
<div class="mini-user-handle">{sourceLabel($musicPlayer.currentTrack)}</div>
</div>
@@ -98,20 +193,20 @@
</div>
<div class="mini-controls">
<button class="mini-btn" on:click={playPrevious} title="Önceki">
<button class="mini-btn" on:click={playPrevious} title="Önceki" data-no-drag>
<i class="fa-solid fa-backward-step"></i>
</button>
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur">
<button class="mini-btn main" on:click={togglePlay} title="Oynat/Durdur" data-no-drag>
{#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">
<button class="mini-btn" on:click={playNext} title="Sonraki" data-no-drag>
<i class="fa-solid fa-forward-step"></i>
</button>
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat">
<button class="mini-btn ghost" on:click={stopPlayback} title="Kapat" data-no-drag>
<i class="fa-solid fa-xmark"></i>
</button>
</div>
@@ -132,6 +227,12 @@
color: #f6f6f6;
backdrop-filter: blur(14px);
z-index: 50;
cursor: grab;
touch-action: none;
}
.mini-player.dragging {
cursor: grabbing;
}
.mini-top {
@@ -140,7 +241,7 @@
.mini-meta {
text-align: left;
margin: 0 4px 8px;
margin: 0 4px 8px -4px;
}
.mini-user-name {
@@ -149,6 +250,19 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
position: relative;
max-width: 100%;
min-width: 0;
}
.mini-user-name .marquee {
display: inline-block;
will-change: transform;
}
.mini-user-name .marquee.active {
animation: mini-marquee var(--marquee-duration) linear infinite;
}
.mini-user-handle {
@@ -156,6 +270,37 @@
color: rgba(255, 255, 255, 0.6);
}
@keyframes mini-marquee {
0% {
transform: translateX(0);
opacity: 1;
}
10% {
transform: translateX(0);
opacity: 1;
}
60% {
transform: translateX(calc(-1 * var(--marquee-shift)));
opacity: 1;
}
80% {
transform: translateX(calc(-1 * var(--marquee-shift)));
opacity: 1;
}
84% {
transform: translateX(calc(-1 * var(--marquee-shift)));
opacity: 0;
}
86% {
transform: translateX(0);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.mini-cover {
margin: -16px -16px 12px;
width: calc(100% + 32px);
@@ -164,7 +309,7 @@
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);
border: none;
display: block;
}

View File

@@ -626,6 +626,25 @@
alert(err?.message || "GDrive taşıma başarısız oldu.");
}
}
async function setCategory(entry, category) {
if (!entry?.name) return;
try {
const resp = await apiFetch("/api/file/category", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ path: entry.name, category })
});
const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data?.ok) {
alert(data?.error || "Kategori güncellenemedi.");
return;
}
await loadFiles();
} catch (err) {
alert(err?.message || "Kategori güncellenemedi.");
}
}
function formatSize(bytes) {
if (!bytes) return "0 MB";
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
@@ -1301,45 +1320,55 @@
}
function matchFile(file) {
if (!file || file.isDirectory) {
closeMenu();
return;
}
// Dosya adını al (path'in son kısmı)
const fileName = file.name.split('/').pop();
// Önce dizi kontrolü yap (SxxExx formatı)
const seriesMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (seriesMatch) {
matchType = "series";
const { title, year } = extractTitleAndYear(fileName);
matchTitle = title || fileName;
matchYear = year ? String(year) : "";
} else {
// Film kontrolü (yıl bilgisi)
const { title, year } = extractTitleAndYear(fileName);
if (year && year >= 1900 && year <= 2099) {
matchType = "movie";
matchTitle = title || fileName;
matchYear = String(year);
} else {
// Varsayılan olarak film kabul et
matchType = "movie";
matchTitle = title || fileName;
matchYear = "";
try {
if (!file || file.isDirectory) {
closeMenu();
return;
}
const rawName = file?.name || file?.displayName || "";
const fileName = rawName.split("/").pop();
if (!fileName) {
showToast("Eşleştirilecek dosya adı bulunamadı.", "error");
closeMenu();
return;
}
// Önce dizi kontrolü yap (SxxExx formatı)
const seriesMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (seriesMatch) {
matchType = "series";
const { title, year } = extractTitleAndYear(fileName);
matchTitle = title || fileName;
matchYear = year ? String(year) : "";
} else {
// Film kontrolü (yıl bilgisi)
const { title, year } = extractTitleAndYear(fileName);
if (year && year >= 1900 && year <= 2099) {
matchType = "movie";
matchTitle = title || fileName;
matchYear = String(year);
} else {
// Varsayılan olarak film kabul et
matchType = "movie";
matchTitle = title || fileName;
matchYear = "";
}
}
matchingFile = file;
showMatchModal = true;
closeMenu();
// Modal açıldıktan sonra otomatik arama yap
tick().then(() => {
searchMetadata();
});
} catch (err) {
showToast("Eşleştirme penceresi açılamadı.", "error");
closeMenu();
}
matchingFile = file;
showMatchModal = true;
closeMenu();
// Modal açıldıktan sonra otomatik arama yap
tick().then(() => {
searchMetadata();
});
}
function closeMatchModal() {
@@ -1370,6 +1399,10 @@
params.set("year", matchYear);
}
const token = localStorage.getItem("token");
if (token) {
params.set("token", token);
}
const response = await apiFetch(`/api/search/metadata?${params}`);
if (response.ok) {
@@ -1486,8 +1519,6 @@
function closeMenu() {
activeMenu = null;
deleteConfirmPending = false;
showMatchModal = false;
matchingFile = null;
}
// Klasör oluşturma fonksiyonları
@@ -2395,6 +2426,20 @@
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Eşleştir</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => setCategory(activeMenu, "music")}
>
<i class="fa-solid fa-music"></i>
<span>Kategori: Müzik</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => setCategory(activeMenu, "auto")}
>
<i class="fa-solid fa-rotate-left"></i>
<span>Kategori: Otomatik</span>
</button>
<div class="menu-divider"></div>
<button
class="menu-item"
@@ -3247,12 +3292,14 @@
.name {
font-weight: 600;
font-size: 13px;
text-align: center;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.3;
max-height: calc(1.3em * 2);
min-height: calc(1.3em * 2);
text-overflow: ellipsis;
word-break: break-word;
}
@@ -3823,7 +3870,8 @@
.folder-info {
margin-top: 4px;
width: 100%;
text-align: center;
text-align: left;
padding: 0 8px;
flex-shrink: 0;
}
.folder-name {
@@ -3835,8 +3883,9 @@
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-align: center;
text-align: left;
max-height: calc(1.25em * 2);
min-height: calc(1.25em * 2);
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -241,8 +241,10 @@
throw new Error(data?.error || `HTTP ${resp.status}`);
}
showToast("Cache temizlendi.", "success");
await loadRcloneStatus();
} catch (err) {
error = err?.message || "Cache temizlenemedi.";
showToast(error, "error");
}
}

View File

@@ -15,10 +15,10 @@ export function showToast(message, type = 'success', duration = 3000) {
}
// Yeni toast'ı göster
toast.update({ message, type, visible: true });
toast.set({ message, type, visible: true });
// Belirli süre sonra gizle
toastTimeout = setTimeout(() => {
toast.update({ message: null, type: 'success', visible: false });
toast.set({ message: null, type: 'success', visible: false });
}, duration);
}