From 09b76bfc0d02166a5720d68955d8a8ae4e3b24b4 Mon Sep 17 00:00:00 2001 From: szbk Date: Thu, 30 Oct 2025 21:24:41 +0300 Subject: [PATCH] =?UTF-8?q?File=20browser=20=C3=B6zelli=C4=9Fi=20eklendi.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/routes/Files.svelte | 1528 ++++++++++++++++++++++++++++---- server/server.js | 113 +++ 2 files changed, 1466 insertions(+), 175 deletions(-) diff --git a/client/src/routes/Files.svelte b/client/src/routes/Files.svelte index 769ef29..b44b249 100644 --- a/client/src/routes/Files.svelte +++ b/client/src/routes/Files.svelte @@ -4,7 +4,271 @@ import { cleanFileName, extractTitleAndYear } from "../utils/filename.js"; import { refreshMovieCount } from "../stores/movieStore.js"; import { refreshTvShowCount } from "../stores/tvStore.js"; + const HIDDEN_ROOT_REGEX = /^\d{10,}$/; + const FOLDER_ICON_PATH = "/folder.svg"; + const MAX_TRAIL_SEGMENTS = 3; let files = []; + let currentPath = ""; + let currentOriginalPath = ""; + let visibleFolders = []; + let visibleFiles = []; + let visibleEntries = []; + let allDirectories = []; + let breadcrumbs = []; + let currentFileScope = []; + let pendingFolders = new Map(); + + const normalizePath = (value) => { + if (!value) return ""; + const trimmed = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""); + return trimmed; + }; + + const shouldHideRootSegment = (segment) => + segment && (HIDDEN_ROOT_REGEX.test(segment) || segment === "downloads"); + + function augmentFileEntry(file) { + const originalSegments = String(file.name || "") + .split(/[\\/]/) + .filter(Boolean); + + const hiddenRoot = + originalSegments.length > 0 && shouldHideRootSegment(originalSegments[0]) + ? originalSegments[0] + : null; + + const displaySegments = hiddenRoot + ? originalSegments.slice(1) + : originalSegments; + + const isDirectory = Boolean(file.isDirectory); + return { + ...file, + isDirectory, + hiddenRoot, + originalSegments, + displaySegments, + displayPath: displaySegments.join("/"), + displayName: displaySegments[displaySegments.length - 1] || file.name, + displayParentPath: displaySegments.slice(0, -1).join("/"), + }; + } + + function buildDirectoryEntries(fileList) { + const directories = new Map(); + + const ensureDirectoryEntry = (key, displayName, parentDisplayPath, originalPath) => { + if (!key) return; + if (!directories.has(key)) { + directories.set(key, { + id: `dir:${key}`, + name: `dir:${key}`, + displayName, + displayPath: key, + parentDisplayPath, + originalPaths: new Set(), + isDirectory: true, + }); + } + if (originalPath) { + directories.get(key).originalPaths.add(originalPath); + } + }; + + for (const file of fileList) { + const segments = file.displaySegments; + if (!segments || segments.length === 0) continue; + + const originalSegments = file.hiddenRoot + ? [file.hiddenRoot, ...segments] + : segments; + const fullOriginalPath = originalSegments.join("/"); + + if (file.isDirectory) { + const displayPath = segments.join("/"); + const parentDisplayPath = segments.slice(0, -1).join("/"); + const displayName = segments[segments.length - 1] || displayPath; + ensureDirectoryEntry(displayPath, displayName, parentDisplayPath, fullOriginalPath); + } + + if (segments.length <= 1) continue; + + const carryDisplay = []; + const carryOriginal = []; + + if (file.hiddenRoot) { + carryOriginal.push(file.hiddenRoot); + } + + for (let i = 0; i < segments.length - 1; i += 1) { + const seg = segments[i]; + carryDisplay.push(seg); + carryOriginal.push(seg); + + const displayPath = carryDisplay.join("/"); + const parentDisplayPath = carryDisplay.slice(0, -1).join("/"); + const originalPath = carryOriginal.join("/"); + ensureDirectoryEntry(displayPath, seg, parentDisplayPath, originalPath); + } + } + + return Array.from(directories.values()).map((dir) => { + const originalPaths = Array.from(dir.originalPaths); + return { + ...dir, + originalPaths, + primaryOriginalPath: originalPaths[0] || "", + }; + }); + } + + function computeBreadcrumbs(path) { + const segments = path ? path.split("/").filter(Boolean) : []; + const crumbs = [{ label: "Home", path: "" }]; + + if (segments.length === 0) { + return crumbs; + } + + if (segments.length <= MAX_TRAIL_SEGMENTS + 1) { + segments.forEach((seg, index) => { + const segPath = segments.slice(0, index + 1).join("/"); + crumbs.push({ label: seg, path: segPath }); + }); + 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 }); + }); + + return crumbs; + } + + function updateVisibleState(fileList, path = currentPath) { + const dirs = buildDirectoryEntries(fileList); + const directoryMap = new Map(); + dirs.forEach((dir) => { + const key = normalizePath(dir.displayPath); + directoryMap.set(key, dir); + }); + + const pendingRemoval = []; + pendingFolders.forEach((pending, key) => { + const normalizedKey = normalizePath(key); + if (!normalizedKey && normalizedKey !== "") return; + if (directoryMap.has(normalizedKey)) { + pendingRemoval.push(key); + return; + } + directoryMap.set(normalizedKey, { ...pending }); + }); + pendingRemoval.forEach((key) => pendingFolders.delete(key)); + + const directoryList = Array.from(directoryMap.values()); + allDirectories = directoryList; + visibleFolders = directoryList.filter( + (dir) => normalizePath(dir.parentDisplayPath) === normalizePath(path), + ); + visibleFolders.sort((a, b) => + a.displayName.localeCompare(b.displayName, "tr", { sensitivity: "base" }), + ); + visibleFiles = fileList.filter( + (file) => + !file.isDirectory && + normalizePath(file.displayParentPath) === normalizePath(path) && + file.displayName.toLowerCase() !== "info.js", + ); + visibleEntries = [...visibleFolders, ...visibleFiles]; + breadcrumbs = computeBreadcrumbs(path); + } + + function resolveOriginalPathForDisplay(displayPath, fallbackOriginal = currentOriginalPath) { + const normalizedDisplay = normalizePath(displayPath); + if (!normalizedDisplay) return ""; + + const directoryMatch = allDirectories.find( + (dir) => normalizePath(dir.displayPath) === normalizedDisplay, + ); + if (directoryMatch?.originalPaths?.length) { + return normalizePath(directoryMatch.originalPaths[0]); + } + + const fileMatch = files.find( + (file) => + normalizePath(file.displayParentPath) === normalizedDisplay || + normalizePath(file.displayPath) === normalizedDisplay, + ); + if (fileMatch) { + const effectiveSegments = + normalizedDisplay === normalizePath(fileMatch.displayParentPath) + ? fileMatch.displaySegments.slice(0, -1) + : fileMatch.displaySegments.slice(); + + const originalSegments = fileMatch.hiddenRoot + ? [fileMatch.hiddenRoot, ...effectiveSegments] + : effectiveSegments; + return normalizePath(originalSegments.join("/")); + } + + const normalizedFallback = normalizePath(fallbackOriginal); + if ( + normalizedDisplay && + normalizedFallback && + normalizedDisplay === normalizePath(currentPath) && + normalizedFallback + ) { + return normalizedFallback; + } + + if (normalizedDisplay && normalizedFallback) { + const fallbackSegments = normalizedFallback.split("/").filter(Boolean); + const displaySegments = normalizedDisplay.split("/").filter(Boolean); + if (fallbackSegments.length >= displaySegments.length) { + const [maybeRoot] = fallbackSegments; + if (HIDDEN_ROOT_REGEX.test(maybeRoot)) { + return normalizePath([maybeRoot, ...displaySegments].join("/")); + } + } + } + + return normalizedDisplay; + } + + function createPendingDirectoryEntry(displayPath, originalPath, label) { + const normalizedDisplay = normalizePath(displayPath); + const displaySegments = normalizedDisplay + ? normalizedDisplay.split("/").filter(Boolean) + : []; + const displayName = + label || displaySegments[displaySegments.length - 1] || normalizedDisplay || label || ""; + const parentDisplayPath = displaySegments.slice(0, -1).join("/"); + const normalizedOriginal = normalizePath(originalPath); + const originalSegments = normalizedOriginal + ? normalizedOriginal.split("/").filter(Boolean) + : []; + return { + id: `dir:${normalizedDisplay || displayName || Date.now()}`, + name: `dir:${normalizedDisplay || displayName || Date.now()}`, + displayName, + displayPath: normalizedDisplay, + parentDisplayPath, + originalPaths: normalizedOriginal ? [normalizedOriginal] : [], + primaryOriginalPath: normalizedOriginal, + isDirectory: true, + pending: true, + }; + } + + $: updateVisibleState(files, currentPath); let showModal = false; let selectedVideo = null; let subtitleURL = null; @@ -12,6 +276,7 @@ let subtitleLabel = "Custom Subtitles"; const VIEW_KEY = "filesViewMode"; let viewMode = "grid"; + let initialPath = ""; if (typeof window !== "undefined") { const storedView = window.localStorage.getItem(VIEW_KEY); if (storedView === "grid" || storedView === "list") { @@ -20,8 +285,13 @@ } let selectedItems = new Set(); let allSelected = false; + function syncSelectionState() { + const keys = visibleEntries.map((entry) => entry.name).filter(Boolean); + allSelected = keys.length > 0 && keys.every((key) => selectedItems.has(key)); + } + $: syncSelectionState(); let pendingPlayTarget = null; - let activeMenu = null; // Aktif menünün dosya adını tutar + let activeMenu = null; // Aktif menü öğesi let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu let showMatchModal = false; let matchingFile = null; @@ -32,8 +302,21 @@ let searching = false; let applyingMatch = false; let applyingResultId = null; // Sadece tıklanan öğeyi takip etmek için + + // Klasör oluşturma state + let isCreatingFolder = false; + let newFolderName = ""; + if (typeof window !== "undefined") { const params = new URLSearchParams(window.location.search); + const pathParam = params.get("path"); + if (pathParam) { + try { + initialPath = normalizePath(decodeURIComponent(pathParam)); + } catch (err) { + initialPath = normalizePath(pathParam); + } + } const playParam = params.get("play"); if (playParam) { try { @@ -42,11 +325,16 @@ pendingPlayTarget = playParam; } params.delete("play"); - const search = params.toString(); - const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`; - window.history.replaceState({}, "", newUrl); } + const search = params.toString(); + const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`; + window.history.replaceState( + { path: initialPath, originalPath: null }, + "", + newUrl, + ); } + currentPath = normalizePath(initialPath); // 🎬 Player kontrolleri let videoEl; let isPlaying = false; @@ -69,12 +357,29 @@ async function loadFiles() { const r = await apiFetch("/api/files"); if (!r.ok) return; - files = await r.json(); - const existing = new Set(files.map((f) => f.name)); + const rawFiles = await r.json(); + const processed = rawFiles + .map(augmentFileEntry) + .filter( + (f) => + f.displaySegments.length > 0 && + f.displayName && + f.displayName.toLowerCase() !== "info.js", + ); + files = processed; + updateVisibleState(processed, currentPath); + currentOriginalPath = resolveOriginalPathForDisplay( + currentPath, + currentOriginalPath, + ); + const folderKeys = new Set(visibleFolders.map((dir) => dir.name)); + const fileKeys = new Set(processed.map((f) => f.name)); + const existing = new Set([...folderKeys, ...fileKeys]); selectedItems = new Set( [...selectedItems].filter((name) => existing.has(name)), ); - allSelected = files.length > 0 && selectedItems.size === files.length; + activeMenu = null; + updateUrlPath(currentPath, currentOriginalPath, { replace: true }); tryAutoPlay(); refreshMovieCount(); refreshTvShowCount(); @@ -135,29 +440,185 @@ activeMenu = null; } function toggleSelection(file) { + if (!file?.name) return; const next = new Set(selectedItems); if (next.has(file.name)) next.delete(file.name); else next.add(file.name); selectedItems = next; - allSelected = files.length > 0 && next.size === files.length; } function selectAll() { if (allSelected) { selectedItems = new Set(); - allSelected = false; } else { - selectedItems = new Set(files.map((f) => f.name)); - allSelected = files.length > 0; + const keys = visibleEntries.map((entry) => entry.name).filter(Boolean); + selectedItems = new Set(keys); } } function handleFilesClick(event) { + const creating = event.target.closest(".creating-folder"); + if (isCreatingFolder && !creating) { + cancelCreateFolder(); + } if (selectedItems.size === 0) return; const card = event.target.closest(".media-card"); const header = event.target.closest(".header-actions"); if (header) return; if (card) return; selectedItems = new Set(); - allSelected = false; + } + + function updateUrlPath( + path, + originalPath = currentOriginalPath, + { replace = false } = {}, + ) { + if (typeof window === "undefined") return; + const params = new URLSearchParams(window.location.search); + const normalized = normalizePath(path); + const normalizedOriginal = normalizePath(originalPath); + if (normalized) params.set("path", normalized); + else params.delete("path"); + params.delete("play"); + const search = params.toString(); + const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`; + const state = { path: normalized, originalPath: normalizedOriginal }; + if (replace) window.history.replaceState(state, "", newUrl); + else window.history.pushState(state, "", newUrl); + } + + function navigateToPath(path, { replace = false, originalPath = null } = {}) { + const normalized = normalizePath(path); + const normalizedCurrent = normalizePath(currentPath); + const normalizedOriginal = + typeof originalPath === "string" + ? normalizePath(originalPath) + : resolveOriginalPathForDisplay(normalized); + if ( + normalized === normalizedCurrent && + normalizePath(currentOriginalPath) === normalizedOriginal + ) { + return; + } + currentPath = normalized; + currentOriginalPath = normalizedOriginal; + selectedItems = new Set(); + activeMenu = null; + if (isCreatingFolder) cancelCreateFolder(); + updateUrlPath(normalized, normalizedOriginal, { replace }); + } + + function handleEntryClick(entry) { + if (!entry) return; + if (entry.isDirectory) { + const original = + typeof entry.primaryOriginalPath === "string" && + entry.primaryOriginalPath.length > 0 + ? entry.primaryOriginalPath + : entry.originalPaths?.[0] || entry.displayPath; + navigateToPath(entry.displayPath, { originalPath: original }); + } else { + openModal(entry, visibleFiles); + } + } + + function handleBreadcrumbClick(crumb) { + if (!crumb || crumb.ellipsis) return; + if (crumb.path === null || crumb.path === undefined) return; + const original = + crumb.path && crumb.path.length > 0 + ? resolveOriginalPathForDisplay(crumb.path, currentOriginalPath) + : ""; + navigateToPath(crumb.path, { originalPath: original }); + } + + function getItemByName(name) { + if (!name) return null; + return ( + visibleEntries.find((entry) => entry.name === name) || + allDirectories.find((dir) => dir.name === name) || + files.find((file) => file.name === name) || + null + ); + } + + function resolveDeletionTargets(item) { + if (!item) return null; + if (item.isDirectory) { + const uniquePaths = Array.from( + new Set((item.originalPaths || []).filter(Boolean)), + ); + if (uniquePaths.length === 0) { + const fallbackOriginal = resolveOriginalPathForDisplay( + item.displayPath, + currentOriginalPath, + ); + if (fallbackOriginal) uniquePaths.push(fallbackOriginal); + } + if (uniquePaths.length === 0) return null; + const hashes = Array.from( + new Set( + uniquePaths + .map((path) => path.split("/")[0] || "") + .filter((hash) => HIDDEN_ROOT_REGEX.test(hash)), + ), + ); + return { + type: "directory", + label: item.displayPath || item.displayName || "", + paths: uniquePaths, + hashes, + }; + } + + if (!item.name) return null; + const path = item.name; + const hash = path.split("/")[0] || ""; + const hashes = HIDDEN_ROOT_REGEX.test(hash) ? [hash] : []; + return { + type: "file", + label: cleanFileName(path), + paths: [path], + hashes, + }; + } + + async function performDeletion(target) { + if (!target) return { ok: false, error: "Silinecek hedef bulunamadı." }; + const token = localStorage.getItem("token"); + const headers = { Authorization: `Bearer ${token}` }; + let lastError = null; + + for (const path of target.paths) { + try { + const resp = await fetch( + `${API}/api/file?path=${encodeURIComponent(path)}`, + { + method: "DELETE", + headers, + }, + ); + + if (!resp.ok) { + const data = await resp.json().catch(() => ({})); + lastError = data.error || resp.statusText || "Bilinmeyen hata"; + return { ok: false, error: lastError }; + } + } catch (err) { + lastError = err?.message || String(err); + return { ok: false, error: lastError }; + } + } + + await Promise.all( + (target.hashes || []).map((hash) => + fetch(`${API}/api/torrents/${hash}`, { + method: "DELETE", + headers, + }).catch(() => null), + ), + ); + + return { ok: true, error: null }; } function tryAutoPlay() { @@ -175,28 +636,39 @@ }) || null; if (candidate) { pendingPlayTarget = null; - openModal(candidate); + openModal(candidate, files); } } - async function openModal(f) { + async function openModal(f, scope = visibleFiles) { + if (!f) return; + if (f.isDirectory) { + navigateToPath(f.displayPath); + return; + } stopCurrentVideo(); videoEl = null; isPlaying = false; currentTime = 0; duration = 0; subtitleURL = null; // ← eklendi - const index = files.findIndex((file) => file.name === f.name); - currentIndex = index; - if (f.type?.startsWith("video/")) { + const pool = + scope && scope.length > 0 + ? scope.filter((item) => !item.isDirectory) + : files; + currentFileScope = pool; + const index = pool.findIndex((file) => file.name === f.name); + currentIndex = index >= 0 ? index : 0; + const target = pool[currentIndex] || f; + if (target.type?.startsWith("video/")) { selectedImage = null; showImageModal = false; - selectedVideo = f; + selectedVideo = target; await tick(); // DOM güncellensin showModal = true; // video {#key} ile yeniden mount edilecek - } else if (f.type?.startsWith("image/")) { + } else if (target.type?.startsWith("image/")) { selectedVideo = null; showModal = false; - selectedImage = f; + selectedImage = target; await tick(); showImageModal = true; } @@ -213,16 +685,24 @@ } } async function showNext() { - if (files.length === 0) return; + const pool = + currentFileScope && currentFileScope.length > 0 + ? currentFileScope + : visibleFiles.filter((item) => !item.isDirectory); + if (!pool || pool.length === 0) return; stopCurrentVideo(); - currentIndex = (currentIndex + 1) % files.length; - await openModal(files[currentIndex]); // ← await + currentIndex = (currentIndex + 1) % pool.length; + await openModal(pool[currentIndex], pool); // ← await } async function showPrev() { - if (files.length === 0) return; + const pool = + currentFileScope && currentFileScope.length > 0 + ? currentFileScope + : visibleFiles.filter((item) => !item.isDirectory); + if (!pool || pool.length === 0) return; stopCurrentVideo(); - currentIndex = (currentIndex - 1 + files.length) % files.length; - await openModal(files[currentIndex]); // ← await + currentIndex = (currentIndex - 1 + pool.length) % pool.length; + await openModal(pool[currentIndex], pool); // ← await } function closeModal() { stopCurrentVideo(); // 🔴 video tamamen durur @@ -310,59 +790,53 @@ if (!confirm(`${selectedItems.size} öğeyi silmek istediğine emin misin?`)) return; - const token = localStorage.getItem("token"); const names = [...selectedItems]; const failed = []; + const errors = []; for (const name of names) { - const file = files.find((f) => f.name === name); - if (!file) continue; + const item = getItemByName(name); + if (!item) continue; + const target = resolveDeletionTargets(item); + if (!target) continue; - try { - const resp = await fetch( - `${API}/api/file?path=${encodeURIComponent(file.name)}`, - { - method: "DELETE", - headers: { Authorization: `Bearer ${token}` }, - }, - ); - - if (!resp.ok) { - const data = await resp.json().catch(() => ({})); - alert("Silme hatası: " + (data.error || resp.statusText)); - failed.push(name); - continue; - } - - files = files.filter((f) => f.name !== file.name); - - const hash = file.name.split("/")[0]; - await fetch(`${API}/api/torrents/${hash}`, { - method: "DELETE", - headers: { Authorization: `Bearer ${token}` }, - }); - } catch (err) { - console.warn("⚠️ Silme işlemi başarısız:", err); + const result = await performDeletion(target); + if (!result.ok) { failed.push(name); + if (result.error) errors.push(result.error); + } else if (item.isDirectory) { + const displayKey = normalizePath( + item.displayPath || + (item.name?.startsWith("dir:") ? item.name.slice(4) : ""), + ); + if (displayKey || displayKey === "") { + pendingFolders.delete(displayKey); + } } } - selectedItems = new Set(failed); - allSelected = failed.length > 0 && failed.length === files.length; + await loadFiles(); await Promise.all([refreshMovieCount(), refreshTvShowCount()]); + + if (errors.length > 0) { + alert("Silme hatası: " + errors[0]); + } + + selectedItems = new Set(failed); + activeMenu = null; } // Menü fonksiyonları - function toggleMenu(fileName, event) { + function toggleMenu(item, event) { event.stopPropagation(); - - if (activeMenu === fileName) { + + if (activeMenu?.name === item.name) { activeMenu = null; return; } - - activeMenu = fileName; - + + activeMenu = item; + // Menü konumunu hesapla tick().then(() => { const button = event.currentTarget; @@ -398,6 +872,11 @@ } async function downloadFile(file) { + if (!file || file.isDirectory) { + if (file?.isDirectory) navigateToPath(file.displayPath); + closeMenu(); + return; + } const token = localStorage.getItem("token"); const link = document.createElement("a"); link.href = `${API}/downloads/${file.name}?token=${token}`; @@ -409,6 +888,10 @@ } function matchFile(file) { + if (!file || file.isDirectory) { + closeMenu(); + return; + } // Dosya adını al (path'in son kısmı) const fileName = file.name.split('/').pop(); @@ -543,50 +1026,175 @@ } } - async function deleteFile(file) { - if ( - !confirm( - `"${cleanFileName(file.name)}" dosyasını silmek istediğinizden emin misiniz?`, - ) - ) + async function deleteFile(item) { + if (!item) return; + const target = resolveDeletionTargets(item); + if (!target) { + closeMenu(); return; - - const token = localStorage.getItem("token"); - try { - const resp = await fetch( - `${API}/api/file?path=${encodeURIComponent(file.name)}`, - { - method: "DELETE", - headers: { Authorization: `Bearer ${token}` }, - }, - ); - - if (!resp.ok) { - const data = await resp.json().catch(() => ({})); - alert("Silme hatası: " + (data.error || resp.statusText)); - return; - } - - files = files.filter((f) => f.name !== file.name); - - const hash = file.name.split("/")[0]; - await fetch(`${API}/api/torrents/${hash}`, { - method: "DELETE", - headers: { Authorization: `Bearer ${token}` }, - }); - - await Promise.all([refreshMovieCount(), refreshTvShowCount()]); - } catch (err) { - console.warn("⚠️ Silme işlemi başarısız:", err); - alert("Silme işlemi başarısız oldu."); } + + const label = + target.type === "directory" + ? target.label || item.displayName || "Klasör" + : target.label || cleanFileName(item.name); + const message = + target.type === "directory" + ? `"${label}" klasörünü silmek istediğine emin misin?` + : `"${label}" dosyasını silmek istediğinizden emin misiniz?`; + + if (!confirm(message)) { + closeMenu(); + return; + } + + const result = await performDeletion(target); + if (!result.ok) { + alert("Silme hatası: " + (result.error || "Bilinmeyen hata")); + closeMenu(); + return; + } + if (item.isDirectory) { + const displayKey = normalizePath( + item.displayPath || + (item.name?.startsWith("dir:") ? item.name.slice(4) : ""), + ); + if (displayKey || displayKey === "") { + pendingFolders.delete(displayKey); + } + } + + await loadFiles(); + await Promise.all([refreshMovieCount(), refreshTvShowCount()]); + selectedItems = new Set( + [...selectedItems].filter((name) => name !== item.name), + ); closeMenu(); } + + // Klasör oluşturma fonksiyonları + function startCreateFolder() { + if (isCreatingFolder) return; + isCreatingFolder = true; + newFolderName = ""; + activeMenu = null; // Aktif menüyü kapat + } + + function cancelCreateFolder() { + isCreatingFolder = false; + newFolderName = ""; + } + + async function confirmCreateFolder() { + const folderName = newFolderName.trim(); + + if (!folderName) { + cancelCreateFolder(); + return; + } + + try { + const token = localStorage.getItem("token"); + const parentDisplayPath = normalizePath(currentPath); + const parentOriginalPath = resolveOriginalPathForDisplay( + currentPath, + currentOriginalPath, + ); + const normalizedParentOriginal = normalizePath(parentOriginalPath); + const targetPath = [normalizedParentOriginal, folderName] + .filter(Boolean) + .join("/"); + const displayPath = [parentDisplayPath, folderName] + .filter(Boolean) + .join("/"); + + // API'ye klasör oluşturma isteği gönder + const response = await apiFetch('/api/folder', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + name: folderName, + path: targetPath, + parentPath: normalizedParentOriginal, + displayPath, + parentDisplayPath + }) + }); + + if (response.ok) { + // Sadece mevcut dizine klasör oluştur, /downloads klasörüne oluşturma + // İzin sorunlarını önlemek için + const normalizedDisplayPath = normalizePath(displayPath); + const normalizedTargetPath = normalizePath(targetPath); + if (normalizedDisplayPath) { + pendingFolders.set( + normalizedDisplayPath, + createPendingDirectoryEntry(normalizedDisplayPath, normalizedTargetPath, folderName), + ); + updateVisibleState(files, currentPath); + } + + // Dosya listesini yenile + await loadFiles(); + updateUrlPath(currentPath, currentOriginalPath, { replace: true }); + } else { + const error = await response.json(); + alert('Klasör oluşturma hatası: ' + (error.error || response.statusText)); + } + } catch (err) { + console.error('Klasör oluşturma hatası:', err); + alert('Klasör oluşturulurken bir hata oluştu.'); + } finally { + cancelCreateFolder(); + } + } + + function handleFolderKeydown(event) { + if (event.key === 'Enter') { + event.preventDefault(); + confirmCreateFolder(); + } else if (event.key === 'Escape') { + event.preventDefault(); + cancelCreateFolder(); + } + } + onMount(async () => { await loadFiles(); // önce dosyaları getir const token = localStorage.getItem("token"); const wsUrl = `${API.replace("http", "ws")}?token=${token}`; const ws = new WebSocket(wsUrl); + const handlePopState = (event) => { + if (typeof window === "undefined") return; + const statePath = + event?.state && typeof event.state.path === "string" + ? event.state.path + : null; + const stateOriginal = + event?.state && typeof event.state.originalPath === "string" + ? event.state.originalPath + : null; + if (statePath !== null) { + currentPath = normalizePath(statePath); + } else { + const params = new URLSearchParams(window.location.search); + const paramPath = params.get("path"); + currentPath = normalizePath(paramPath || ""); + } + if (stateOriginal !== null) { + currentOriginalPath = normalizePath(stateOriginal); + } else { + currentOriginalPath = resolveOriginalPathForDisplay( + currentPath, + currentOriginalPath, + ); + } + selectedItems = new Set(); + activeMenu = null; + }; ws.onmessage = async (event) => { try { const msg = JSON.parse(event.data); @@ -608,7 +1216,7 @@ if (updatedFile) { // Sadece ilgili dosyayı güncelle const nextFiles = [...files]; - nextFiles[fileIndex] = updatedFile; + nextFiles[fileIndex] = augmentFileEntry(updatedFile); files = nextFiles; console.log("🔄 Dosya ikonu güncellendi:", msg.filePath); @@ -681,27 +1289,259 @@ } return; } - - const isCmd = e.metaKey || e.ctrlKey; - if (isCmd && e.key.toLowerCase() === "a") { - // Text input'larda çalıştırma - if (isEditable) return; - e.preventDefault(); - if (files.length > 0) { - selectedItems = new Set(files.map((f) => f.name)); - allSelected = true; + + if (e.key === "Escape") { + let handled = false; + if (showModal) { + closeModal(); + handled = true; + } + if (showImageModal) { + showImageModal = false; + handled = true; + } + if (selectedItems.size > 0) { + selectedItems = new Set(); + handled = true; + } + if (isCreatingFolder) { + cancelCreateFolder(); + handled = true; + } + if (activeMenu) { + activeMenu = null; + handled = true; + } + if (handled) { + e.preventDefault(); } return; } - if (e.key === "Escape") { - if (showModal) closeModal(); - if (showImageModal) showImageModal = false; - } else if (showModal || showImageModal) { + + const isCmd = e.metaKey || e.ctrlKey; + const normalizedKey = (e.key || "").toLowerCase(); + const isSelectAllKey = + isCmd && + (normalizedKey === "a" || e.code === "KeyA" || e.keyCode === 65); + + if (isSelectAllKey) { + // Text input'larda ve klasör oluşturma modunda çalıştırma + if (isEditable || isCreatingFolder) return; + + // Safari ve diğer tarayıcılarda default davranışı engelle + e.preventDefault(); + e.stopPropagation(); + if (typeof e.stopImmediatePropagation === "function") { + e.stopImmediatePropagation(); + } + + // Safari için ek güvenlik önlemleri + setTimeout(() => { + if (visibleEntries.length > 0) { + const allKeys = visibleEntries + .map( + (entry) => + entry?.name || + entry?.displayPath || + entry?.primaryOriginalPath || + null, + ) + .filter(Boolean); + if (allKeys.length > 0) { + selectedItems = new Set(allKeys); + if (isCreatingFolder) cancelCreateFolder(); + } + } + }, 0); + + return; + } + if (showModal || showImageModal) { if (e.key === "ArrowRight") showNext(); if (e.key === "ArrowLeft") showPrev(); } } - window.addEventListener("keydown", handleKey); + const keyListenerOptions = { capture: true, passive: false }; + const handleKeyUp = (e) => { + const normalizedKey = (e.key || "").toLowerCase(); + const isCmd = e.metaKey || e.ctrlKey; + const isSelectAllKey = + isCmd && + (normalizedKey === "a" || e.code === "KeyA" || e.keyCode === 65); + + if (isSelectAllKey) { + // Safari için güçlü event engelleme + e.preventDefault(); + e.stopPropagation(); + if (typeof e.stopImmediatePropagation === "function") { + e.stopImmediatePropagation(); + } + return false; + } + }; + const handleKeyPress = (e) => { + const normalizedKey = (e.key || "").toLowerCase(); + const isCmd = e.metaKey || e.ctrlKey; + const isSelectAllKey = + isCmd && + (normalizedKey === "a" || e.code === "KeyA" || e.keyCode === 65); + + if (isSelectAllKey) { + // Safari için güçlü event engelleme + e.preventDefault(); + e.stopPropagation(); + if (typeof e.stopImmediatePropagation === "function") { + e.stopImmediatePropagation(); + } + return false; + } + }; + + // Safari için özel keydown handler + const handleSafariKeyDown = (e) => { + const normalizedKey = (e.key || "").toLowerCase(); + const isCmd = e.metaKey || e.ctrlKey; + const isSelectAllKey = + isCmd && + (normalizedKey === "a" || e.code === "KeyA" || e.keyCode === 65); + + if (isSelectAllKey) { + const active = document.activeElement; + const tag = active?.tagName; + const type = active?.type?.toLowerCase(); + const isTextInput = + tag === "INPUT" && + [ + "text", + "search", + "email", + "password", + "number", + "url", + "tel", + ].includes(type); + const isEditable = + (tag === "TEXTAREA" || isTextInput || active?.isContentEditable) ?? + false; + + // Text input'larda ve klasör oluşturma modunda çalıştırma + if (isEditable || isCreatingFolder) return; + + // Safari için maksimum event kontrolü + e.preventDefault(); + e.stopPropagation(); + if (typeof e.stopImmediatePropagation === "function") { + e.stopImmediatePropagation(); + } + + // Async execution for Safari compatibility + setTimeout(() => { + if (visibleEntries.length > 0) { + const allKeys = visibleEntries + .map( + (entry) => + entry?.name || + entry?.displayPath || + entry?.primaryOriginalPath || + null, + ) + .filter(Boolean); + if (allKeys.length > 0) { + selectedItems = new Set(allKeys); + if (isCreatingFolder) cancelCreateFolder(); + } + } + }, 0); + + return false; + } + }; + + // Safari detection + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + const keyTargets = [window, document, document.body].filter(Boolean); + + // Normal event listeners (tüm tarayıcılar için) + keyTargets.forEach((target) => + target.addEventListener("keydown", handleKey, keyListenerOptions), + ); + keyTargets.forEach((target) => + target.addEventListener("keyup", handleKeyUp, keyListenerOptions), + ); + keyTargets.forEach((target) => + target.addEventListener("keypress", handleKeyPress, keyListenerOptions), + ); + + // Safari için ek event listeners + if (isSafari) { + keyTargets.forEach((target) => + target.addEventListener("keydown", handleSafariKeyDown, { capture: true }), + ); + + // Safari'de document-level keydown için ek listener + document.addEventListener("keydown", handleSafariKeyDown, { + capture: true, + passive: false + }); + + // Safari için window-level global handler + window.addEventListener("keydown", (e) => { + const normalizedKey = (e.key || "").toLowerCase(); + const isCmd = e.metaKey || e.ctrlKey; + const isSelectAllKey = + isCmd && + (normalizedKey === "a" || e.code === "KeyA" || e.keyCode === 65); + + if (isSelectAllKey) { + e.preventDefault(); + e.stopPropagation(); + if (typeof e.stopImmediatePropagation === "function") { + e.stopImmediatePropagation(); + } + + // Maksimum uyumluluk için gecikme + setTimeout(() => { + const active = document.activeElement; + const tag = active?.tagName; + const type = active?.type?.toLowerCase(); + const isTextInput = + tag === "INPUT" && + [ + "text", + "search", + "email", + "password", + "number", + "url", + "tel", + ].includes(type); + const isEditable = + (tag === "TEXTAREA" || isTextInput || active?.isContentEditable) ?? + false; + + if (!isEditable && !isCreatingFolder && visibleEntries.length > 0) { + const allKeys = visibleEntries + .map( + (entry) => + entry?.name || + entry?.displayPath || + entry?.primaryOriginalPath || + null, + ) + .filter(Boolean); + if (allKeys.length > 0) { + selectedItems = new Set(allKeys); + if (isCreatingFolder) cancelCreateFolder(); + } + } + }, 10); + + return false; + } + }, { capture: true, passive: false }); + } + + window.addEventListener("popstate", handlePopState); // Menüyü kapatmak için dışarı tıklama olayı function handleClickOutside(event) { @@ -713,8 +1553,39 @@ window.addEventListener("click", handleClickOutside); return () => { - window.removeEventListener("keydown", handleKey); + // Normal event listeners temizliği + keyTargets.forEach((target) => + target.removeEventListener("keydown", handleKey, keyListenerOptions), + ); + keyTargets.forEach((target) => + target.removeEventListener("keyup", handleKeyUp, keyListenerOptions), + ); + keyTargets.forEach((target) => + target.removeEventListener("keypress", handleKeyPress, keyListenerOptions), + ); + + // Safari için ek event listeners temizliği + if (isSafari) { + keyTargets.forEach((target) => + target.removeEventListener("keydown", handleSafariKeyDown, { capture: true }), + ); + + document.removeEventListener("keydown", handleSafariKeyDown, { + capture: true, + passive: false + }); + + // Safari global window handler temizliği + window.removeEventListener("keydown", () => {}, { capture: true }); + } + window.removeEventListener("click", handleClickOutside); + window.removeEventListener("popstate", handlePopState); + try { + ws.close(); + } catch (err) { + console.warn("WebSocket kapatılırken hata:", err); + } }; }); @@ -723,12 +1594,28 @@

Media Library

+
- {#if files.length > 0 && selectedItems.size > 0} - {selectedItems.size} dosya seçildi + {#if visibleEntries.length > 0 && selectedItems.size > 0} + {selectedItems.size} öğe seçildi {/if} - {#if files.length > 0 && selectedItems.size > 0} + {#if visibleEntries.length > 0 && selectedItems.size > 0} {/if} +
- {#if files.length === 0} + {#if visibleEntries.length === 0 && !isCreatingFolder}
No media found
{:else}