Files
dupe/client/src/routes/Files.svelte
wisecolt 569a7975de refactor(files): konum senkronizasyonunu sağlamlaştır
URL tabanlı konum yönetimini tek bir fonksiyon altında toplayarak
tarayıcı navigasyonu ve history API olaylarının tutarlı şekilde işlenmesini
sağla. pushState ve replaceState metodlarını patch ederek özel locationchange
olayı oluşturur ve bileşen yok edildiğinde patch işlemini geri alır.
2026-01-31 10:36:07 +03:00

3970 lines
110 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
import { onDestroy, onMount, tick } from "svelte";
import MatchModal from "../components/MatchModal.svelte";
import { API, apiFetch, moveEntry, renameFolder, copyEntry } from "../utils/api.js";
import { cleanFileName, extractTitleAndYear } from "../utils/filename.js";
import { refreshMovieCount } from "../stores/movieStore.js";
import { refreshTvShowCount } from "../stores/tvStore.js";
import { fetchTrashItems } from "../stores/trashStore.js";
import {
activeSearchTerm,
setSearchScope,
clearSearch
} from "../stores/searchStore.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 renderedEntries = [];
let allDirectories = [];
let breadcrumbs = [];
let currentFileScope = [];
let pendingFolders = new Map();
let customOrder = new Map();
let draggingItem = null;
let dragOverItem = null;
let lastDragPath = "";
let searchTerm = "";
let hasSearch = false;
let renamingFolder = null;
let renameValue = "";
let renameInput;
const normalizePath = (value) => {
if (!value) return "";
const trimmed = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
return trimmed;
};
const IS_SAFARI =
typeof navigator !== "undefined" &&
/safari/i.test(navigator.userAgent || "") &&
!/chrome|crios|android/i.test(navigator.userAgent || "");
function filterEntriesBySearch(entries, term) {
const query = term.trim().toLowerCase();
if (!query) return entries;
return entries.filter((entry) => {
const labels = [
entry.displayName,
entry.displayPath,
entry.name
]
.filter(Boolean)
.join(" ")
.toLowerCase();
return labels.includes(query);
});
}
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);
const baseDisplayName =
file.displayName ||
displaySegments[displaySegments.length - 1] ||
file.name;
return {
...file,
isDirectory,
mediaCategory: file.mediaCategory || null,
hiddenRoot,
originalSegments,
displaySegments,
displayPath: displaySegments.join("/"),
displayName: baseDisplayName,
displayParentPath: displaySegments.slice(0, -1).join("/"),
};
}
function displayFileName(entry) {
if (!entry?.name) return "";
if (entry.isDirectory) {
const dirLabel = entry.displayName || entry.name;
return cleanFileName(dirLabel);
}
const baseName = entry.name.split("/").pop() || "";
const withoutExt = baseName.replace(/\.[^.]+$/, "");
if (/S\d{1,2}xE\d{1,2}/i.test(withoutExt)) {
return withoutExt;
}
return cleanFileName(entry.name);
}
function buildDirectoryEntries(fileList) {
const directories = new Map();
const ensureDirectoryEntry = (
key,
displayName,
parentDisplayPath,
originalPath,
mediaCategory
) => {
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,
mediaCategory: mediaCategory || null
});
}
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 = file.displayName || segments[segments.length - 1] || displayPath;
ensureDirectoryEntry(
displayPath,
displayName,
parentDisplayPath,
fullOriginalPath,
file.mediaCategory
);
}
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 applyOrdering(path) {
const key = normalizePath(path);
const combined = [...visibleFolders, ...visibleFiles];
const order = customOrder.get(key);
if (!order) {
visibleEntries = combined;
return;
}
const map = new Map(combined.map((item) => [item.name, item]));
const ordered = [];
const filteredOrder = [];
for (const name of order) {
const item = map.get(name);
if (!item) continue;
ordered.push(item);
filteredOrder.push(name);
map.delete(name);
}
for (const item of map.values()) {
ordered.push(item);
filteredOrder.push(item.name);
}
visibleEntries = ordered;
customOrder.set(key, filteredOrder);
}
$: searchTerm = $activeSearchTerm;
$: hasSearch = searchTerm.trim().length > 0;
$: renderedEntries = filterEntriesBySearch(visibleEntries, searchTerm);
function computeBreadcrumbs(path) {
const segments = path ? path.split("/").filter(Boolean) : [];
const crumbs = [{ label: "Home", path: "" }];
if (segments.length === 0) {
return crumbs;
}
// 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 });
});
return crumbs;
}
// 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);
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",
);
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) {
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;
let subtitleLang = "en";
let subtitleLabel = "Custom Subtitles";
const VIEW_KEY = "filesViewMode";
let viewMode = "grid";
let initialPath = "";
let unpatchHistory = null;
if (typeof window !== "undefined") {
const storedView = window.localStorage.getItem(VIEW_KEY);
if (storedView === "grid" || storedView === "list") {
viewMode = storedView;
}
}
let selectedItems = new Set();
let allSelected = false;
$: {
const keys = renderedEntries.map((entry) => entry.name).filter(Boolean);
allSelected = keys.length > 0 && keys.every((key) => selectedItems.has(key));
}
let pendingPlayTarget = null;
let activeMenu = null; // Aktif menü öğesi
let menuPosition = { top: 0, left: 0 }; // Menü pozisyonu
let showMatchModal = false;
let matchingFile = null;
let matchTitle = "";
let matchYear = "";
let matchType = ""; // "movie" veya "series"
let searchResults = [];
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 = "";
// Breadcrumb menü state
let breadcrumbContainer;
let showBreadcrumbMenu = false;
let breadcrumbMenuPosition = { top: 0, left: 0 };
let hiddenBreadcrumbs = [];
// Clipboard state
let clipboardItem = null;
let clipboardOperation = null; // 'cut' veya 'copy'
const syncFromLocation = ({ replace = false } = {}) => {
if (typeof window === "undefined") return;
const params = new URLSearchParams(window.location.search);
const pathParam = params.get("path");
let nextPath = "";
if (pathParam) {
try {
nextPath = normalizePath(decodeURIComponent(pathParam));
} catch (err) {
nextPath = normalizePath(pathParam);
}
}
const playParam = params.get("play");
if (playParam) {
try {
pendingPlayTarget = decodeURIComponent(playParam);
} catch (err) {
pendingPlayTarget = playParam;
}
params.delete("play");
const search = params.toString();
const newUrl = `${window.location.pathname}${search ? `?${search}` : ""}${window.location.hash}`;
window.history.replaceState(
{ path: nextPath, originalPath: null },
"",
newUrl,
);
}
const state = window.history.state || {};
const nextOriginal =
typeof state.originalPath === "string"
? normalizePath(state.originalPath)
: resolveOriginalPathForDisplay(nextPath);
if (
normalizePath(currentPath) === nextPath &&
normalizePath(currentOriginalPath) === nextOriginal
) {
return;
}
const hadSearch = searchTerm.trim().length > 0;
const nextSearchTerm = hadSearch ? "" : searchTerm;
currentPath = nextPath;
currentOriginalPath = nextOriginal;
if (hadSearch) {
clearSearch("files");
}
selectedItems = new Set();
activeMenu = null;
if (isCreatingFolder) cancelCreateFolder();
updateVisibleState(files, nextPath);
renderedEntries = filterEntriesBySearch(visibleEntries, nextSearchTerm);
if (replace) {
updateUrlPath(nextPath, nextOriginal, { replace: true });
}
};
if (typeof window !== "undefined") {
syncFromLocation({ replace: true });
initialPath = currentPath;
}
// 🎬 Player kontrolleri
let videoEl;
let isPlaying = false;
let currentTime = 0;
let duration = 0;
let volume = 1;
let currentIndex;
let showImageModal = false;
let selectedImage = null;
// ✅ REACTIVE: selectedVideo güvenli kullanımlar
$: selectedName = selectedVideo?.name ?? "";
$: encName = encodeURIComponent(selectedName);
// ✅ Token'lı video URL'ini fonksiyonla üret (başta çağrılmasın)
function getVideoURL() {
if (!selectedName) return "";
const token = localStorage.getItem("token");
return `${API}/media/${encName}?token=${token}`;
}
// 📂 Dosyaları yükle (tokenlı)
async function loadFiles() {
const r = await apiFetch("/api/files");
if (!r.ok) return;
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)),
);
activeMenu = null;
updateUrlPath(currentPath, currentOriginalPath, { replace: true });
tryAutoPlay();
refreshMovieCount();
refreshTvShowCount();
}
function formatSize(bytes) {
if (!bytes) return "0 MB";
if (bytes < 1e6) return (bytes / 1e3).toFixed(1) + " KB";
if (bytes < 1e9) return (bytes / 1e6).toFixed(1) + " MB";
return (bytes / 1e9).toFixed(2) + " GB";
}
function formatTracker(value) {
if (!value) return "Bilinmiyor";
const raw = String(value).trim();
if (!raw) return "Bilinmiyor";
try {
const url = new URL(raw);
const host = url.hostname.replace(/^tracker\./i, "");
return host || raw;
} catch (err) {
const stripped = raw.replace(/^.*?:\/\//, "").replace(/\/.*$/, "");
return stripped.replace(/^tracker\./i, "") || raw;
}
}
function formatDateTime(value) {
if (!value) return "—";
const date = new Date(Number(value));
if (Number.isNaN(date.getTime())) return "—";
return date.toLocaleString();
}
function formatVideoCodec(info) {
if (!info) return null;
const codec = info.codec ? info.codec.toUpperCase() : null;
const resolution =
info.resolution || (info.height ? `${info.height}p` : null);
return [codec, resolution].filter(Boolean).join(" · ");
}
function formatAudioCodec(info) {
if (!info) return null;
const codec = info.codec ? info.codec.toUpperCase() : null;
let channels = null;
if (info.channelLayout) channels = info.channelLayout.toUpperCase();
else if (info.channels) {
channels =
info.channels === 6
? "5.1"
: info.channels === 2
? "2.0"
: `${info.channels}`;
}
return [codec, channels].filter(Boolean).join(" · ");
}
function toggleView() {
viewMode = viewMode === "grid" ? "list" : "grid";
if (typeof window !== "undefined") {
window.localStorage.setItem(VIEW_KEY, viewMode);
}
// Görünüm değiştiğinde menüyü kapat
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;
}
function selectAll() {
if (allSelected) {
selectedItems = new Set();
} else {
const keys = renderedEntries.map((entry) => entry.name).filter(Boolean);
selectedItems = new Set(keys);
}
}
function handleFilesClick(event) {
const creating = event.target.closest(".creating-folder");
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");
if (header) return;
if (card) return;
selectedItems = new Set();
}
function resolveEntryOriginalPath(entry) {
if (!entry) return "";
if (entry.isDirectory) {
const original =
entry.primaryOriginalPath ||
(Array.isArray(entry.originalPaths) ? entry.originalPaths[0] : null) ||
resolveOriginalPathForDisplay(entry.displayPath, currentOriginalPath);
return normalizePath(original);
}
return normalizePath(entry?.name);
}
function clearDragState() {
draggingItem = null;
dragOverItem = null;
}
function handleDragStart(entry, event) {
draggingItem = entry;
dragOverItem = null;
lastDragPath = normalizePath(currentPath);
if (event?.dataTransfer) {
try {
event.dataTransfer.setData("text/plain", entry.name);
} catch (err) {
// ignore
}
event.dataTransfer.effectAllowed = "move";
}
}
function handleDragOver(entry, event) {
if (!draggingItem) return;
if (normalizePath(currentPath) !== lastDragPath) return;
if (draggingItem.name === entry.name) return;
event.preventDefault();
if (event?.dataTransfer) event.dataTransfer.dropEffect = "move";
dragOverItem = entry;
}
function handleDragLeave(entry) {
if (dragOverItem && dragOverItem.name === entry.name) {
dragOverItem = null;
}
}
async function handleDrop(entry, event) {
if (!draggingItem) return;
if (normalizePath(currentPath) !== lastDragPath) {
clearDragState();
return;
}
event.preventDefault();
event.stopPropagation();
const source = draggingItem;
clearDragState();
if (!source || !entry) return;
if (entry.isDirectory) {
if (source.name === entry.name) return;
await moveEntryToDirectory(source, entry);
return;
}
reorderEntries(source, entry);
}
function handleDragEnd() {
clearDragState();
}
function handleContainerDragOver(event) {
if (!draggingItem) return;
if (normalizePath(currentPath) !== lastDragPath) return;
event.preventDefault();
}
function handleContainerDrop(event) {
if (!draggingItem) return;
if (normalizePath(currentPath) !== lastDragPath) {
clearDragState();
return;
}
event.preventDefault();
event.stopPropagation();
const key = normalizePath(currentPath);
const currentOrder =
customOrder.get(key) || visibleEntries.map((item) => item.name);
const filtered = currentOrder.filter((name) => name !== draggingItem.name);
filtered.push(draggingItem.name);
customOrder.set(key, filtered);
applyOrdering(currentPath);
clearDragState();
}
function reorderEntries(source, target) {
const key = normalizePath(currentPath);
const currentOrder =
customOrder.get(key) || visibleEntries.map((item) => item.name);
const sourceIndex = currentOrder.indexOf(source.name);
const targetIndex = currentOrder.indexOf(target.name);
if (sourceIndex === -1 || targetIndex === -1) return;
const updatedOrder = [...currentOrder];
updatedOrder.splice(sourceIndex, 1);
const newTargetIndex = updatedOrder.indexOf(target.name);
if (newTargetIndex === -1) return;
updatedOrder.splice(newTargetIndex, 0, source.name);
customOrder.set(key, updatedOrder);
applyOrdering(currentPath);
}
async function moveEntryToDirectory(source, target) {
const sourcePath = resolveEntryOriginalPath(source);
const targetPath = resolveEntryOriginalPath(target);
if (!sourcePath || !targetPath) return;
const normalizedSource = normalizePath(sourcePath);
const normalizedTarget = normalizePath(targetPath);
if (!normalizedSource || !normalizedTarget) return;
if (source?.isDirectory) {
if (
normalizedTarget === normalizedSource ||
normalizedTarget.startsWith(`${normalizedSource}/`)
) {
alert("Bir klasörü kendi içine taşıyamazsın.");
return;
}
}
const parentOfSource = normalizedSource.split("/").slice(0, -1).join("/");
if (parentOfSource === normalizedTarget) {
return;
}
const sourceLabel = cleanFileName(source.displayName || source.name || normalizedSource);
const targetLabel = cleanFileName(target.displayName || target.name || normalizedTarget);
if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" içine taşımak istiyor musun?`)) {
return;
}
try {
const result = await moveEntry(normalizedSource, normalizedTarget);
if (!result?.success) {
const message =
result?.error || "Öğe taşınırken bir hata oluştu.";
alert(message);
return;
}
if (!result?.unchanged) {
await loadFiles();
}
selectedItems = new Set();
} catch (err) {
console.error("❌ Taşıma hatası:", err);
alert("Öğe taşınamadı. Lütfen tekrar dene.");
}
}
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;
}
const hadSearch = searchTerm.trim().length > 0;
const nextSearchTerm = hadSearch ? "" : searchTerm;
currentPath = normalized;
currentOriginalPath = normalizedOriginal;
if (hadSearch) {
clearSearch("files");
}
selectedItems = new Set();
activeMenu = null;
if (isCreatingFolder) cancelCreateFolder();
updateUrlPath(normalized, normalizedOriginal, { replace });
updateVisibleState(files, normalized);
renderedEntries = filterEntriesBySearch(visibleEntries, nextSearchTerm);
}
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() {
if (!pendingPlayTarget || files.length === 0) return;
const normalizedTarget = pendingPlayTarget
.replace(/^\.?\//, "")
.replace(/\\/g, "/");
const candidate =
files.find((f) => {
const normalizedName = f.name.replace(/^\.?\//, "").replace(/\\/g, "/");
return (
normalizedName === normalizedTarget ||
normalizedName.endsWith(normalizedTarget)
);
}) || null;
if (candidate) {
pendingPlayTarget = null;
openModal(candidate, files);
}
}
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 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 = target;
await tick(); // DOM güncellensin
showModal = true; // video {#key} ile yeniden mount edilecek
} else if (target.type?.startsWith("image/")) {
selectedVideo = null;
showModal = false;
selectedImage = target;
await tick();
showImageModal = true;
}
}
function stopCurrentVideo() {
if (videoEl) {
try {
videoEl.pause();
videoEl.src = "";
videoEl.load();
} catch (err) {
console.warn("Video stop error:", err.message);
}
}
}
async function showNext() {
const pool =
currentFileScope && currentFileScope.length > 0
? currentFileScope
: visibleFiles.filter((item) => !item.isDirectory);
if (!pool || pool.length === 0) return;
stopCurrentVideo();
currentIndex = (currentIndex + 1) % pool.length;
await openModal(pool[currentIndex], pool); // ← await
}
async function showPrev() {
const pool =
currentFileScope && currentFileScope.length > 0
? currentFileScope
: visibleFiles.filter((item) => !item.isDirectory);
if (!pool || pool.length === 0) return;
stopCurrentVideo();
currentIndex = (currentIndex - 1 + pool.length) % pool.length;
await openModal(pool[currentIndex], pool); // ← await
}
function closeModal() {
stopCurrentVideo(); // 🔴 video tamamen durur
showModal = false;
selectedVideo = null;
subtitleURL = null;
isPlaying = false;
}
// 🎞️ Video kontrolleri
async function togglePlay() {
if (!videoEl) return;
if (videoEl.paused) {
try {
await videoEl.play();
isPlaying = true;
} catch (err) {
console.warn("Play rejected:", err?.message || err);
isPlaying = false;
}
} else {
videoEl.pause();
isPlaying = false;
}
}
function updateProgress() {
currentTime = videoEl?.currentTime || 0;
}
function updateDuration() {
duration = videoEl?.duration || 0;
}
function seekVideo(e) {
if (!videoEl) return;
const newTime = parseFloat(e.target.value);
if (Math.abs(videoEl.currentTime - newTime) > 0.2) {
videoEl.currentTime = newTime;
}
}
function changeVolume(e) {
if (!videoEl) return;
const val = parseFloat(e.target.value);
videoEl.volume = val;
e.target.style.setProperty("--fill", (val || 0) * 100);
}
function toggleFullscreen() {
if (!videoEl) return;
if (document.fullscreenElement) document.exitFullscreen();
else videoEl.requestFullscreen();
}
function formatTime(seconds) {
const m = Math.floor(seconds / 60)
.toString()
.padStart(2, "0");
const s = Math.floor(seconds % 60)
.toString()
.padStart(2, "0");
return `${m}:${s}`;
}
function handleSubtitleUpload(e) {
const file = e.target.files?.[0];
if (!file) return;
const ext = file.name.split(".").pop().toLowerCase();
const reader = new FileReader();
reader.onload = (ev) => {
const decoder = new TextDecoder("utf-8");
const content =
typeof ev.target.result === "string"
? ev.target.result
: decoder.decode(ev.target.result);
if (ext === "srt") {
const vttText =
"\uFEFFWEBVTT\n\n" + content.replace(/\r+/g, "").replace(/,/g, ".");
const blob = new Blob([vttText], { type: "text/vtt;charset=utf-8" });
subtitleURL = URL.createObjectURL(blob);
} else if (ext === "vtt") {
const blob = new Blob([content], { type: "text/vtt;charset=utf-8" });
subtitleURL = URL.createObjectURL(blob);
} else {
alert("Yalnızca .srt veya .vtt dosyaları destekleniyor.");
}
};
reader.readAsArrayBuffer(file);
}
async function deleteSelectedFiles() {
if (selectedItems.size === 0) return;
if (!confirm(`${selectedItems.size} öğeyi silmek istediğine emin misin?`))
return;
const names = [...selectedItems];
const failed = [];
const errors = [];
for (const name of names) {
const item = getItemByName(name);
if (!item) continue;
const target = resolveDeletionTargets(item);
if (!target) continue;
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);
}
}
}
await loadFiles();
await Promise.all([refreshMovieCount(), refreshTvShowCount(), fetchTrashItems()]);
if (errors.length > 0) {
alert("Silme hatası: " + errors[0]);
}
selectedItems = new Set(failed);
activeMenu = null;
}
// Menü fonksiyonları
function toggleMenu(item, event) {
event.stopPropagation();
if (activeMenu?.name === item.name) {
activeMenu = null;
return;
}
activeMenu = item;
// Menü konumunu hesapla
tick().then(() => {
const button = event.currentTarget;
const rect = button.getBoundingClientRect();
const menuWidth = 160;
const menuHeight = 140; // Yaklaşık menü yüksekliği
// Üç noktanın son noktası ile menünün sol kenarını hizala
// Düğme genişliği 34px, son nokta sağ kenara yakın
let top = rect.bottom + 4;
let left = rect.right - 8; // Son noktadan 8px sola
// Ekran sınırlarını kontrol et
if (left + menuWidth > window.innerWidth) {
// Sağda yer yoksa, menüyü sola kaydır
left = window.innerWidth - menuWidth - 12;
}
if (left < 0) {
left = 12;
}
if (top + menuHeight > window.innerHeight) {
top = rect.top - menuHeight - 4;
}
menuPosition = { top, left };
});
}
function closeMenu() {
activeMenu = null;
}
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}`;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
closeMenu();
}
function matchFile(file) {
if (!file || file.isDirectory) {
closeMenu();
return;
}
// Dosya adını al (path'in son kısmı)
const fileName = file.name.split('/').pop();
// Önce dizi kontrolü yap (SxxExx formatı)
const seriesMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (seriesMatch) {
matchType = "series";
const { title, year } = extractTitleAndYear(fileName);
matchTitle = title || fileName;
matchYear = year ? String(year) : "";
} else {
// Film kontrolü (yıl bilgisi)
const { title, year } = extractTitleAndYear(fileName);
if (year && year >= 1900 && year <= 2099) {
matchType = "movie";
matchTitle = title || fileName;
matchYear = String(year);
} else {
// Varsayılan olarak film kabul et
matchType = "movie";
matchTitle = title || fileName;
matchYear = "";
}
}
matchingFile = file;
showMatchModal = true;
closeMenu();
// Modal açıldıktan sonra otomatik arama yap
tick().then(() => {
searchMetadata();
});
}
function closeMatchModal() {
showMatchModal = false;
matchingFile = null;
matchTitle = "";
matchYear = "";
matchType = "";
searchResults = [];
searching = false;
applyingMatch = false;
applyingResultId = null;
}
async function searchMetadata() {
if (!matchTitle.trim()) return;
searching = true;
searchResults = [];
try {
const params = new URLSearchParams({
query: matchTitle,
type: matchType
});
if (matchYear) {
params.set("year", matchYear);
}
const response = await apiFetch(`/api/search/metadata?${params}`);
if (response.ok) {
const data = await response.json();
searchResults = data.results || [];
}
} catch (err) {
console.error("Arama hatası:", err);
} finally {
searching = false;
}
}
// Debounce için timer
let searchTimer;
function handleSearchInput() {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
searchMetadata();
}, 500);
}
async function selectMatchResult(result) {
if (!matchingFile || !result || applyingMatch) return;
applyingMatch = true;
applyingResultId = result.id; // Sadece tıklanan öğeyi işaretle
try {
// Dosya adından sezon/bölüm bilgilerini çıkar (dizi için)
const fileName = matchingFile.name.split('/').pop();
const seriesMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
const response = await apiFetch('/api/match/manual', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
filePath: matchingFile.name,
metadata: result,
type: matchType,
season: seriesMatch ? parseInt(seriesMatch[1]) : null,
episode: seriesMatch ? parseInt(seriesMatch[2]) : null
})
});
if (response.ok) {
const data = await response.json();
console.log('Manual match successful:', data);
// Modalı kapat
closeMatchModal();
// WebSocket üzerinden güncelleme gelecek, beklemek için kısa bir gecikme
await new Promise(resolve => setTimeout(resolve, 300));
} else {
const error = await response.json();
alert('Eşleştirme hatası: ' + (error.error || response.statusText));
}
} catch (err) {
console.error('Manual match error:', err);
alert('Eşleştirme sırasında bir hata oluştu.');
} finally {
applyingMatch = false;
applyingResultId = null;
}
}
async function deleteFile(item) {
if (!item) return;
const target = resolveDeletionTargets(item);
if (!target) {
closeMenu();
return;
}
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(), fetchTrashItems()]);
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();
}
}
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();
}
}
// Clipboard fonksiyonları
function cutFile(item) {
if (!item) return;
clipboardItem = item;
clipboardOperation = 'cut';
closeMenu();
}
function copyFile(item) {
if (!item) return;
clipboardItem = item;
clipboardOperation = 'copy';
closeMenu();
}
async function pasteFile() {
if (!clipboardItem || !clipboardOperation) return;
const sourcePath = resolveEntryOriginalPath(clipboardItem);
const targetPath = resolveOriginalPathForDisplay(currentPath, currentOriginalPath);
if (!sourcePath || !targetPath) {
alert("Geçersiz kaynak veya hedef yolu");
return;
}
const normalizedSource = normalizePath(sourcePath);
const normalizedTarget = normalizePath(targetPath);
if (!normalizedSource || !normalizedTarget) {
alert("Geçersiz kaynak veya hedef yolu");
return;
}
// Aynı konuma yapıştırmayı engelle
const sourceParent = normalizedSource.split("/").slice(0, -1).join("/");
if (sourceParent === normalizedTarget && clipboardOperation === 'cut') {
alert("Öğe zaten bu konumda");
return;
}
const sourceLabel = cleanFileName(clipboardItem.displayName || clipboardItem.name || normalizedSource);
const targetLabel = cleanFileName(currentPath || normalizedTarget);
const actionLabel = clipboardOperation === 'cut' ? 'taşı' : 'kopyala';
if (!confirm(`"${sourceLabel}" öğesini "${targetLabel}" konumuna ${actionLabel}mak istiyor musun?`)) {
return;
}
try {
let result;
if (clipboardOperation === 'cut') {
// Kes işlemi - moveEntry kullan
result = await moveEntry(normalizedSource, normalizedTarget);
} else {
// Kopyala işlemi - copyEntry kullan
result = await copyEntry(normalizedSource, normalizedTarget);
}
if (!result?.success) {
const message = result?.error || "İşlem başarısız oldu";
alert(message);
return;
}
if (!result?.unchanged) {
await loadFiles();
}
// Kes işleminden sonra clipboard'u temizle
if (clipboardOperation === 'cut') {
clipboardItem = null;
clipboardOperation = null;
}
// Kopyala işleminde clipboard'u koru (tekrar yapıştırmaya izin ver)
selectedItems = new Set();
} catch (err) {
console.error("❌ Yapıştırma hatası:", err);
alert("İşlem tamamlanamadı. Lütfen tekrar dene.");
}
}
onMount(async () => {
setSearchScope("files");
await loadFiles(); // önce dosyaları getir
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 = () => {
syncFromLocation();
};
const handleLocationChange = () => {
syncFromLocation();
};
if (typeof window !== "undefined") {
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
const triggerChange = () => {
window.dispatchEvent(new Event("locationchange"));
};
window.history.pushState = function (...args) {
originalPushState.apply(this, args);
triggerChange();
};
window.history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
triggerChange();
};
window.addEventListener("locationchange", handleLocationChange);
window.addEventListener("popstate", handlePopState);
unpatchHistory = () => {
window.history.pushState = originalPushState;
window.history.replaceState = originalReplaceState;
window.removeEventListener("locationchange", handleLocationChange);
window.removeEventListener("popstate", handlePopState);
};
}
ws.onmessage = async (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.type === "fileUpdate") {
console.log("📸 Yeni thumbnail bildirimi:", msg.path);
await loadFiles();
fetchTrashItems().catch(() => null);
}
if (msg.type === "manualMatch") {
console.log("🔗 Manuel eşleştirme bildirimi:", msg);
// Sadece ilgili dosyayı güncelle
const fileIndex = files.findIndex(f => f.name === msg.filePath);
if (fileIndex !== -1) {
// Dosyayı yeniden API'den al
try {
const response = await apiFetch("/api/files");
if (response.ok) {
const allFiles = await response.json();
const updatedFile = allFiles.find(f => f.name === msg.filePath);
if (updatedFile) {
// Sadece ilgili dosyayı güncelle
const nextFiles = [...files];
nextFiles[fileIndex] = augmentFileEntry(updatedFile);
files = nextFiles;
console.log("🔄 Dosya ikonu güncellendi:", msg.filePath);
// Store'ları da güncelle
await Promise.all([refreshMovieCount(), refreshTvShowCount()]);
}
}
} catch (err) {
console.warn("Dosya güncellenemedi:", err);
// Fallback olarak tüm dosyaları yeniden yükle
await loadFiles();
}
}
}
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 || "";
const folderId = savePath.split("/").pop();
files = files.map((f) => {
const fileFolder = f.name.split("/")[0];
if (fileFolder === folderId) {
return t.progress < 1
? {
...f,
progressText: `${Math.floor(t.progress * 100)}%`,
}
: { ...f, progressText: null };
}
return f;
});
}
files = [...files];
}
} catch (err) {
console.warn("WebSocket mesajı çözümlenemedi:", err);
}
};
function handleKey(e) {
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 (e.metaKey && e.key && e.key.toLowerCase() === "backspace") {
if (isEditable) return;
if (selectedItems.size > 0) {
e.preventDefault();
deleteSelectedFiles();
}
return;
}
// Modal açıkken Escape dışındaki tüm kısayolları deaktif et
if (showMatchModal) {
if (e.key === "Escape") {
closeMatchModal();
}
return;
}
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 (renamingFolder) {
cancelRenameFolder();
handled = true;
}
if (activeMenu) {
activeMenu = null;
handled = true;
}
if (handled) {
e.preventDefault();
}
return;
}
const isCmd = e.metaKey || e.ctrlKey;
const normalizedKey = (e.key || "").toLowerCase();
const isSelectAllKey =
isCmd &&
(normalizedKey === "a" || e.code === "KeyA" || e.keyCode === 65);
if (isSelectAllKey) {
if (isEditable || isCreatingFolder) return;
e.preventDefault();
e.stopPropagation();
if (IS_SAFARI && typeof e.stopImmediatePropagation === "function") {
e.stopImmediatePropagation();
}
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();
}
return;
}
if (showModal || showImageModal) {
if (e.key === "ArrowRight") showNext();
if (e.key === "ArrowLeft") showPrev();
}
}
const safariListenerOptions = { capture: true, passive: false };
const keyTargets = IS_SAFARI
? [window, document, document.body].filter(Boolean)
: [window];
const keydownOptions = IS_SAFARI ? safariListenerOptions : false;
const preventIfSelectAll = (e) => {
if (!IS_SAFARI) return;
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();
}
}
};
keyTargets.forEach((target) =>
target.addEventListener("keydown", handleKey, keydownOptions),
);
if (IS_SAFARI) {
keyTargets.forEach((target) =>
target.addEventListener("keyup", preventIfSelectAll, safariListenerOptions),
);
keyTargets.forEach((target) =>
target.addEventListener("keypress", preventIfSelectAll, safariListenerOptions),
);
}
window.addEventListener("popstate", handlePopState);
// Menüyü kapatmak için dışarı tıklama olayı
function handleClickOutside(event) {
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);
return () => {
keyTargets.forEach((target) =>
target.removeEventListener("keydown", handleKey, keydownOptions),
);
if (IS_SAFARI) {
keyTargets.forEach((target) =>
target.removeEventListener("keyup", preventIfSelectAll, safariListenerOptions),
);
keyTargets.forEach((target) =>
target.removeEventListener("keypress", preventIfSelectAll, safariListenerOptions),
);
}
window.removeEventListener("click", handleClickOutside);
window.removeEventListener("popstate", handlePopState);
if (unpatchHistory) unpatchHistory();
// Resize observer'ı temizle
if (breadcrumbContainer) {
const resizeObserver = new ResizeObserver(() => {});
resizeObserver.disconnect();
}
try {
ws.close();
} catch (err) {
console.warn("WebSocket kapatılırken hata:", err);
}
};
});
</script>
<section class="files" on:click={handleFilesClick}>
<div class="files-header">
<div class="header-title">
<h2>Media Library</h2>
<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(breadcrumbs[0].path) === normalizePath(currentPath)}
on:click|stopPropagation={() => handleBreadcrumbClick(breadcrumbs[0])}
>
{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}
</div>
</div>
</div>
<div class="header-actions">
{#if renderedEntries.length > 0 && selectedItems.size > 0}
<span class="selection-count">{selectedItems.size} öğe seçildi</span>
{/if}
{#if renderedEntries.length > 0 && selectedItems.size > 0}
<button
class="select-all-btn"
type="button"
on:click|stopPropagation={selectAll}
aria-label={allSelected ? "Seçimi temizle" : "Tümünü seç"}
>
<i class="fa-solid fa-square-check"></i>
</button>
{/if}
{#if clipboardItem && clipboardOperation}
<button
class="paste-btn"
type="button"
on:click|stopPropagation={pasteFile}
aria-label="Yapıştır"
title={clipboardOperation === 'cut' ? 'Kes ve yapıştır' : 'Kopyala ve yapıştır'}
>
<i class="fa-solid fa-paste"></i>
</button>
{/if}
<button
class="create-folder-btn"
type="button"
on:click|stopPropagation={startCreateFolder}
aria-label="Yeni klasör oluştur"
>
<i class="fa-solid fa-folder-plus"></i>
</button>
<button
class="view-toggle"
class:list-active={viewMode === "list"}
type="button"
on:click|stopPropagation={toggleView}
aria-label={viewMode === "grid"
? "Liste görünümüne geç"
: "Izgara görünümüne geç"}
>
{#if viewMode === "grid"}
<i class="fa-solid fa-list"></i>
{:else}
<i class="fa-solid fa-border-all"></i>
{/if}
</button>
</div>
</div>
{#if renderedEntries.length === 0 && !isCreatingFolder}
<div class="empty">
<div style="font-size:42px"><i class="fa-solid fa-folder-open"></i></div>
<div style="font-weight:700">
{#if hasSearch}
Aramanla eşleşen öğe bulunamadı
{:else}
No media found
{/if}
</div>
</div>
{:else}
<div
class="gallery"
class:list-view={viewMode === "list"}
on:dragover={handleContainerDragOver}
on:drop={handleContainerDrop}
>
{#if isCreatingFolder}
<div class="creating-folder" class:list-view={viewMode === "list"}>
<div class="folder-thumb">
<img src={FOLDER_ICON_PATH} alt="Yeni klasör" />
</div>
<div class="folder-info">
<input
type="text"
class="folder-name-input"
bind:value={newFolderName}
on:keydown={handleFolderKeydown}
on:blur={confirmCreateFolder}
placeholder="Klasör adını girin..."
autofocus
/>
</div>
</div>
{/if}
{#each renderedEntries as entry (entry.name)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="media-card"
class:folder-card={entry.isDirectory}
class:list-view={viewMode === "list"}
class:is-selected={selectedItems.has(entry.name)}
class:is-dragging={draggingItem?.name === entry.name}
class:is-drag-over={dragOverItem?.name === entry.name}
draggable="true"
on:dragstart={(event) => handleDragStart(entry, event)}
on:dragover={(event) => handleDragOver(entry, event)}
on:dragleave={() => handleDragLeave(entry)}
on:drop={(event) => handleDrop(entry, event)}
on:dragend={handleDragEnd}
on:click={() => handleEntryClick(entry)}
>
{#if entry.isDirectory}
<div class="folder-thumb">
<img src={FOLDER_ICON_PATH} alt={`${entry.displayName} klasörü`} />
</div>
<div class="folder-info">
{#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"
title={displayFileName(entry)}
>
{displayFileName(entry)}
</div>
{/if}
</div>
{:else}
{#if entry.thumbnail}
<img
src={`${API}${entry.thumbnail}?token=${localStorage.getItem("token")}`}
alt={entry.name}
class="thumb"
on:load={(e) => e.target.classList.add("loaded")}
/>
{:else}
<div class="thumb placeholder">
<i class="fa-regular fa-image"></i>
</div>
{/if}
<div class="info">
<div class="name" title={displayFileName(entry)}>
{displayFileName(entry)}
</div>
<div class="size">
{#if entry.progressText}
<span class="progress-text">{entry.progressText}</span>
{:else}
{formatSize(entry.size)}
{/if}
</div>
<div class="list-meta">
<div class="meta-line primary">
<span>{formatDateTime(entry.added || entry.completedAt)}</span>
<span class="meta-separator">|</span>
<span>{formatSize(entry.size)}</span>
</div>
<div class="meta-line secondary">
{#if entry.progressText}
<span class="status-badge">{entry.progressText}</span>
<span class="meta-separator">|</span>
{/if}
Tracker:
<span class="tracker-name">
{formatTracker(entry.tracker)}
</span>
</div>
{#if entry.mediaInfo?.video || entry.mediaInfo?.audio}
<div class="meta-line codecs">
{#if entry.extension}
<span class="codec-chip file-type">
{#if entry.type?.startsWith("image/")}
<i class="fa-solid fa-file-image"></i>
{:else}
<i class="fa-solid fa-file-video"></i>
{/if}
{entry.extension.toUpperCase()}
</span>
{/if}
{#if entry.mediaInfo?.video}
<span class="codec-chip">
<i class="fa-solid fa-film"></i>
{formatVideoCodec(entry.mediaInfo.video)}
</span>
{/if}
{#if entry.mediaInfo?.video && entry.mediaInfo?.audio}
<span class="codec-separator">|</span>
{/if}
{#if entry.mediaInfo?.audio}
<span class="codec-chip">
<i class="fa-solid fa-volume-high"></i>
{formatAudioCodec(entry.mediaInfo.audio)}
</span>
{/if}
</div>
{/if}
</div>
</div>
<div class="media-type-icon">
{#if entry.type?.startsWith("image/")}
<i class="fa-solid fa-image"></i>
{:else if entry.mediaCategory === "tv"}
<i class="fa-solid fa-tv"></i>
{:else if entry.mediaCategory === "movie"}
<i class="fa-solid fa-film"></i>
{:else if entry.mediaCategory === "music"}
<i class="fa-solid fa-music"></i>
{:else if entry.type?.startsWith("video/")}
<i class="fa-solid fa-play"></i>
{:else}
<i class="fa-solid fa-file"></i>
{/if}
</div>
{/if}
<button
class="selection-toggle"
class:is-selected={selectedItems.has(entry.name)}
type="button"
on:click|stopPropagation={() => toggleSelection(entry)}
aria-label={selectedItems.has(entry.name)
? "Seçimi kaldır"
: "Bu öğeyi seç"}
>
{#if selectedItems.has(entry.name)}
<i class="fa-solid fa-circle-check"></i>
{:else}
<i class="fa-regular fa-circle"></i>
{/if}
</button>
<button
class="menu-toggle"
type="button"
on:click|stopPropagation={(e) => toggleMenu(entry, e)}
aria-label="Menü"
>
<i class="fa-solid fa-ellipsis"></i>
</button>
</div>
{/each}
</div>
{/if}
</section>
{#if activeMenu}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="dropdown-menu-portal"
style="top: {menuPosition.top}px; left: {menuPosition.left}px;"
on:click|stopPropagation
>
{#if activeMenu.isDirectory}
<button
class="menu-item"
on:click|stopPropagation={() =>
navigateToPath(activeMenu.displayPath, {
originalPath:
activeMenu.primaryOriginalPath ||
activeMenu.originalPaths?.[0] ||
activeMenu.displayPath,
})}
>
<i class="fa-solid fa-folder-open"></i>
<span></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>
<button
class="menu-item"
on:click|stopPropagation={() => cutFile(activeMenu)}
>
<i class="fa-solid fa-scissors"></i>
<span>Kes</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => copyFile(activeMenu)}
>
<i class="fa-solid fa-copy"></i>
<span>Kopyala</span>
</button>
<div class="menu-divider"></div>
{:else}
<button
class="menu-item"
on:click|stopPropagation={() => downloadFile(activeMenu)}
>
<i class="fa-solid fa-download"></i>
<span>İndir</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => matchFile(activeMenu)}
>
<i class="fa-solid fa-wand-magic-sparkles"></i>
<span>Eşleştir</span>
</button>
<div class="menu-divider"></div>
<button
class="menu-item"
on:click|stopPropagation={() => cutFile(activeMenu)}
>
<i class="fa-solid fa-scissors"></i>
<span>Kes</span>
</button>
<button
class="menu-item"
on:click|stopPropagation={() => copyFile(activeMenu)}
>
<i class="fa-solid fa-copy"></i>
<span>Kopyala</span>
</button>
<div class="menu-divider"></div>
{/if}
<button
class="menu-item delete"
on:click|stopPropagation={() => deleteFile(activeMenu)}
>
<i class="fa-solid fa-trash"></i>
<span>Sil</span>
</button>
</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}>
<button class="global-close-btn" on:click|stopPropagation={closeModal}
>✕</button
>
<button class="nav-btn left" on:click|stopPropagation={showPrev}>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="nav-btn right" on:click|stopPropagation={showNext}>
<i class="fa-solid fa-chevron-right"></i>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation>
<div class="modal-header">
<div class="video-title">{cleanFileName(selectedName)}</div>
</div>
<div class="custom-player">
<!-- ✅ selectedVideo yokken boş src -->
<!-- svelte-ignore a11y-media-has-caption -->
{#key encName}
<!-- svelte-ignore a11y-media-has-caption -->
<video
bind:this={videoEl}
src={getVideoURL()}
class="video-element"
playsinline
on:timeupdate={updateProgress}
on:loadedmetadata={async () => {
// her yeni videoda statei sıfırla
isPlaying = false;
currentTime = 0;
updateDuration();
const slider = document.querySelector(".volume-slider");
if (slider) {
slider.value = volume;
slider.style.setProperty("--fill", slider.value * 100);
}
// 🎬 Otomatik oynatma (tarayıcı izin verirse)
try {
await videoEl.play();
isPlaying = true;
} catch (err) {
console.warn("Autoplay engellendi:", err?.message || err);
isPlaying = false;
}
}}
on:ended={() => (isPlaying = false)}
>
{#if subtitleURL}
<track
kind="subtitles"
src={subtitleURL}
srclang={subtitleLang}
label={subtitleLabel}
default
/>
{/if}
</video>
{/key}
<div class="controls">
<div class="top-controls">
<button class="control-btn" on:click={togglePlay}>
{#if isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
{/if}
</button>
<div class="right-controls">
<input
type="range"
min="0"
max="1"
step="0.01"
bind:value={volume}
on:input={changeVolume}
class="volume-slider"
/>
<button class="control-btn" on:click={toggleFullscreen}>
<i class="fa-solid fa-expand"></i>
</button>
<!-- ✅ selectedVideo yokken '#' -->
<a
href={selectedName ? `${API}/downloads/${selectedName}` : "#"}
download={selectedName || undefined}
class="control-btn"
title="Download"
>
<i class="fa-solid fa-download"></i>
</a>
<label class="control-btn subtitle-icon" title="Add subtitles">
<i class="fa-solid fa-closed-captioning"></i>
<input
type="file"
accept=".srt,.vtt"
on:change={handleSubtitleUpload}
style="display: none"
/>
</label>
</div>
</div>
<div class="bottom-controls">
<span class="time">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<input
type="range"
min="0"
max={duration}
step="0.1"
bind:value={currentTime}
on:input={seekVideo}
class="progress-slider"
/>
</div>
</div>
</div>
</div>
</div>
{/if}
{#if showImageModal && selectedImage}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="image-modal-overlay" on:click={() => (showImageModal = false)}>
<button
class="image-close-btn"
on:click|stopPropagation={() => (showImageModal = false)}>✕</button
>
<button class="nav-btn left" on:click|stopPropagation={showPrev}>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="nav-btn right" on:click|stopPropagation={showNext}>
<i class="fa-solid fa-chevron-right"></i>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="image-modal-content" on:click|stopPropagation>
<img
src={`${API}${selectedImage.url}?token=${localStorage.getItem("token")}`}
alt={selectedImage.name}
class="image-modal-img"
/>
</div>
</div>
{/if}
{#if selectedItems.size > 0}
<button class="floating-delete" type="button" on:click={deleteSelectedFiles}>
<i class="fa-solid fa-trash"></i>
</button>
{/if}
{#if showModal && selectedVideo}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="modal-overlay" on:click={closeModal}>
<button class="global-close-btn" on:click|stopPropagation={closeModal}
>✕</button
>
<button class="nav-btn left" on:click|stopPropagation={showPrev}>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="nav-btn right" on:click|stopPropagation={showNext}>
<i class="fa-solid fa-chevron-right"></i>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="modal-content" on:click|stopPropagation>
<div class="modal-header">
<div class="video-title">{cleanFileName(selectedName)}</div>
</div>
<div class="custom-player">
<!-- ✅ selectedVideo yokken boş src -->
<!-- svelte-ignore a11y-media-has-caption -->
{#key encName}
<!-- svelte-ignore a11y-media-has-caption -->
<video
bind:this={videoEl}
src={getVideoURL()}
class="video-element"
playsinline
on:timeupdate={updateProgress}
on:loadedmetadata={async () => {
// her yeni videoda statei sıfırla
isPlaying = false;
currentTime = 0;
updateDuration();
const slider = document.querySelector(".volume-slider");
if (slider) {
slider.value = volume;
slider.style.setProperty("--fill", slider.value * 100);
}
// 🎬 Otomatik oynatma (tarayıcı izin verirse)
try {
await videoEl.play();
isPlaying = true;
} catch (err) {
console.warn("Autoplay engellendi:", err?.message || err);
isPlaying = false;
}
}}
on:ended={() => (isPlaying = false)}
>
{#if subtitleURL}
<track
kind="subtitles"
src={subtitleURL}
srclang={subtitleLang}
label={subtitleLabel}
default
/>
{/if}
</video>
{/key}
<div class="controls">
<div class="top-controls">
<button class="control-btn" on:click={togglePlay}>
{#if isPlaying}
<i class="fa-solid fa-pause"></i>
{:else}
<i class="fa-solid fa-play"></i>
{/if}
</button>
<div class="right-controls">
<input
type="range"
min="0"
max="1"
step="0.01"
bind:value={volume}
on:input={changeVolume}
class="volume-slider"
/>
<button class="control-btn" on:click={toggleFullscreen}>
<i class="fa-solid fa-expand"></i>
</button>
<!-- ✅ selectedVideo yokken '#' -->
<a
href={selectedName ? `${API}/downloads/${selectedName}` : "#"}
download={selectedName || undefined}
class="control-btn"
title="Download"
>
<i class="fa-solid fa-download"></i>
</a>
<label class="control-btn subtitle-icon" title="Add subtitles">
<i class="fa-solid fa-closed-captioning"></i>
<input
type="file"
accept=".srt,.vtt"
on:change={handleSubtitleUpload}
style="display: none"
/>
</label>
</div>
</div>
<div class="bottom-controls">
<span class="time">
{formatTime(currentTime)} / {formatTime(duration)}
</span>
<input
type="range"
min="0"
max={duration}
step="0.1"
bind:value={currentTime}
on:input={seekVideo}
class="progress-slider"
/>
</div>
</div>
</div>
</div>
</div>
{/if}
{#if showImageModal && selectedImage}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="image-modal-overlay" on:click={() => (showImageModal = false)}>
<button
class="image-close-btn"
on:click|stopPropagation={() => (showImageModal = false)}>✕</button
>
<button class="nav-btn left" on:click|stopPropagation={showPrev}>
<i class="fa-solid fa-chevron-left"></i>
</button>
<button class="nav-btn right" on:click|stopPropagation={showNext}>
<i class="fa-solid fa-chevron-right"></i>
</button>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="image-modal-content" on:click|stopPropagation>
<img
src={`${API}${selectedImage.url}?token=${localStorage.getItem("token")}`}
alt={selectedImage.name}
class="image-modal-img"
/>
</div>
</div>
{/if}
<MatchModal
show={showMatchModal && !!matchingFile}
headerTitle="Eşlemeyi Düzelt"
fileLabel="Dosya"
fileName={matchingFile ? matchingFile.name.split("/").pop() : ""}
sizeText={matchingFile ? formatSize(matchingFile.size) : ""}
titleLabel={matchType === "series" ? "Dizi Adı" : "Film Adı"}
yearLabel="Yıl"
titlePlaceholder={matchType === "series" ? "Dizi adını girin" : "Film adını girin"}
showYearInput={true}
bind:titleValue={matchTitle}
bind:yearValue={matchYear}
searching={searching}
results={searchResults}
showEmpty={false}
applyingId={applyingResultId}
onClose={closeMatchModal}
onTitleInput={handleSearchInput}
onYearInput={handleSearchInput}
onSelect={selectMatchResult}
/>
<style>
:root {
--yellow: #ffc107;
--yellow-dark: #e0a800;
}
.files-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
gap: 12px;
}
.header-title {
display: flex;
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: 6px;
font-size: 14px;
color: #757575;
white-space: nowrap;
overflow: hidden;
}
.crumb {
border: none;
background: transparent;
color: inherit;
font: inherit;
padding: 2px;
border-radius: 6px;
cursor: pointer;
transition:
color 0.18s ease,
background 0.18s ease;
}
.crumb:hover {
color: #1f78ff;
background: rgba(31, 120, 255, 0.1);
}
.crumb.is-active {
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;
background: transparent;
}
.crumb.ellipsis:hover {
background: transparent;
}
.selection-count {
font-size: 13px;
color: #6a6a6a;
font-weight: 500;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.select-all-btn {
background: #2e2e2e;
border: none;
color: #f5f5f5;
width: 36px;
height: 36px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
transition:
background 0.2s ease,
transform 0.2s ease;
}
.select-all-btn i {
font-size: 16px;
}
.view-toggle {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
transition: all 0.2s ease;
font-size: 14px;
}
.view-toggle:hover {
background: var(--yellow);
border-color: var(--yellow-dark);
color: #222;
transform: scale(1.05);
}
.view-toggle:active {
transform: scale(0.95);
}
.view-toggle.list-active {
background: var(--yellow);
border-color: var(--yellow-dark);
color: #222;
}
.create-folder-btn {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
transition: all 0.2s ease;
font-size: 14px;
}
.create-folder-btn:hover {
background: var(--yellow);
border-color: var(--yellow-dark);
color: #222;
transform: scale(1.05);
}
.create-folder-btn:active {
transform: scale(0.95);
}
.paste-btn {
background: transparent;
border: 1px solid #ddd;
color: #666;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
width: 36px;
transition: all 0.2s ease;
font-size: 14px;
}
.paste-btn:hover {
background: #4caf50;
border-color: #45a049;
color: #fff;
transform: scale(1.05);
}
.paste-btn:active {
transform: scale(0.95);
}
/* Klasör oluşturma UI elemanları */
.creating-folder {
background: rgba(245, 179, 51, 0.1);
border: 2px dashed var(--yellow);
border-radius: 8px;
padding: 12px 12px 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 8px;
width: 100%;
animation: fadeIn 0.3s ease;
}
.creating-folder.list-view {
flex-direction: row;
align-items: center;
padding: 12px 16px;
min-height: 96px;
gap: 18px;
}
.creating-folder .folder-thumb {
width: 100%;
height: 110px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.creating-folder.list-view .folder-thumb {
width: 128px;
height: 128px;
}
.creating-folder .folder-thumb img {
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: 125px;
height: 125px;
}
.creating-folder .folder-info {
width: 100%;
text-align: center;
flex-shrink: 0;
}
.creating-folder.list-view .folder-info {
text-align: left;
flex: 1;
}
.folder-name-input {
width: 100%;
padding: 10px 14px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
outline: none;
background: white;
transition: border-color 0.2s ease;
text-align: center;
}
.creating-folder.list-view .folder-name-input {
text-align: left;
}
.folder-name-input:focus {
border-color: var(--yellow);
}
.folder-name-input::placeholder {
color: #999;
}
/* === GALERİ === */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.gallery.list-view {
display: flex;
flex-direction: column;
gap: 14px;
}
.media-card {
position: relative;
background: #f6f6f6;
border-radius: 10px;
overflow: visible;
border: 1px solid #e2e2e2;
box-shadow: 0 1px 2px rgba(15, 15, 15, 0.04);
display: flex;
flex-direction: column;
isolation: isolate;
transition:
border-color 0.18s ease,
background 0.18s ease,
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);
cursor: pointer;
}
.media-card::after {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.08);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.media-card:hover {
border-color: #d4d4d4;
background: #f1f1f1;
box-shadow: 0 2px 4px rgba(15, 15, 15, 0.06);
}
.media-card:hover::after {
opacity: 0.16;
}
.media-card.is-selected {
border-color: #2d965a;
background: #f4fbf7;
box-shadow:
0 0 0 1px rgba(45, 150, 90, 0.35),
0 4px 12px rgba(45, 150, 90, 0.12);
}
.media-card.is-selected::after {
opacity: 0.12;
}
.media-card.is-selected:hover::after {
opacity: 0.18;
}
.media-card.list-view {
flex-direction: row;
align-items: center;
padding: 12px 16px;
gap: 16px;
min-height: 96px;
}
.media-card.list-view .thumb {
width: 128px;
height: 72px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
transition:
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-radius 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.media-card:hover {
transform: none;
}
.media-card.is-selected:hover {
transform: none;
}
.selection-toggle {
position: absolute;
top: 12px;
left: 12px;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.45);
color: #f5f5f5;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
outline: none;
transform: scale(0.88);
transition:
opacity 0.2s ease,
transform 0.2s ease,
background 0.2s ease;
cursor: pointer;
pointer-events: none;
z-index: 2;
}
.selection-toggle i {
font-size: 14px;
}
.media-card:hover .selection-toggle,
.media-card.is-selected .selection-toggle {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.selection-toggle.is-selected {
background: rgba(45, 150, 90, 0.85);
}
.selection-toggle.is-selected i {
color: #fff;
}
.media-card.list-view .selection-toggle {
top: 16px;
left: 16px;
}
.thumb {
width: 100%;
height: 110px;
object-fit: cover;
border-radius: 10px 10px 0 0;
transition:
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
border-radius 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 42px;
background: #ddd;
}
.info {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
transition:
padding 0.3s cubic-bezier(0.4, 0, 0.2, 1),
gap 0.3s cubic-bezier(0.4, 0, 0.2, 1),
flex 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.name {
font-weight: 600;
font-size: 13px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.3;
max-height: calc(1.3em * 2);
text-overflow: ellipsis;
word-break: break-word;
}
.size {
font-size: 12px;
color: #666;
transition:
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.media-card.list-view .info {
flex: 1;
padding: 0;
gap: 6px;
}
.media-card.list-view .name {
font-size: 15px;
}
.media-card.list-view .size {
display: none;
}
.list-meta {
display: none;
opacity: 0;
max-height: 0;
transition:
opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),
max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.media-card.list-view .list-meta {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
color: #7a7a7a;
opacity: 1;
max-height: 200px;
}
.meta-line.primary {
display: flex;
align-items: center;
gap: 6px;
}
.meta-separator {
opacity: 0.65;
}
.meta-line.secondary .tracker-name {
margin-left: 4px;
color: #5a5a5a;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
}
.meta-line.codecs {
display: flex;
align-items: center;
gap: 12px;
color: #5a5a5a;
font-size: 13px;
}
.codec-chip {
display: inline-flex;
align-items: center;
gap: 6px;
color: #4e4e4e;
font-weight: 500;
}
.codec-chip.file-type {
color: #1f1f1f;
text-transform: uppercase;
}
.codec-chip i {
color: #ffc107;
font-size: 12px;
}
.codec-separator {
color: #7a7a7a;
font-weight: 500;
}
.status-badge {
color: #2f8a4d;
font-weight: 600;
}
.media-card.is-dragging {
opacity: 0.55;
}
.media-card.is-drag-over {
border-color: #1f78ff;
box-shadow: 0 0 0 2px rgba(31, 120, 255, 0.25);
}
.media-card.folder-card.is-drag-over {
background: rgba(31, 120, 255, 0.1);
}
.floating-delete {
position: fixed;
right: 28px;
bottom: 28px;
width: 52px;
height: 52px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.65);
color: #fefefe;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
backdrop-filter: blur(8px);
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.28);
transition: background 0.2s ease;
z-index: 2500;
}
.floating-delete:hover {
background: #e53935;
}
.media-card.list-view .media-type-icon {
position: static;
color: rgba(0, 0, 0, 0.35);
font-size: 18px;
align-self: flex-start;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 28px;
cursor: pointer;
z-index: 2100;
width: 50px;
height: 60px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition:
background 0.2s ease,
transform 0.2s ease;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.05);
}
.nav-btn.left {
left: 15px;
}
.nav-btn.right {
right: 15px;
}
.media-card {
position: relative;
}
.media-type-icon {
position: absolute;
bottom: 6px;
right: 8px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
pointer-events: none;
}
.media-type-icon i {
filter: drop-shadow(0 1px 1px rgba(255, 255, 255, 0.3));
}
.progress-text {
color: #666; /* gri */
font-weight: 600;
font-size: 12px;
animation: pulse 1.2s infinite ease-in-out;
}
@keyframes pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
/* === RESPONSIVE === */
@media (max-width: 768px) {
.floating-delete {
right: 20px;
bottom: 20px;
width: 48px;
height: 48px;
}
.gallery {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.media-card.list-view {
flex-direction: column;
align-items: flex-start;
}
.media-card.list-view .thumb {
width: 100%;
height: 160px;
}
.media-card.list-view .media-type-icon {
align-self: center;
}
}
@media (max-width: 480px) {
.floating-delete {
right: 16px;
bottom: 16px;
width: 44px;
height: 44px;
}
.gallery {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}
.files-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
gap: 12px;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.select-all-btn {
background: #2e2e2e;
border: none;
color: #f5f5f5;
width: 36px;
height: 36px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
outline: none;
transition:
background 0.2s ease,
transform 0.2s ease;
}
.select-all-btn i {
font-size: 16px;
}
/* === GALERİ === */
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 12px;
}
.gallery.list-view {
display: flex;
flex-direction: column;
gap: 14px;
}
.media-card {
position: relative;
background: #f5f5f5;
border-radius: 12px;
overflow: visible;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
isolation: isolate;
transition: box-shadow 0.18s ease;
cursor: pointer;
}
.media-card::after {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.18);
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease;
}
.media-card:hover::after {
opacity: 0.22;
}
.media-card.is-selected {
transform: none;
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.22);
}
.media-card.is-selected::after {
opacity: 0.32;
}
.media-card.is-selected:hover::after {
opacity: 0.35;
}
.media-card.list-view {
flex-direction: row;
align-items: center;
padding: 12px 16px;
gap: 16px;
min-height: 96px;
}
.media-card.list-view .thumb {
width: 120px;
height: 72px;
border-radius: 8px;
object-fit: cover;
flex-shrink: 0;
}
.media-card:hover {
transform: none;
}
.media-card.is-selected:hover {
transform: none;
}
.selection-toggle {
position: absolute;
top: 12px;
left: 12px;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.45);
color: #f5f5f5;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
outline: none;
transform: scale(0.88);
transition:
opacity 0.2s ease,
transform 0.2s ease,
background 0.2s ease;
cursor: pointer;
pointer-events: none;
z-index: 2;
}
.selection-toggle i {
font-size: 14px;
}
.media-card:hover .selection-toggle,
.media-card.is-selected .selection-toggle {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.selection-toggle.is-selected {
background: rgba(45, 150, 90, 0.85);
}
.selection-toggle.is-selected i {
color: #fff;
}
.media-card.list-view .selection-toggle {
top: 16px;
left: 16px;
}
.thumb {
width: 100%;
height: 110px;
object-fit: cover;
}
.thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 42px;
background: #ddd;
}
.info {
padding: 10px;
display: flex;
flex-direction: column;
gap: 4px;
}
.name {
font-weight: 600;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.size {
font-size: 12px;
color: #666;
}
.media-card.list-view .info {
flex: 1;
padding: 0;
gap: 6px;
}
.media-card.list-view .name {
font-size: 15px;
min-height: 22px; /* Liste görünümündeki dosya isimleri için tutarlı yükseklik */
display: flex;
align-items: center;
}
.media-card.list-view .size {
display: none;
}
.list-meta {
display: none;
}
.media-card.list-view .list-meta {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 13px;
color: #7a7a7a;
}
.meta-line.primary {
display: flex;
align-items: center;
gap: 6px;
}
.meta-separator {
opacity: 0.65;
}
.meta-line.secondary .tracker-name {
margin-left: 4px;
color: #5a5a5a;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
}
.status-badge {
color: #2f8a4d;
font-weight: 600;
}
.media-card.list-view .media-type-icon {
position: static;
color: rgba(0, 0, 0, 0.35);
font-size: 18px;
align-self: flex-start;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 28px;
cursor: pointer;
z-index: 2100;
width: 50px;
height: 60px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition:
background 0.2s ease,
transform 0.2s ease;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.05);
}
.nav-btn.left {
left: 15px;
}
.nav-btn.right {
right: 15px;
}
.media-card {
position: relative;
}
.media-type-icon {
position: absolute;
bottom: 6px;
right: 8px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
pointer-events: none;
}
.media-type-icon i {
filter: drop-shadow(0 1px 1px rgba(255, 255, 255, 0.3));
}
.progress-text {
color: #666; /* gri */
font-weight: 600;
font-size: 12px;
animation: pulse 1.2s infinite ease-in-out;
}
@keyframes pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
/* === RESPONSIVE === */
@media (max-width: 768px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.media-card.list-view {
flex-direction: column;
align-items: flex-start;
}
.media-card.list-view .thumb {
width: 100%;
height: 160px;
}
.media-card.list-view .media-type-icon {
align-self: center;
}
}
@media (max-width: 480px) {
.gallery {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}
/* Folder görünümü */
.folder-card {
background: transparent;
border: none;
box-shadow: none;
padding: 12px 12px 8px;
align-items: center;
}
.folder-card::after {
display: none;
}
.folder-card:hover {
background: rgba(0, 0, 0, 0.03);
border: none;
box-shadow: none;
}
.folder-card.is-selected {
background: rgba(45, 150, 90, 0.12);
box-shadow: none;
}
.folder-card.is-selected::after {
display: none;
}
.folder-thumb {
width: 100%;
height: 110px;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
}
.folder-thumb img {
width: 95px;
height: 95px;
object-fit: contain;
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
}
.folder-info {
margin-top: 4px;
width: 100%;
text-align: center;
flex-shrink: 0;
}
.folder-name {
font-weight: 600;
font-size: 14px;
color: #2d2d2d;
line-height: 1.25;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-align: center;
max-height: calc(1.25em * 2);
overflow: hidden;
text-overflow: ellipsis;
}
.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: #333;
}
.folder-card.list-view {
flex-direction: row;
align-items: center;
padding: 12px 18px;
min-height: unset;
gap: 18px;
}
.folder-card.list-view .folder-thumb {
width: 128px;
height: 128px;
}
.folder-card.list-view .folder-info {
margin-top: 0;
text-align: left;
}
.folder-card.list-view .folder-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.25;
max-height: calc(1.25em * 2);
overflow: hidden;
text-overflow: ellipsis;
}
/* Menü düğmesi ve dropdown stilleri */
.menu-toggle {
position: absolute;
top: 12px;
right: 12px;
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.45);
color: #f5f5f5;
display: inline-flex;
align-items: center;
justify-content: center;
opacity: 0;
outline: none;
transform: scale(0.88);
transition:
opacity 0.2s ease,
transform 0.2s ease,
background 0.2s ease;
cursor: pointer;
pointer-events: none;
z-index: 2;
}
.menu-toggle i {
font-size: 14px;
}
.media-card:hover .menu-toggle,
.media-card.is-selected .menu-toggle {
opacity: 1;
transform: scale(1);
pointer-events: auto;
}
.menu-toggle:hover {
background: rgba(0, 0, 0, 0.65);
}
.media-card.list-view .menu-toggle {
top: 16px;
right: 16px;
}
.dropdown-menu-portal {
position: fixed;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
min-width: 160px;
z-index: 10000;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-item {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px 16px;
border: none;
background: transparent;
color: #333;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease;
text-align: left;
}
.menu-item:first-child {
border-radius: 8px 8px 0 0;
}
.menu-item:last-child {
border-radius: 0 0 8px 8px;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.menu-item.delete {
color: #e53935;
}
.menu-item.delete:hover {
background-color: #ffebee;
}
.menu-item i {
font-size: 14px;
width: 16px;
text-align: center;
}
.menu-divider {
height: 1px;
background-color: #e0e0e0;
margin: 0;
}
</style>