Files ekranına sürükleme özelliği eklendi.

This commit is contained in:
2025-10-30 23:50:24 +03:00
parent 95c452107a
commit e3c9b9f4c6

View File

@@ -17,6 +17,10 @@
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 "";
@@ -127,6 +131,32 @@
});
}
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: "" }];
@@ -192,7 +222,7 @@
normalizePath(file.displayParentPath) === normalizePath(path) &&
file.displayName.toLowerCase() !== "info.js",
);
visibleEntries = [...visibleFolders, ...visibleFiles];
applyOrdering(path);
breadcrumbs = computeBreadcrumbs(path);
}
@@ -472,6 +502,96 @@
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,
@@ -1489,7 +1609,12 @@
<div style="font-weight:700">No media found</div>
</div>
{:else}
<div class="gallery" class:list-view={viewMode === "list"}>
<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">
@@ -1508,57 +1633,36 @@
</div>
</div>
{/if}
{#each visibleFolders as folder (folder.name)}
<div
class="media-card folder-card"
class:list-view={viewMode === "list"}
class:is-selected={selectedItems.has(folder.name)}
on:click={() => handleEntryClick(folder)}
>
<div class="folder-thumb">
<img src={FOLDER_ICON_PATH} alt={`${folder.displayName} klasörü`} />
</div>
<div class="folder-info">
<div class="folder-name">{cleanFileName(folder.displayName)}</div>
</div>
<button
class="selection-toggle"
class:is-selected={selectedItems.has(folder.name)}
type="button"
on:click|stopPropagation={() => toggleSelection(folder)}
aria-label={selectedItems.has(folder.name)
? "Seçimi kaldır"
: "Bu öğeyi seç"}
>
{#if selectedItems.has(folder.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(folder, e)}
aria-label="Menü"
>
<i class="fa-solid fa-ellipsis"></i>
</button>
</div>
{/each}
{#each visibleFiles as f (f.name)}
{#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(f.name)}
on:click={() => handleEntryClick(f)}
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 f.thumbnail}
{#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}${f.thumbnail}?token=${localStorage.getItem("token")}&t=${Date.now()}`}
alt={f.name}
src={`${API}${entry.thumbnail}?token=${localStorage.getItem("token")}&t=${Date.now()}`}
alt={entry.name}
class="thumb"
on:load={(e) => e.target.classList.add("loaded")}
/>
@@ -1568,55 +1672,55 @@
</div>
{/if}
<div class="info">
<div class="name">{cleanFileName(f.name)}</div>
<div class="name">{cleanFileName(entry.name)}</div>
<div class="size">
{#if f.progressText}
<span class="progress-text">{f.progressText}</span>
{#if entry.progressText}
<span class="progress-text">{entry.progressText}</span>
{:else}
{formatSize(f.size)}
{formatSize(entry.size)}
{/if}
</div>
<div class="list-meta">
<div class="meta-line primary">
<span>{formatDateTime(f.added || f.completedAt)}</span>
<span>{formatDateTime(entry.added || entry.completedAt)}</span>
<span class="meta-separator">|</span>
<span>{formatSize(f.size)}</span>
<span>{formatSize(entry.size)}</span>
</div>
<div class="meta-line secondary">
{#if f.progressText}
<span class="status-badge">{f.progressText}</span>
{#if entry.progressText}
<span class="status-badge">{entry.progressText}</span>
<span class="meta-separator">|</span>
{/if}
Tracker:
<span class="tracker-name">
{formatTracker(f.tracker)}
{formatTracker(entry.tracker)}
</span>
</div>
{#if f.mediaInfo?.video || f.mediaInfo?.audio}
{#if entry.mediaInfo?.video || entry.mediaInfo?.audio}
<div class="meta-line codecs">
{#if f.extension}
{#if entry.extension}
<span class="codec-chip file-type">
{#if f.type?.startsWith("image/")}
{#if entry.type?.startsWith("image/")}
<i class="fa-solid fa-file-image"></i>
{:else}
<i class="fa-solid fa-file-video"></i>
{/if}
{f.extension.toUpperCase()}
{entry.extension.toUpperCase()}
</span>
{/if}
{#if f.mediaInfo?.video}
{#if entry.mediaInfo?.video}
<span class="codec-chip">
<i class="fa-solid fa-film"></i>
{formatVideoCodec(f.mediaInfo.video)}
{formatVideoCodec(entry.mediaInfo.video)}
</span>
{/if}
{#if f.mediaInfo?.video && f.mediaInfo?.audio}
{#if entry.mediaInfo?.video && entry.mediaInfo?.audio}
<span class="codec-separator">|</span>
{/if}
{#if f.mediaInfo?.audio}
{#if entry.mediaInfo?.audio}
<span class="codec-chip">
<i class="fa-solid fa-volume-high"></i>
{formatAudioCodec(f.mediaInfo.audio)}
{formatAudioCodec(entry.mediaInfo.audio)}
</span>
{/if}
</div>
@@ -1624,28 +1728,29 @@
</div>
</div>
<div class="media-type-icon">
{#if f.type?.startsWith("video/")}
{#if f.seriesEpisode || (f.seriesEpisodes && Object.keys(f.seriesEpisodes).length > 0)}
{#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 f.movieMatch}
{:else if entry.movieMatch}
<i class="fa-solid fa-film"></i>
{:else}
<i class="fa-solid fa-ban"></i>
{/if}
{:else if f.type?.startsWith("image/")}
{: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(f.name)}
class:is-selected={selectedItems.has(entry.name)}
type="button"
on:click|stopPropagation={() => toggleSelection(f)}
aria-label={selectedItems.has(f.name)
on:click|stopPropagation={() => toggleSelection(entry)}
aria-label={selectedItems.has(entry.name)
? "Seçimi kaldır"
: "Bu öğeyi seç"}
>
{#if selectedItems.has(f.name)}
{#if selectedItems.has(entry.name)}
<i class="fa-solid fa-circle-check"></i>
{:else}
<i class="fa-regular fa-circle"></i>
@@ -1654,7 +1759,7 @@
<button
class="menu-toggle"
type="button"
on:click|stopPropagation={(e) => toggleMenu(f, e)}
on:click|stopPropagation={(e) => toggleMenu(entry, e)}
aria-label="Menü"
>
<i class="fa-solid fa-ellipsis"></i>
@@ -2609,6 +2714,17 @@
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;