Trash eklendi
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
import { API } from "./utils/api.js";
|
||||
import { refreshMovieCount } from "./stores/movieStore.js";
|
||||
import { refreshTvShowCount } from "./stores/tvStore.js";
|
||||
import { fetchTrashItems } from "./stores/trashStore.js";
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
refreshTimer = setTimeout(async () => {
|
||||
refreshTimer = null;
|
||||
try {
|
||||
await Promise.all([refreshMovieCount(), refreshTvShowCount()]);
|
||||
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
|
||||
} catch (err) {
|
||||
console.warn("Medya sayacı yenileme başarısız:", err);
|
||||
}
|
||||
@@ -45,6 +46,7 @@
|
||||
if (token) {
|
||||
refreshMovieCount();
|
||||
refreshTvShowCount();
|
||||
fetchTrashItems();
|
||||
const authToken = localStorage.getItem("token");
|
||||
if (authToken) {
|
||||
const wsUrl = `${API.replace("http", "ws")}?token=${authToken}`;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<script>
|
||||
import { Link } from "svelte-routing";
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
|
||||
import { movieCount } from "../stores/movieStore.js";
|
||||
import { tvShowCount } from "../stores/tvStore.js";
|
||||
import { apiFetch } from "../utils/api.js";
|
||||
import { Link } from "svelte-routing";
|
||||
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
|
||||
import { movieCount } from "../stores/movieStore.js";
|
||||
import { tvShowCount } from "../stores/tvStore.js";
|
||||
import { trashCount } from "../stores/trashStore.js";
|
||||
import { apiFetch } from "../utils/api.js";
|
||||
|
||||
export let menuOpen = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
let hasMovies = false;
|
||||
let hasShows = false;
|
||||
let hasTrash = false;
|
||||
// Svelte store kullanarak reaktivite sağla
|
||||
import { writable } from 'svelte/store';
|
||||
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
|
||||
@@ -37,9 +39,14 @@
|
||||
hasShows = (count ?? 0) > 0;
|
||||
});
|
||||
|
||||
const unsubscribeTrash = trashCount.subscribe((count) => {
|
||||
hasTrash = (count ?? 0) > 0;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribeMovie();
|
||||
unsubscribeTv();
|
||||
unsubscribeTrash();
|
||||
if (unsubscribeDiskSpace) {
|
||||
unsubscribeDiskSpace();
|
||||
}
|
||||
@@ -182,6 +189,9 @@
|
||||
>
|
||||
<i class="fa-solid fa-trash icon"></i>
|
||||
Trash
|
||||
{#if hasTrash}
|
||||
<span class="badge">{$trashCount}</span>
|
||||
{/if}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -208,7 +218,25 @@
|
||||
style="width: {diskSpace.usedPercent}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
margin-left: 8px;
|
||||
background: #f44336;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border-radius: 9px;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { onMount, tick } from "svelte";
|
||||
import { API, apiFetch } from "../utils/api.js";
|
||||
import { API, apiFetch, renameFolder } from "../utils/api.js";
|
||||
import { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
|
||||
import { refreshMovieCount } from "../stores/movieStore.js";
|
||||
import { refreshTvShowCount } from "../stores/tvStore.js";
|
||||
@@ -29,6 +29,9 @@
|
||||
let lastDragPath = "";
|
||||
let searchTerm = "";
|
||||
let hasSearch = false;
|
||||
let renamingFolder = null;
|
||||
let renameValue = "";
|
||||
let renameInput;
|
||||
|
||||
const normalizePath = (value) => {
|
||||
if (!value) return "";
|
||||
@@ -193,7 +196,8 @@
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
if (segments.length <= MAX_TRAIL_SEGMENTS + 1) {
|
||||
// Normal durumda tüm segmentleri göster
|
||||
if (segments.length <= 4) {
|
||||
segments.forEach((seg, index) => {
|
||||
const segPath = segments.slice(0, index + 1).join("/");
|
||||
crumbs.push({ label: seg, path: segPath });
|
||||
@@ -201,20 +205,84 @@
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
const firstSegment = segments[0];
|
||||
crumbs.push({ label: firstSegment, path: firstSegment });
|
||||
crumbs.push({ label: "...", path: null, ellipsis: true });
|
||||
|
||||
const tail = segments.slice(-MAX_TRAIL_SEGMENTS);
|
||||
tail.forEach((seg, index) => {
|
||||
const segPath = segments
|
||||
.slice(0, segments.length - tail.length + index + 1)
|
||||
.join("/");
|
||||
crumbs.push({ label: seg, path: segPath });
|
||||
// Overflow durumunda sadece ilk ve son 3'ü göster
|
||||
segments.forEach((seg, index) => {
|
||||
const segPath = segments.slice(0, index + 1).join("/");
|
||||
|
||||
if (index === 0) {
|
||||
// İlk segmenti her zaman göster
|
||||
crumbs.push({ label: seg, path: segPath });
|
||||
} else if (index >= segments.length - 3) {
|
||||
// Son 3 segmenti göster
|
||||
crumbs.push({ label: seg, path: segPath });
|
||||
}
|
||||
// Aradaki segmentleri gösterme (ellipsis ile değiştirilecek)
|
||||
});
|
||||
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
function checkBreadcrumbOverflow() {
|
||||
if (!breadcrumbContainer) return;
|
||||
|
||||
const containerWidth = breadcrumbContainer.offsetWidth;
|
||||
|
||||
// Tüm breadcrumb'ları göstererek genişliği hesapla
|
||||
const allBreadcrumbs = computeBreadcrumbs(currentPath);
|
||||
const totalSegments = allBreadcrumbs.length;
|
||||
|
||||
// Eğer 4'ten az segment varsa overflow gösterme
|
||||
if (totalSegments <= 4) {
|
||||
showBreadcrumbMenu = false;
|
||||
hiddenBreadcrumbs = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Container genişliğine göre overflow karar ver
|
||||
// 533px'den dar olduğunda overflow göster
|
||||
const shouldShowOverflow = containerWidth <= 533;
|
||||
|
||||
if (shouldShowOverflow) {
|
||||
updateHiddenBreadcrumbs();
|
||||
showBreadcrumbMenu = true;
|
||||
} else {
|
||||
showBreadcrumbMenu = false;
|
||||
hiddenBreadcrumbs = [];
|
||||
}
|
||||
}
|
||||
|
||||
function updateHiddenBreadcrumbs() {
|
||||
// İlk öğeyi (Home) koru, son 3 öğeyi koru, aradakileri gizle
|
||||
if (breadcrumbs.length <= 4) {
|
||||
hiddenBreadcrumbs = [];
|
||||
return;
|
||||
}
|
||||
|
||||
// Home'dan sonraki ve son 3'ten önceki öğeleri gizle
|
||||
hiddenBreadcrumbs = breadcrumbs.slice(1, -3);
|
||||
}
|
||||
|
||||
function toggleBreadcrumbMenu(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (!hiddenBreadcrumbs.length) return;
|
||||
|
||||
const button = event.currentTarget;
|
||||
const rect = button.getBoundingClientRect();
|
||||
|
||||
breadcrumbMenuPosition = {
|
||||
top: rect.bottom + 4,
|
||||
left: rect.left
|
||||
};
|
||||
|
||||
// Menüyü aç
|
||||
showBreadcrumbMenu = true;
|
||||
}
|
||||
|
||||
function closeBreadcrumbMenu() {
|
||||
showBreadcrumbMenu = false;
|
||||
}
|
||||
|
||||
function updateVisibleState(fileList, path = currentPath) {
|
||||
const dirs = buildDirectoryEntries(fileList);
|
||||
@@ -252,6 +320,16 @@
|
||||
);
|
||||
applyOrdering(path);
|
||||
breadcrumbs = computeBreadcrumbs(path);
|
||||
|
||||
// Breadcrumb'lar güncellendiğinde overflow kontrolünü tetikle
|
||||
tick().then(() => {
|
||||
if (breadcrumbContainer) {
|
||||
// Biraz bekle DOM'un güncellenmesi için
|
||||
setTimeout(() => {
|
||||
checkBreadcrumbOverflow();
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resolveOriginalPathForDisplay(displayPath, fallbackOriginal = currentOriginalPath) {
|
||||
@@ -369,6 +447,12 @@
|
||||
let isCreatingFolder = false;
|
||||
let newFolderName = "";
|
||||
|
||||
// Breadcrumb menü state
|
||||
let breadcrumbContainer;
|
||||
let showBreadcrumbMenu = false;
|
||||
let breadcrumbMenuPosition = { top: 0, left: 0 };
|
||||
let hiddenBreadcrumbs = [];
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const pathParam = params.get("path");
|
||||
@@ -521,6 +605,10 @@
|
||||
if (isCreatingFolder && !creating) {
|
||||
cancelCreateFolder();
|
||||
}
|
||||
const renaming = event.target.closest(".folder-rename-input");
|
||||
if (renamingFolder && !renaming) {
|
||||
cancelRenameFolder();
|
||||
}
|
||||
if (selectedItems.size === 0) return;
|
||||
const card = event.target.closest(".media-card");
|
||||
const header = event.target.closest(".header-actions");
|
||||
@@ -1310,7 +1398,7 @@
|
||||
cancelCreateFolder();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleFolderKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
@@ -1320,6 +1408,73 @@
|
||||
cancelCreateFolder();
|
||||
}
|
||||
}
|
||||
|
||||
function startRenameFolder(entry) {
|
||||
if (!entry || isCreatingFolder) return;
|
||||
if (renamingFolder && renamingFolder.name === entry.name) return;
|
||||
if (renamingFolder) cancelRenameFolder();
|
||||
renamingFolder = entry;
|
||||
renameValue = entry.displayName || "";
|
||||
activeMenu = null;
|
||||
tick().then(() => {
|
||||
if (renameInput) {
|
||||
renameInput.focus();
|
||||
renameInput.select();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function cancelRenameFolder() {
|
||||
renamingFolder = null;
|
||||
renameValue = "";
|
||||
renameInput = null;
|
||||
}
|
||||
|
||||
async function submitRenameFolder(entry = renamingFolder) {
|
||||
if (!renamingFolder || !entry || renamingFolder.name !== entry.name) {
|
||||
return;
|
||||
}
|
||||
const targetName = renameValue.trim();
|
||||
const originalPath = normalizePath(
|
||||
entry.primaryOriginalPath || entry.originalPaths?.[0] || entry.displayPath
|
||||
);
|
||||
|
||||
if (!originalPath) {
|
||||
cancelRenameFolder();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!targetName || targetName === entry.displayName) {
|
||||
cancelRenameFolder();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await renameFolder(originalPath, targetName);
|
||||
if (!response.success) {
|
||||
alert(
|
||||
"Yeniden adlandırma başarısız: " +
|
||||
(response.error || response.message || "Bilinmeyen hata")
|
||||
);
|
||||
return;
|
||||
}
|
||||
await loadFiles();
|
||||
cancelRenameFolder();
|
||||
} catch (err) {
|
||||
console.error("Yeniden adlandırma hatası:", err);
|
||||
alert("Klasör yeniden adlandırılamadı.");
|
||||
}
|
||||
}
|
||||
|
||||
function handleRenameKeydown(event, entry) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
submitRenameFolder(entry);
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
cancelRenameFolder();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
setSearchScope("files");
|
||||
@@ -1327,6 +1482,25 @@
|
||||
const token = localStorage.getItem("token");
|
||||
const wsUrl = `${API.replace("http", "ws")}?token=${token}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
// Breadcrumb overflow kontrolü için resize observer
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
// Biraz bekle DOM'un güncellenmesi için
|
||||
setTimeout(() => {
|
||||
checkBreadcrumbOverflow();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// Breadcrumb container'ı DOM'a eklendikten sonra gözlemciyi başlat
|
||||
tick().then(() => {
|
||||
if (breadcrumbContainer) {
|
||||
resizeObserver.observe(breadcrumbContainer);
|
||||
// İlk kontrolü yap
|
||||
setTimeout(() => {
|
||||
checkBreadcrumbOverflow();
|
||||
}, 50);
|
||||
}
|
||||
});
|
||||
const handlePopState = (event) => {
|
||||
if (typeof window === "undefined") return;
|
||||
const statePath =
|
||||
@@ -1391,6 +1565,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.type === "mediaDetected") {
|
||||
console.log("🎬 Otomatik medya tespiti tamamlandı:", msg);
|
||||
// Otomatik medya tespiti tamamlandığında dosyaları yeniden yükle
|
||||
await loadFiles();
|
||||
}
|
||||
if (msg.type === "progress" && msg.torrents) {
|
||||
for (const t of msg.torrents) {
|
||||
const savePath = t.savePath || "";
|
||||
@@ -1468,6 +1647,10 @@
|
||||
cancelCreateFolder();
|
||||
handled = true;
|
||||
}
|
||||
if (renamingFolder) {
|
||||
cancelRenameFolder();
|
||||
handled = true;
|
||||
}
|
||||
if (activeMenu) {
|
||||
activeMenu = null;
|
||||
handled = true;
|
||||
@@ -1550,6 +1733,9 @@
|
||||
if (activeMenu && !event.target.closest(".media-card")) {
|
||||
activeMenu = null;
|
||||
}
|
||||
if (showBreadcrumbMenu && !event.target.closest(".breadcrumb") && !event.target.closest(".breadcrumb-menu-portal")) {
|
||||
closeBreadcrumbMenu();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("click", handleClickOutside);
|
||||
@@ -1569,6 +1755,13 @@
|
||||
|
||||
window.removeEventListener("click", handleClickOutside);
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
|
||||
// Resize observer'ı temizle
|
||||
if (breadcrumbContainer) {
|
||||
const resizeObserver = new ResizeObserver(() => {});
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
try {
|
||||
ws.close();
|
||||
} catch (err) {
|
||||
@@ -1582,21 +1775,61 @@
|
||||
<div class="files-header">
|
||||
<div class="header-title">
|
||||
<h2>Media Library</h2>
|
||||
<div class="breadcrumb">
|
||||
{#each breadcrumbs as crumb, index (index)}
|
||||
{#if crumb.ellipsis}
|
||||
<span class="crumb ellipsis">...</span>
|
||||
{:else}
|
||||
<div class="breadcrumb-container" bind:this={breadcrumbContainer}>
|
||||
<div class="breadcrumb" class:has-overflow={showBreadcrumbMenu}>
|
||||
{#if showBreadcrumbMenu && hiddenBreadcrumbs.length > 0}
|
||||
<!-- İlk öğe (Home) -->
|
||||
<button
|
||||
type="button"
|
||||
class="crumb"
|
||||
class:is-active={normalizePath(crumb.path) === normalizePath(currentPath)}
|
||||
on:click|stopPropagation={() => handleBreadcrumbClick(crumb)}
|
||||
class:is-active={normalizePath(breadcrumbs[0].path) === normalizePath(currentPath)}
|
||||
on:click|stopPropagation={() => handleBreadcrumbClick(breadcrumbs[0])}
|
||||
>
|
||||
{index === 0 ? "/Home" : `/${crumb.label}`}
|
||||
{breadcrumbs[0].label === "Home" ? "Home" : breadcrumbs[0].label}
|
||||
</button>
|
||||
<i class="fa-solid fa-caret-right breadcrumb-separator"></i>
|
||||
|
||||
<!-- Ellipsis butonu -->
|
||||
<button
|
||||
type="button"
|
||||
class="crumb ellipsis"
|
||||
on:click={toggleBreadcrumbMenu}
|
||||
>
|
||||
...
|
||||
</button>
|
||||
<i class="fa-solid fa-caret-right breadcrumb-separator"></i>
|
||||
|
||||
<!-- Son 3 öğe -->
|
||||
{#each breadcrumbs.slice(-3) as crumb, index (crumb.path)}
|
||||
<button
|
||||
type="button"
|
||||
class="crumb"
|
||||
class:is-active={normalizePath(crumb.path) === normalizePath(currentPath)}
|
||||
on:click|stopPropagation={() => handleBreadcrumbClick(crumb)}
|
||||
>
|
||||
{crumb.label}
|
||||
</button>
|
||||
{#if index < 2}
|
||||
<i class="fa-solid fa-caret-right breadcrumb-separator"></i>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<!-- Normal breadcrumb görüntüleme -->
|
||||
{#each breadcrumbs as crumb, index (index)}
|
||||
<button
|
||||
type="button"
|
||||
class="crumb"
|
||||
class:is-active={normalizePath(crumb.path) === normalizePath(currentPath)}
|
||||
on:click|stopPropagation={() => handleBreadcrumbClick(crumb)}
|
||||
>
|
||||
{index === 0 ? "Home" : crumb.label}
|
||||
</button>
|
||||
{#if index < breadcrumbs.length - 1}
|
||||
<i class="fa-solid fa-caret-right breadcrumb-separator"></i>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@@ -1697,7 +1930,20 @@
|
||||
<img src={FOLDER_ICON_PATH} alt={`${entry.displayName} klasörü`} />
|
||||
</div>
|
||||
<div class="folder-info">
|
||||
<div class="folder-name">{cleanFileName(entry.displayName)}</div>
|
||||
{#if renamingFolder?.name === entry.name}
|
||||
<input
|
||||
class="folder-rename-input"
|
||||
type="text"
|
||||
bind:this={renameInput}
|
||||
bind:value={renameValue}
|
||||
on:keydown={(event) => handleRenameKeydown(event, entry)}
|
||||
on:blur={() => submitRenameFolder(entry)}
|
||||
on:click|stopPropagation
|
||||
autofocus
|
||||
/>
|
||||
{:else}
|
||||
<div class="folder-name">{cleanFileName(entry.displayName)}</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{#if entry.thumbnail}
|
||||
@@ -1833,6 +2079,13 @@
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
<span>Aç</span>
|
||||
</button>
|
||||
<button
|
||||
class="menu-item"
|
||||
on:click|stopPropagation={() => startRenameFolder(activeMenu)}
|
||||
>
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
<span>Yeniden adlandır</span>
|
||||
</button>
|
||||
<div class="menu-divider"></div>
|
||||
{:else}
|
||||
<button
|
||||
@@ -1861,6 +2114,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showBreadcrumbMenu && hiddenBreadcrumbs.length > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="breadcrumb-menu-portal"
|
||||
style="top: {breadcrumbMenuPosition.top}px; left: {breadcrumbMenuPosition.left}px;"
|
||||
on:click|stopPropagation
|
||||
>
|
||||
{#each hiddenBreadcrumbs as crumb, index (index)}
|
||||
<button
|
||||
class="breadcrumb-menu-item"
|
||||
on:click|stopPropagation={() => {
|
||||
handleBreadcrumbClick(crumb);
|
||||
closeBreadcrumbMenu();
|
||||
}}
|
||||
>
|
||||
<span class="breadcrumb-menu-text">{crumb.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showModal && selectedVideo}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="modal-overlay" on:click={closeModal}>
|
||||
@@ -2302,13 +2577,22 @@
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.breadcrumb-container {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
color: #757575;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.crumb {
|
||||
border: none;
|
||||
@@ -2330,6 +2614,59 @@
|
||||
color: #1f78ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 0 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.crumb.ellipsis {
|
||||
background: transparent;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.crumb.ellipsis:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.breadcrumb-menu-portal {
|
||||
position: fixed;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
min-width: 180px;
|
||||
max-width: 240px;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
.breadcrumb-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
.breadcrumb-menu-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.breadcrumb-menu-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
.crumb.ellipsis {
|
||||
cursor: default;
|
||||
color: #9b9b9b;
|
||||
@@ -2432,14 +2769,13 @@
|
||||
background: rgba(245, 179, 51, 0.1);
|
||||
border: 2px dashed var(--yellow);
|
||||
border-radius: 8px;
|
||||
padding: 18px 12px;
|
||||
padding: 12px 12px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 18px;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 210px;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@@ -2453,32 +2789,34 @@
|
||||
|
||||
.creating-folder .folder-thumb {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.creating-folder.list-view .folder-thumb {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
|
||||
.creating-folder .folder-thumb img {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
width: 95px;
|
||||
height: 95px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
|
||||
}
|
||||
|
||||
.creating-folder.list-view .folder-thumb img {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
}
|
||||
|
||||
.creating-folder .folder-info {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.creating-folder.list-view .folder-info {
|
||||
@@ -2512,8 +2850,8 @@
|
||||
/* === GALERİ === */
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.gallery.list-view {
|
||||
display: flex;
|
||||
@@ -2536,8 +2874,7 @@
|
||||
box-shadow 0.18s ease,
|
||||
flex-direction 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
padding 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
gap 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
min-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
gap 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
}
|
||||
.media-card::after {
|
||||
@@ -2578,7 +2915,7 @@
|
||||
min-height: 96px;
|
||||
}
|
||||
.media-card.list-view .thumb {
|
||||
width: 120px;
|
||||
width: 128px;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
@@ -2639,7 +2976,7 @@
|
||||
}
|
||||
.thumb {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 110px;
|
||||
object-fit: cover;
|
||||
border-radius: 10px 10px 0 0;
|
||||
transition:
|
||||
@@ -2655,7 +2992,7 @@
|
||||
background: #ddd;
|
||||
}
|
||||
.info {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
@@ -2867,7 +3204,7 @@
|
||||
}
|
||||
|
||||
.gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
.media-card.list-view {
|
||||
flex-direction: column;
|
||||
@@ -2890,7 +3227,7 @@
|
||||
}
|
||||
|
||||
.gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2928,8 +3265,8 @@
|
||||
/* === GALERİ === */
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
.gallery.list-view {
|
||||
display: flex;
|
||||
@@ -3037,7 +3374,7 @@
|
||||
}
|
||||
.thumb {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 110px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.thumb.placeholder {
|
||||
@@ -3178,7 +3515,7 @@
|
||||
/* === RESPONSIVE === */
|
||||
@media (max-width: 768px) {
|
||||
.gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
.media-card.list-view {
|
||||
flex-direction: column;
|
||||
@@ -3194,7 +3531,7 @@
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.gallery {
|
||||
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
}
|
||||
}
|
||||
/* Folder görünümü */
|
||||
@@ -3202,15 +3539,14 @@
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 24px 12px 18px;
|
||||
padding: 12px 12px 8px;
|
||||
align-items: center;
|
||||
min-height: 210px;
|
||||
}
|
||||
.folder-card::after {
|
||||
display: none;
|
||||
}
|
||||
.folder-card:hover {
|
||||
background: transparent;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -3223,21 +3559,23 @@
|
||||
}
|
||||
.folder-thumb {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
}
|
||||
.folder-thumb img {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
width: 95px;
|
||||
height: 95px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
|
||||
}
|
||||
.folder-info {
|
||||
margin-top: 14px;
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.folder-name {
|
||||
font-weight: 600;
|
||||
@@ -3246,9 +3584,24 @@
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.folder-rename-input {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #c9c9c9;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.folder-rename-input:focus {
|
||||
border-color: #2d965a;
|
||||
box-shadow: 0 0 0 2px rgba(45, 150, 90, 0.2);
|
||||
}
|
||||
.folder-card:hover .folder-name,
|
||||
.folder-card.is-selected .folder-name {
|
||||
color: #1f78ff;
|
||||
color: #333;
|
||||
}
|
||||
.folder-card.list-view {
|
||||
flex-direction: row;
|
||||
@@ -3258,8 +3611,8 @@
|
||||
gap: 18px;
|
||||
}
|
||||
.folder-card.list-view .folder-thumb {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
}
|
||||
.folder-card.list-view .folder-info {
|
||||
margin-top: 0;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -280,8 +280,8 @@ let filteredShows = [];
|
||||
const ext = name.split(".").pop()?.toLowerCase() || "";
|
||||
const inferredType = ext ? `video/${ext}` : "video/mp4";
|
||||
const size =
|
||||
Number(episode.fileSize) ||
|
||||
Number(episode.mediaInfo?.format?.size) ||
|
||||
Number(episode.mediaInfo?.format?.bit_rate) ||
|
||||
null;
|
||||
return {
|
||||
name,
|
||||
@@ -300,9 +300,19 @@ let filteredShows = [];
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const encodePathSegments = (value) =>
|
||||
value
|
||||
? value
|
||||
.split(/[\\/]/)
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/")
|
||||
: "";
|
||||
|
||||
$: selectedName = selectedVideo?.name ?? "";
|
||||
$: encName = selectedName ? encodeURIComponent(selectedName) : "";
|
||||
$: downloadHref = encName ? `${API}/downloads/${encName}` : "#";
|
||||
$: encName = encodePathSegments(selectedName);
|
||||
$: downloadHref = encName
|
||||
? `${API}/downloads/${encName}?token=${localStorage.getItem("token") || ""}`
|
||||
: "#";
|
||||
$: selectedLabel = selectedVideo?.episode
|
||||
? `${selectedVideo.show.title} · ${formatEpisodeCode(
|
||||
selectedVideo.episode
|
||||
@@ -414,8 +424,9 @@ async function openVideoAtIndex(index) {
|
||||
function getVideoURL() {
|
||||
if (!selectedName) return "";
|
||||
const token = localStorage.getItem("token");
|
||||
// selectedName zaten encode edilmiş, tekrar encode etme
|
||||
return `${API}/media/${selectedName}?token=${token}`;
|
||||
const encoded = encodePathSegments(selectedName);
|
||||
if (!encoded) return "";
|
||||
return `${API}/media/${encoded}?token=${token}`;
|
||||
}
|
||||
|
||||
function playEpisodeFromCard(episode) {
|
||||
|
||||
67
client/src/stores/trashStore.js
Normal file
67
client/src/stores/trashStore.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { getTrashItems, restoreFromTrash, deleteFromTrash } from '../utils/api';
|
||||
|
||||
export const trashItems = writable([]);
|
||||
export const trashCount = writable(0);
|
||||
|
||||
// Çöp öğelerini API'den al
|
||||
export async function fetchTrashItems() {
|
||||
try {
|
||||
const items = await getTrashItems();
|
||||
|
||||
const processedItems = Array.isArray(items)
|
||||
? items.map((item) => {
|
||||
const segments = String(item.name || "")
|
||||
.split(/[\\/]/)
|
||||
.filter(Boolean);
|
||||
const displayName =
|
||||
segments.length > 0 ? segments[segments.length - 1] : item.name;
|
||||
const parentPath =
|
||||
segments.length > 1 ? segments.slice(0, -1).join("/") : "";
|
||||
return {
|
||||
...item,
|
||||
displayName,
|
||||
parentPath
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
trashItems.set(processedItems);
|
||||
trashCount.set(processedItems.length);
|
||||
return processedItems;
|
||||
} catch (error) {
|
||||
console.error('Çöp öğeleri alınırken hata:', error);
|
||||
trashCount.set(0);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Çöpten geri yükle
|
||||
export async function restoreItem(trashName) {
|
||||
try {
|
||||
const result = await restoreFromTrash(trashName);
|
||||
if (result.success) {
|
||||
// Listeyi yenile
|
||||
await fetchTrashItems();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Öğe geri yüklenirken hata:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Çöpten tamamen sil
|
||||
export async function deleteItemPermanently(trashName) {
|
||||
try {
|
||||
const result = await deleteFromTrash(trashName);
|
||||
if (result.success) {
|
||||
// Listeyi yenile
|
||||
await fetchTrashItems();
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Öğe silinirken hata:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -19,3 +19,36 @@ export async function apiFetch(path, options = {}) {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// 🗑️ Çöp API'leri
|
||||
export async function getTrashItems() {
|
||||
const res = await apiFetch("/api/trash");
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function restoreFromTrash(trashName) {
|
||||
const res = await apiFetch("/api/trash/restore", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ trashName })
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function deleteFromTrash(trashName) {
|
||||
const res = await apiFetch(`/api/trash`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ trashName })
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function renameFolder(path, newName) {
|
||||
const res = await apiFetch("/api/folder", {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ path, newName })
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user