Trash eklendi

This commit is contained in:
2025-11-02 00:15:06 +03:00
parent 90009d9fbe
commit 3e07e2a270
8 changed files with 2670 additions and 135 deletions

View File

@@ -12,6 +12,7 @@
import { API } from "./utils/api.js"; import { API } from "./utils/api.js";
import { refreshMovieCount } from "./stores/movieStore.js"; import { refreshMovieCount } from "./stores/movieStore.js";
import { refreshTvShowCount } from "./stores/tvStore.js"; import { refreshTvShowCount } from "./stores/tvStore.js";
import { fetchTrashItems } from "./stores/trashStore.js";
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@@ -24,7 +25,7 @@
refreshTimer = setTimeout(async () => { refreshTimer = setTimeout(async () => {
refreshTimer = null; refreshTimer = null;
try { try {
await Promise.all([refreshMovieCount(), refreshTvShowCount()]); await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
} catch (err) { } catch (err) {
console.warn("Medya sayacı yenileme başarısız:", err); console.warn("Medya sayacı yenileme başarısız:", err);
} }
@@ -45,6 +46,7 @@
if (token) { if (token) {
refreshMovieCount(); refreshMovieCount();
refreshTvShowCount(); refreshTvShowCount();
fetchTrashItems();
const authToken = localStorage.getItem("token"); const authToken = localStorage.getItem("token");
if (authToken) { if (authToken) {
const wsUrl = `${API.replace("http", "ws")}?token=${authToken}`; const wsUrl = `${API.replace("http", "ws")}?token=${authToken}`;

View File

@@ -1,14 +1,16 @@
<script> <script>
import { Link } from "svelte-routing"; import { Link } from "svelte-routing";
import { createEventDispatcher, onDestroy, onMount, tick } from "svelte"; import { createEventDispatcher, onDestroy, onMount, tick } from "svelte";
import { movieCount } from "../stores/movieStore.js"; import { movieCount } from "../stores/movieStore.js";
import { tvShowCount } from "../stores/tvStore.js"; import { tvShowCount } from "../stores/tvStore.js";
import { apiFetch } from "../utils/api.js"; import { trashCount } from "../stores/trashStore.js";
import { apiFetch } from "../utils/api.js";
export let menuOpen = false; export let menuOpen = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let hasMovies = false; let hasMovies = false;
let hasShows = false; let hasShows = false;
let hasTrash = false;
// Svelte store kullanarak reaktivite sağla // Svelte store kullanarak reaktivite sağla
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 }); const diskSpaceStore = writable({ totalGB: '0', usedGB: '0', usedPercent: 0 });
@@ -37,9 +39,14 @@
hasShows = (count ?? 0) > 0; hasShows = (count ?? 0) > 0;
}); });
const unsubscribeTrash = trashCount.subscribe((count) => {
hasTrash = (count ?? 0) > 0;
});
onDestroy(() => { onDestroy(() => {
unsubscribeMovie(); unsubscribeMovie();
unsubscribeTv(); unsubscribeTv();
unsubscribeTrash();
if (unsubscribeDiskSpace) { if (unsubscribeDiskSpace) {
unsubscribeDiskSpace(); unsubscribeDiskSpace();
} }
@@ -182,6 +189,9 @@
> >
<i class="fa-solid fa-trash icon"></i> <i class="fa-solid fa-trash icon"></i>
Trash Trash
{#if hasTrash}
<span class="badge">{$trashCount}</span>
{/if}
</Link> </Link>
</div> </div>
@@ -208,7 +218,25 @@
style="width: {diskSpace.usedPercent}%" style="width: {diskSpace.usedPercent}%"
></div> ></div>
</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> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
<script> <script>
import { onMount, tick } from "svelte"; 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 { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
import { refreshMovieCount } from "../stores/movieStore.js"; import { refreshMovieCount } from "../stores/movieStore.js";
import { refreshTvShowCount } from "../stores/tvStore.js"; import { refreshTvShowCount } from "../stores/tvStore.js";
@@ -29,6 +29,9 @@
let lastDragPath = ""; let lastDragPath = "";
let searchTerm = ""; let searchTerm = "";
let hasSearch = false; let hasSearch = false;
let renamingFolder = null;
let renameValue = "";
let renameInput;
const normalizePath = (value) => { const normalizePath = (value) => {
if (!value) return ""; if (!value) return "";
@@ -193,7 +196,8 @@
return crumbs; return crumbs;
} }
if (segments.length <= MAX_TRAIL_SEGMENTS + 1) { // Normal durumda tüm segmentleri göster
if (segments.length <= 4) {
segments.forEach((seg, index) => { segments.forEach((seg, index) => {
const segPath = segments.slice(0, index + 1).join("/"); const segPath = segments.slice(0, index + 1).join("/");
crumbs.push({ label: seg, path: segPath }); crumbs.push({ label: seg, path: segPath });
@@ -201,20 +205,84 @@
return crumbs; return crumbs;
} }
const firstSegment = segments[0]; // Overflow durumunda sadece ilk ve son 3'ü göster
crumbs.push({ label: firstSegment, path: firstSegment }); segments.forEach((seg, index) => {
crumbs.push({ label: "...", path: null, ellipsis: true }); const segPath = segments.slice(0, index + 1).join("/");
const tail = segments.slice(-MAX_TRAIL_SEGMENTS); if (index === 0) {
tail.forEach((seg, index) => { // İlk segmenti her zaman göster
const segPath = segments crumbs.push({ label: seg, path: segPath });
.slice(0, segments.length - tail.length + index + 1) } else if (index >= segments.length - 3) {
.join("/"); // Son 3 segmenti göster
crumbs.push({ label: seg, path: segPath }); crumbs.push({ label: seg, path: segPath });
}
// Aradaki segmentleri gösterme (ellipsis ile değiştirilecek)
}); });
return crumbs; 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) { function updateVisibleState(fileList, path = currentPath) {
const dirs = buildDirectoryEntries(fileList); const dirs = buildDirectoryEntries(fileList);
@@ -252,6 +320,16 @@
); );
applyOrdering(path); applyOrdering(path);
breadcrumbs = computeBreadcrumbs(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) { function resolveOriginalPathForDisplay(displayPath, fallbackOriginal = currentOriginalPath) {
@@ -369,6 +447,12 @@
let isCreatingFolder = false; let isCreatingFolder = false;
let newFolderName = ""; let newFolderName = "";
// Breadcrumb menü state
let breadcrumbContainer;
let showBreadcrumbMenu = false;
let breadcrumbMenuPosition = { top: 0, left: 0 };
let hiddenBreadcrumbs = [];
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const pathParam = params.get("path"); const pathParam = params.get("path");
@@ -521,6 +605,10 @@
if (isCreatingFolder && !creating) { if (isCreatingFolder && !creating) {
cancelCreateFolder(); cancelCreateFolder();
} }
const renaming = event.target.closest(".folder-rename-input");
if (renamingFolder && !renaming) {
cancelRenameFolder();
}
if (selectedItems.size === 0) return; if (selectedItems.size === 0) return;
const card = event.target.closest(".media-card"); const card = event.target.closest(".media-card");
const header = event.target.closest(".header-actions"); const header = event.target.closest(".header-actions");
@@ -1310,7 +1398,7 @@
cancelCreateFolder(); cancelCreateFolder();
} }
} }
function handleFolderKeydown(event) { function handleFolderKeydown(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
@@ -1320,6 +1408,73 @@
cancelCreateFolder(); 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 () => { onMount(async () => {
setSearchScope("files"); setSearchScope("files");
@@ -1327,6 +1482,25 @@
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
const wsUrl = `${API.replace("http", "ws")}?token=${token}`; const wsUrl = `${API.replace("http", "ws")}?token=${token}`;
const ws = new WebSocket(wsUrl); 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) => { const handlePopState = (event) => {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const statePath = 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) { if (msg.type === "progress" && msg.torrents) {
for (const t of msg.torrents) { for (const t of msg.torrents) {
const savePath = t.savePath || ""; const savePath = t.savePath || "";
@@ -1468,6 +1647,10 @@
cancelCreateFolder(); cancelCreateFolder();
handled = true; handled = true;
} }
if (renamingFolder) {
cancelRenameFolder();
handled = true;
}
if (activeMenu) { if (activeMenu) {
activeMenu = null; activeMenu = null;
handled = true; handled = true;
@@ -1550,6 +1733,9 @@
if (activeMenu && !event.target.closest(".media-card")) { if (activeMenu && !event.target.closest(".media-card")) {
activeMenu = null; activeMenu = null;
} }
if (showBreadcrumbMenu && !event.target.closest(".breadcrumb") && !event.target.closest(".breadcrumb-menu-portal")) {
closeBreadcrumbMenu();
}
} }
window.addEventListener("click", handleClickOutside); window.addEventListener("click", handleClickOutside);
@@ -1569,6 +1755,13 @@
window.removeEventListener("click", handleClickOutside); window.removeEventListener("click", handleClickOutside);
window.removeEventListener("popstate", handlePopState); window.removeEventListener("popstate", handlePopState);
// Resize observer'ı temizle
if (breadcrumbContainer) {
const resizeObserver = new ResizeObserver(() => {});
resizeObserver.disconnect();
}
try { try {
ws.close(); ws.close();
} catch (err) { } catch (err) {
@@ -1582,21 +1775,61 @@
<div class="files-header"> <div class="files-header">
<div class="header-title"> <div class="header-title">
<h2>Media Library</h2> <h2>Media Library</h2>
<div class="breadcrumb"> <div class="breadcrumb-container" bind:this={breadcrumbContainer}>
{#each breadcrumbs as crumb, index (index)} <div class="breadcrumb" class:has-overflow={showBreadcrumbMenu}>
{#if crumb.ellipsis} {#if showBreadcrumbMenu && hiddenBreadcrumbs.length > 0}
<span class="crumb ellipsis">...</span> <!-- İlk öğe (Home) -->
{:else}
<button <button
type="button" type="button"
class="crumb" class="crumb"
class:is-active={normalizePath(crumb.path) === normalizePath(currentPath)} class:is-active={normalizePath(breadcrumbs[0].path) === normalizePath(currentPath)}
on:click|stopPropagation={() => handleBreadcrumbClick(crumb)} on:click|stopPropagation={() => handleBreadcrumbClick(breadcrumbs[0])}
> >
{index === 0 ? "/Home" : `/${crumb.label}`} {breadcrumbs[0].label === "Home" ? "Home" : breadcrumbs[0].label}
</button> </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} {/if}
{/each} </div>
</div> </div>
</div> </div>
<div class="header-actions"> <div class="header-actions">
@@ -1697,7 +1930,20 @@
<img src={FOLDER_ICON_PATH} alt={`${entry.displayName} klasörü`} /> <img src={FOLDER_ICON_PATH} alt={`${entry.displayName} klasörü`} />
</div> </div>
<div class="folder-info"> <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> </div>
{:else} {:else}
{#if entry.thumbnail} {#if entry.thumbnail}
@@ -1833,6 +2079,13 @@
<i class="fa-solid fa-folder-open"></i> <i class="fa-solid fa-folder-open"></i>
<span></span> <span></span>
</button> </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> <div class="menu-divider"></div>
{:else} {:else}
<button <button
@@ -1861,6 +2114,28 @@
</div> </div>
{/if} {/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} {#if showModal && selectedVideo}
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-overlay" on:click={closeModal}> <div class="modal-overlay" on:click={closeModal}>
@@ -2302,13 +2577,22 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 4px; gap: 4px;
min-width: 0;
flex: 1;
}
.breadcrumb-container {
width: 100%;
overflow: hidden;
position: relative;
} }
.breadcrumb { .breadcrumb {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2px; gap: 6px;
font-size: 14px; font-size: 14px;
color: #757575; color: #757575;
white-space: nowrap;
overflow: hidden;
} }
.crumb { .crumb {
border: none; border: none;
@@ -2330,6 +2614,59 @@
color: #1f78ff; color: #1f78ff;
font-weight: 600; 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 { .crumb.ellipsis {
cursor: default; cursor: default;
color: #9b9b9b; color: #9b9b9b;
@@ -2432,14 +2769,13 @@
background: rgba(245, 179, 51, 0.1); background: rgba(245, 179, 51, 0.1);
border: 2px dashed var(--yellow); border: 2px dashed var(--yellow);
border-radius: 8px; border-radius: 8px;
padding: 18px 12px; padding: 12px 12px 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 18px; gap: 8px;
width: 100%; width: 100%;
min-height: 210px;
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
} }
@@ -2453,32 +2789,34 @@
.creating-folder .folder-thumb { .creating-folder .folder-thumb {
width: 100%; width: 100%;
height: 150px; height: 110px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex: 1;
} }
.creating-folder.list-view .folder-thumb { .creating-folder.list-view .folder-thumb {
width: 135px; width: 128px;
height: 135px; height: 128px;
} }
.creating-folder .folder-thumb img { .creating-folder .folder-thumb img {
width: 135px; width: 95px;
height: 135px; height: 95px;
object-fit: contain; object-fit: contain;
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16)); filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
} }
.creating-folder.list-view .folder-thumb img { .creating-folder.list-view .folder-thumb img {
width: 135px; width: 125px;
height: 135px; height: 125px;
} }
.creating-folder .folder-info { .creating-folder .folder-info {
width: 100%; width: 100%;
text-align: center; text-align: center;
flex-shrink: 0;
} }
.creating-folder.list-view .folder-info { .creating-folder.list-view .folder-info {
@@ -2512,8 +2850,8 @@
/* === GALERİ === */ /* === GALERİ === */
.gallery { .gallery {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px; gap: 12px;
} }
.gallery.list-view { .gallery.list-view {
display: flex; display: flex;
@@ -2536,8 +2874,7 @@
box-shadow 0.18s ease, box-shadow 0.18s ease,
flex-direction 0.3s cubic-bezier(0.4, 0, 0.2, 1), flex-direction 0.3s cubic-bezier(0.4, 0, 0.2, 1),
padding 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), gap 0.3s cubic-bezier(0.4, 0, 0.2, 1);
min-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer; cursor: pointer;
} }
.media-card::after { .media-card::after {
@@ -2578,7 +2915,7 @@
min-height: 96px; min-height: 96px;
} }
.media-card.list-view .thumb { .media-card.list-view .thumb {
width: 120px; width: 128px;
height: 72px; height: 72px;
border-radius: 8px; border-radius: 8px;
object-fit: cover; object-fit: cover;
@@ -2639,7 +2976,7 @@
} }
.thumb { .thumb {
width: 100%; width: 100%;
height: 150px; height: 110px;
object-fit: cover; object-fit: cover;
border-radius: 10px 10px 0 0; border-radius: 10px 10px 0 0;
transition: transition:
@@ -2655,7 +2992,7 @@
background: #ddd; background: #ddd;
} }
.info { .info {
padding: 10px; padding: 8px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
@@ -2867,7 +3204,7 @@
} }
.gallery { .gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
} }
.media-card.list-view { .media-card.list-view {
flex-direction: column; flex-direction: column;
@@ -2890,7 +3227,7 @@
} }
.gallery { .gallery {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
} }
} }
@@ -2928,8 +3265,8 @@
/* === GALERİ === */ /* === GALERİ === */
.gallery { .gallery {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px; gap: 12px;
} }
.gallery.list-view { .gallery.list-view {
display: flex; display: flex;
@@ -3037,7 +3374,7 @@
} }
.thumb { .thumb {
width: 100%; width: 100%;
height: 150px; height: 110px;
object-fit: cover; object-fit: cover;
} }
.thumb.placeholder { .thumb.placeholder {
@@ -3178,7 +3515,7 @@
/* === RESPONSIVE === */ /* === RESPONSIVE === */
@media (max-width: 768px) { @media (max-width: 768px) {
.gallery { .gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
} }
.media-card.list-view { .media-card.list-view {
flex-direction: column; flex-direction: column;
@@ -3194,7 +3531,7 @@
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.gallery { .gallery {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
} }
} }
/* Folder görünümü */ /* Folder görünümü */
@@ -3202,15 +3539,14 @@
background: transparent; background: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
padding: 24px 12px 18px; padding: 12px 12px 8px;
align-items: center; align-items: center;
min-height: 210px;
} }
.folder-card::after { .folder-card::after {
display: none; display: none;
} }
.folder-card:hover { .folder-card:hover {
background: transparent; background: rgba(0, 0, 0, 0.03);
border: none; border: none;
box-shadow: none; box-shadow: none;
} }
@@ -3223,21 +3559,23 @@
} }
.folder-thumb { .folder-thumb {
width: 100%; width: 100%;
height: 150px; height: 110px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex: 1;
} }
.folder-thumb img { .folder-thumb img {
width: 135px; width: 95px;
height: 135px; height: 95px;
object-fit: contain; object-fit: contain;
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16)); filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
} }
.folder-info { .folder-info {
margin-top: 14px; margin-top: 4px;
width: 100%; width: 100%;
text-align: center; text-align: center;
flex-shrink: 0;
} }
.folder-name { .folder-name {
font-weight: 600; font-weight: 600;
@@ -3246,9 +3584,24 @@
line-height: 1.35; line-height: 1.35;
word-break: break-word; 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:hover .folder-name,
.folder-card.is-selected .folder-name { .folder-card.is-selected .folder-name {
color: #1f78ff; color: #333;
} }
.folder-card.list-view { .folder-card.list-view {
flex-direction: row; flex-direction: row;
@@ -3258,8 +3611,8 @@
gap: 18px; gap: 18px;
} }
.folder-card.list-view .folder-thumb { .folder-card.list-view .folder-thumb {
width: 135px; width: 128px;
height: 135px; height: 128px;
} }
.folder-card.list-view .folder-info { .folder-card.list-view .folder-info {
margin-top: 0; margin-top: 0;

File diff suppressed because it is too large Load Diff

View File

@@ -280,8 +280,8 @@ let filteredShows = [];
const ext = name.split(".").pop()?.toLowerCase() || ""; const ext = name.split(".").pop()?.toLowerCase() || "";
const inferredType = ext ? `video/${ext}` : "video/mp4"; const inferredType = ext ? `video/${ext}` : "video/mp4";
const size = const size =
Number(episode.fileSize) ||
Number(episode.mediaInfo?.format?.size) || Number(episode.mediaInfo?.format?.size) ||
Number(episode.mediaInfo?.format?.bit_rate) ||
null; null;
return { return {
name, name,
@@ -300,9 +300,19 @@ let filteredShows = [];
) )
.filter(Boolean); .filter(Boolean);
const encodePathSegments = (value) =>
value
? value
.split(/[\\/]/)
.map((segment) => encodeURIComponent(segment))
.join("/")
: "";
$: selectedName = selectedVideo?.name ?? ""; $: selectedName = selectedVideo?.name ?? "";
$: encName = selectedName ? encodeURIComponent(selectedName) : ""; $: encName = encodePathSegments(selectedName);
$: downloadHref = encName ? `${API}/downloads/${encName}` : "#"; $: downloadHref = encName
? `${API}/downloads/${encName}?token=${localStorage.getItem("token") || ""}`
: "#";
$: selectedLabel = selectedVideo?.episode $: selectedLabel = selectedVideo?.episode
? `${selectedVideo.show.title} · ${formatEpisodeCode( ? `${selectedVideo.show.title} · ${formatEpisodeCode(
selectedVideo.episode selectedVideo.episode
@@ -414,8 +424,9 @@ async function openVideoAtIndex(index) {
function getVideoURL() { function getVideoURL() {
if (!selectedName) return ""; if (!selectedName) return "";
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
// selectedName zaten encode edilmiş, tekrar encode etme const encoded = encodePathSegments(selectedName);
return `${API}/media/${selectedName}?token=${token}`; if (!encoded) return "";
return `${API}/media/${encoded}?token=${token}`;
} }
function playEpisodeFromCard(episode) { function playEpisodeFromCard(episode) {

View 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;
}
}

View File

@@ -19,3 +19,36 @@ export async function apiFetch(path, options = {}) {
} }
return res; 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();
}

File diff suppressed because it is too large Load Diff