Files ekranına sürükleme özelliği eklendi.
This commit is contained in:
@@ -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,144 +1633,124 @@
|
||||
</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}
|
||||
<img
|
||||
src={`${API}${f.thumbnail}?token=${localStorage.getItem("token")}&t=${Date.now()}`}
|
||||
alt={f.name}
|
||||
class="thumb"
|
||||
on:load={(e) => e.target.classList.add("loaded")}
|
||||
/>
|
||||
{#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}
|
||||
<div class="thumb placeholder">
|
||||
<i class="fa-regular fa-image"></i>
|
||||
{#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}
|
||||
<div class="info">
|
||||
<div class="name">{cleanFileName(f.name)}</div>
|
||||
<div class="size">
|
||||
{#if f.progressText}
|
||||
<span class="progress-text">{f.progressText}</span>
|
||||
{:else}
|
||||
{formatSize(f.size)}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<div class="meta-line primary">
|
||||
<span>{formatDateTime(f.added || f.completedAt)}</span>
|
||||
<span class="meta-separator">|</span>
|
||||
<span>{formatSize(f.size)}</span>
|
||||
</div>
|
||||
<div class="meta-line secondary">
|
||||
{#if f.progressText}
|
||||
<span class="status-badge">{f.progressText}</span>
|
||||
<span class="meta-separator">|</span>
|
||||
{/if}
|
||||
Tracker:
|
||||
<span class="tracker-name">
|
||||
{formatTracker(f.tracker)}
|
||||
</span>
|
||||
</div>
|
||||
{#if f.mediaInfo?.video || f.mediaInfo?.audio}
|
||||
<div class="meta-line codecs">
|
||||
{#if f.extension}
|
||||
<span class="codec-chip file-type">
|
||||
{#if f.type?.startsWith("image/")}
|
||||
<i class="fa-solid fa-file-image"></i>
|
||||
{:else}
|
||||
<i class="fa-solid fa-file-video"></i>
|
||||
{/if}
|
||||
{f.extension.toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
{#if f.mediaInfo?.video}
|
||||
<span class="codec-chip">
|
||||
<i class="fa-solid fa-film"></i>
|
||||
{formatVideoCodec(f.mediaInfo.video)}
|
||||
</span>
|
||||
{/if}
|
||||
{#if f.mediaInfo?.video && f.mediaInfo?.audio}
|
||||
<span class="codec-separator">|</span>
|
||||
{/if}
|
||||
{#if f.mediaInfo?.audio}
|
||||
<span class="codec-chip">
|
||||
<i class="fa-solid fa-volume-high"></i>
|
||||
{formatAudioCodec(f.mediaInfo.audio)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="media-type-icon">
|
||||
{#if f.type?.startsWith("video/")}
|
||||
{#if f.seriesEpisode || (f.seriesEpisodes && Object.keys(f.seriesEpisodes).length > 0)}
|
||||
<i class="fa-solid fa-tv"></i>
|
||||
{:else if f.movieMatch}
|
||||
<i class="fa-solid fa-film"></i>
|
||||
{:else}
|
||||
<i class="fa-solid fa-ban"></i>
|
||||
{/if}
|
||||
{:else if f.type?.startsWith("image/")}
|
||||
<i class="fa-solid fa-image"></i>
|
||||
{/if}
|
||||
</div>
|
||||
<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;
|
||||
|
||||
Reference in New Issue
Block a user