3740 lines
101 KiB
Svelte
3740 lines
101 KiB
Svelte
<script>
|
||
import { onMount, tick } from "svelte";
|
||
import { API, apiFetch } from "../utils/api.js";
|
||
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();
|
||
let customOrder = new Map();
|
||
let draggingItem = null;
|
||
let dragOverItem = null;
|
||
let lastDragPath = "";
|
||
|
||
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 || "");
|
||
|
||
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 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);
|
||
}
|
||
|
||
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",
|
||
);
|
||
applyOrdering(path);
|
||
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;
|
||
let subtitleLang = "en";
|
||
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") {
|
||
viewMode = storedView;
|
||
}
|
||
}
|
||
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ü öğ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 = "";
|
||
|
||
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 {
|
||
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: initialPath, originalPath: null },
|
||
"",
|
||
newUrl,
|
||
);
|
||
}
|
||
currentPath = normalizePath(initialPath);
|
||
// 🎬 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 = 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();
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
function handleDrop(entry, event) {
|
||
if (!draggingItem) return;
|
||
if (normalizePath(currentPath) !== lastDragPath) {
|
||
draggingItem = null;
|
||
dragOverItem = null;
|
||
return;
|
||
}
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
reorderEntries(draggingItem, entry);
|
||
draggingItem = null;
|
||
dragOverItem = null;
|
||
}
|
||
|
||
function handleDragEnd() {
|
||
draggingItem = null;
|
||
dragOverItem = null;
|
||
}
|
||
|
||
function handleContainerDragOver(event) {
|
||
if (!draggingItem) return;
|
||
if (normalizePath(currentPath) !== lastDragPath) return;
|
||
event.preventDefault();
|
||
}
|
||
|
||
function handleContainerDrop(event) {
|
||
if (!draggingItem) return;
|
||
if (normalizePath(currentPath) !== lastDragPath) {
|
||
draggingItem = null;
|
||
dragOverItem = null;
|
||
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);
|
||
draggingItem = null;
|
||
dragOverItem = null;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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() {
|
||
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()]);
|
||
|
||
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()]);
|
||
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);
|
||
if (msg.type === "fileUpdate") {
|
||
console.log("📸 Yeni thumbnail bildirimi:", msg.path);
|
||
await loadFiles();
|
||
}
|
||
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 === "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 (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;
|
||
}
|
||
}
|
||
|
||
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);
|
||
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">
|
||
{#each breadcrumbs as crumb, index (index)}
|
||
{#if crumb.ellipsis}
|
||
<span class="crumb ellipsis">...</span>
|
||
{:else}
|
||
<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}
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
{#if visibleEntries.length > 0 && selectedItems.size > 0}
|
||
<span class="selection-count">{selectedItems.size} öğe seçildi</span>
|
||
{/if}
|
||
{#if visibleEntries.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}
|
||
<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 visibleEntries.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">No media found</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 visibleEntries 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">
|
||
<div class="folder-name">{cleanFileName(entry.displayName)}</div>
|
||
</div>
|
||
{:else}
|
||
{#if entry.thumbnail}
|
||
<img
|
||
src={`${API}${entry.thumbnail}?token=${localStorage.getItem("token")}&t=${Date.now()}`}
|
||
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">{cleanFileName(entry.name)}</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("video/")}
|
||
{#if entry.seriesEpisode || (entry.seriesEpisodes && Object.keys(entry.seriesEpisodes).length > 0)}
|
||
<i class="fa-solid fa-tv"></i>
|
||
{:else if entry.movieMatch}
|
||
<i class="fa-solid fa-film"></i>
|
||
{:else}
|
||
<i class="fa-solid fa-ban"></i>
|
||
{/if}
|
||
{:else if entry.type?.startsWith("image/")}
|
||
<i class="fa-solid fa-image"></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>Aç</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>
|
||
{/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 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 state’i 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 state’i 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 showMatchModal && matchingFile}
|
||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||
<div class="match-overlay" on:click={closeMatchModal}>
|
||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||
<div class="match-overlay-content" on:click|stopPropagation>
|
||
<button class="match-close" on:click={closeMatchModal} aria-label="Kapat">
|
||
<i class="fa-solid fa-xmark"></i>
|
||
</button>
|
||
|
||
<div class="match-header">
|
||
<h3 class="match-title">
|
||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||
Eşlemeyi Düzelt
|
||
</h3>
|
||
<div class="match-subtitle">
|
||
<span class="match-location" title={matchingFile.name}>
|
||
<i class="fa-solid fa-file"></i>
|
||
<span class="location-text">Dosya: {matchingFile.name.split('/').pop()}</span>
|
||
</span>
|
||
<span class="match-separator">|</span>
|
||
<span class="match-size">
|
||
<i class="fa-solid fa-database"></i>
|
||
{formatSize(matchingFile.size)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="match-body">
|
||
<div class="match-inputs">
|
||
<div class="input-group title-input">
|
||
<label for="match-title">{matchType === "series" ? "Dizi Adı" : "Film Adı"}</label>
|
||
<input
|
||
id="match-title"
|
||
type="text"
|
||
bind:value={matchTitle}
|
||
on:input={handleSearchInput}
|
||
placeholder={matchType === "series" ? "Dizi adını girin" : "Film adını girin"}
|
||
/>
|
||
</div>
|
||
<div class="input-group year-input">
|
||
<label for="match-year">Yıl</label>
|
||
<input
|
||
id="match-year"
|
||
type="text"
|
||
bind:value={matchYear}
|
||
on:input={handleSearchInput}
|
||
placeholder="YYYY"
|
||
maxlength="4"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="match-divider"></div>
|
||
|
||
{#if searching}
|
||
<div class="search-loading">
|
||
<i class="fa-solid fa-spinner fa-spin"></i>
|
||
Aranıyor...
|
||
</div>
|
||
{:else if searchResults.length > 0}
|
||
<div class="search-results">
|
||
{#each searchResults as result}
|
||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||
<div class="result-item" class:applying={applyingResultId === result.id} on:click={() => selectMatchResult(result)}>
|
||
<div class="result-poster">
|
||
{#if result.poster}
|
||
<img src={result.poster} alt={result.title} loading="lazy" />
|
||
{:else}
|
||
<div class="result-poster-placeholder">
|
||
<i class="fa-regular fa-image"></i>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<div class="result-info">
|
||
<div class="result-title">{result.title}</div>
|
||
<div class="result-meta">
|
||
{#if result.year}
|
||
<span class="result-year">
|
||
<i class="fa-solid fa-calendar-days"></i>
|
||
{result.year}
|
||
</span>
|
||
{/if}
|
||
{#if result.runtime}
|
||
<span class="result-separator">•</span>
|
||
<span class="result-runtime">
|
||
<i class="fa-solid fa-clock"></i>
|
||
{result.runtime} dk
|
||
</span>
|
||
{/if}
|
||
{#if result.status}
|
||
<span class="result-separator">•</span>
|
||
<span class="result-status">
|
||
<i class="fa-solid fa-signal"></i>
|
||
{result.status}
|
||
</span>
|
||
{/if}
|
||
</div>
|
||
{#if result.genres && result.genres.length > 0}
|
||
<div class="result-genres">{result.genres.slice(0, 3).join(", ")}</div>
|
||
{/if}
|
||
{#if result.cast && result.cast.length > 0}
|
||
<div class="result-cast">
|
||
<i class="fa-solid fa-user"></i>
|
||
{result.cast.join(", ")}
|
||
</div>
|
||
{/if}
|
||
{#if result.overview}
|
||
<div class="result-overview">{result.overview}</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<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;
|
||
}
|
||
.breadcrumb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
font-size: 14px;
|
||
color: #757575;
|
||
}
|
||
.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;
|
||
}
|
||
.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);
|
||
}
|
||
|
||
/* Klasör oluşturma UI elemanları */
|
||
.creating-folder {
|
||
background: rgba(245, 179, 51, 0.1);
|
||
border: 2px dashed var(--yellow);
|
||
border-radius: 8px;
|
||
padding: 18px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 18px;
|
||
width: 100%;
|
||
min-height: 210px;
|
||
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: 150px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.creating-folder.list-view .folder-thumb {
|
||
width: 135px;
|
||
height: 135px;
|
||
}
|
||
|
||
.creating-folder .folder-thumb img {
|
||
width: 135px;
|
||
height: 135px;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
|
||
}
|
||
|
||
.creating-folder.list-view .folder-thumb img {
|
||
width: 135px;
|
||
height: 135px;
|
||
}
|
||
|
||
.creating-folder .folder-info {
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.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(220px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
.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),
|
||
min-height 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: 120px;
|
||
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: 150px;
|
||
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: 10px;
|
||
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: 14px;
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.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(150px, 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(130px, 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(220px, 1fr));
|
||
gap: 20px;
|
||
}
|
||
.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:
|
||
transform 0.18s ease,
|
||
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: translateY(-6px) scale(0.965);
|
||
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: translateY(-4px) scale(0.98);
|
||
}
|
||
.media-card.is-selected:hover {
|
||
transform: translateY(-6px) scale(0.965);
|
||
}
|
||
.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: 150px;
|
||
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;
|
||
}
|
||
.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(150px, 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(130px, 1fr));
|
||
}
|
||
}
|
||
/* Folder görünümü */
|
||
.folder-card {
|
||
background: transparent;
|
||
border: none;
|
||
box-shadow: none;
|
||
padding: 24px 12px 18px;
|
||
align-items: center;
|
||
min-height: 210px;
|
||
}
|
||
.folder-card::after {
|
||
display: none;
|
||
}
|
||
.folder-card:hover {
|
||
background: transparent;
|
||
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: 150px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.folder-thumb img {
|
||
width: 135px;
|
||
height: 135px;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.16));
|
||
}
|
||
.folder-info {
|
||
margin-top: 14px;
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
.folder-name {
|
||
font-weight: 600;
|
||
font-size: 15px;
|
||
color: #2d2d2d;
|
||
line-height: 1.35;
|
||
word-break: break-word;
|
||
}
|
||
.folder-card:hover .folder-name,
|
||
.folder-card.is-selected .folder-name {
|
||
color: #1f78ff;
|
||
}
|
||
.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: 135px;
|
||
height: 135px;
|
||
}
|
||
.folder-card.list-view .folder-info {
|
||
margin-top: 0;
|
||
text-align: left;
|
||
}
|
||
.folder-card.list-view .folder-name {
|
||
white-space: nowrap;
|
||
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;
|
||
}
|
||
|
||
/* Eşleştirme Modal Stilleri */
|
||
.match-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 5000;
|
||
background: rgba(10, 10, 10, 0.28);
|
||
backdrop-filter: blur(4px);
|
||
padding: 56px 32px;
|
||
}
|
||
|
||
.match-overlay-content {
|
||
position: relative;
|
||
width: min(60vw, 1100px);
|
||
max-height: 60vh;
|
||
border-radius: 18px;
|
||
background: rgba(12, 12, 12, 0.92);
|
||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.45);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.match-close {
|
||
position: absolute;
|
||
top: 16px;
|
||
right: 20px;
|
||
background: rgba(0, 0, 0, 0.55);
|
||
color: #fafafa;
|
||
border: none;
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 20px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background 0.2s ease;
|
||
z-index: 1;
|
||
}
|
||
|
||
.match-close:hover {
|
||
background: rgba(0, 0, 0, 0.85);
|
||
}
|
||
|
||
.match-header {
|
||
padding: 32px 48px 24px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.match-title {
|
||
margin: 0 0 12px 0;
|
||
font-size: 26px;
|
||
font-weight: 600;
|
||
color: #fafafa;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.match-title i {
|
||
color: #f5b333;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.match-subtitle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 14px;
|
||
color: #c7c7c7;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.match-location {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
max-width: 60%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.match-location .location-text {
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.match-size {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.match-location i,
|
||
.match-size i {
|
||
color: #8d8d8d;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.match-separator {
|
||
color: #7a7a7a;
|
||
}
|
||
|
||
.match-body {
|
||
padding: 28px 48px 36px;
|
||
color: #f5f5f5;
|
||
overflow-y: auto;
|
||
max-height: calc(60vh - 120px);
|
||
}
|
||
|
||
.match-body::-webkit-scrollbar {
|
||
width: 8px;
|
||
}
|
||
|
||
.match-body::-webkit-scrollbar-track {
|
||
background: rgba(0, 0, 0, 0.2);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.match-body::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 4px;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.match-body::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.match-inputs {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.input-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.input-group.title-input {
|
||
flex: 3;
|
||
}
|
||
|
||
.input-group.year-input {
|
||
flex: 1;
|
||
}
|
||
|
||
.input-group label {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #c7c7c7;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.input-group input {
|
||
padding: 12px 16px;
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: #f5f5f5;
|
||
font-size: 15px;
|
||
outline: none;
|
||
transition: border-color 0.2s ease, background 0.2s ease;
|
||
}
|
||
|
||
.input-group input:focus {
|
||
border-color: rgba(245, 179, 51, 0.5);
|
||
background: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.input-group input::placeholder {
|
||
color: #7a7a7a;
|
||
}
|
||
|
||
.match-divider {
|
||
height: 1px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
margin: 0 0 24px 0;
|
||
}
|
||
|
||
.search-loading {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
padding: 40px 20px;
|
||
color: #c7c7c7;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.search-loading i {
|
||
font-size: 20px;
|
||
color: #f5b333;
|
||
}
|
||
|
||
.search-results {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.result-item {
|
||
display: flex;
|
||
gap: 16px;
|
||
padding: 12px;
|
||
border-radius: 10px;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.result-item:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-color: rgba(245, 179, 51, 0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.result-item.applying {
|
||
opacity: 0.6;
|
||
pointer-events: none;
|
||
position: relative;
|
||
}
|
||
|
||
.result-item.applying::after {
|
||
content: "";
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 2px solid rgba(245, 179, 51, 0.3);
|
||
border-top: 2px solid #f5b333;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||
}
|
||
|
||
.result-poster {
|
||
width: 80px;
|
||
height: 120px;
|
||
flex-shrink: 0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.result-poster img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.result-poster-placeholder {
|
||
color: #7a7a7a;
|
||
font-size: 24px;
|
||
}
|
||
|
||
.result-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.result-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #fafafa;
|
||
}
|
||
|
||
.result-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
color: #c7c7c7;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.result-separator {
|
||
color: #7a7a7a;
|
||
}
|
||
|
||
.result-year,
|
||
.result-runtime,
|
||
.result-status {
|
||
color: #c7c7c7;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.result-year i,
|
||
.result-runtime i,
|
||
.result-status i {
|
||
font-size: 13px;
|
||
color: #8d8d8d;
|
||
}
|
||
|
||
.result-genres {
|
||
font-size: 12px;
|
||
color: #8d8d8d;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.3px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.result-cast {
|
||
font-size: 12px;
|
||
color: #9f9f9f;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.result-cast i {
|
||
color: #ffc107;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.result-overview {
|
||
font-size: 13px;
|
||
color: #9f9f9f;
|
||
line-height: 1.5;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.match-overlay {
|
||
padding: 32px 16px;
|
||
}
|
||
|
||
.match-overlay-content {
|
||
width: 96vw;
|
||
}
|
||
|
||
.match-header {
|
||
padding: 28px 32px 20px;
|
||
}
|
||
|
||
.match-title {
|
||
font-size: 22px;
|
||
}
|
||
|
||
.match-body {
|
||
padding: 24px 32px 32px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.match-header {
|
||
padding: 24px 24px 18px;
|
||
}
|
||
|
||
.match-body {
|
||
padding: 20px 24px 28px;
|
||
}
|
||
|
||
.match-inputs {
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
.input-group.title-input,
|
||
.input-group.year-input {
|
||
flex: 1;
|
||
}
|
||
}
|
||
</style>
|