Files ekranına sürükleme özelliği eklendi.
This commit is contained in:
@@ -17,6 +17,10 @@
|
|||||||
let breadcrumbs = [];
|
let breadcrumbs = [];
|
||||||
let currentFileScope = [];
|
let currentFileScope = [];
|
||||||
let pendingFolders = new Map();
|
let pendingFolders = new Map();
|
||||||
|
let customOrder = new Map();
|
||||||
|
let draggingItem = null;
|
||||||
|
let dragOverItem = null;
|
||||||
|
let lastDragPath = "";
|
||||||
|
|
||||||
const normalizePath = (value) => {
|
const normalizePath = (value) => {
|
||||||
if (!value) return "";
|
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) {
|
function computeBreadcrumbs(path) {
|
||||||
const segments = path ? path.split("/").filter(Boolean) : [];
|
const segments = path ? path.split("/").filter(Boolean) : [];
|
||||||
const crumbs = [{ label: "Home", path: "" }];
|
const crumbs = [{ label: "Home", path: "" }];
|
||||||
@@ -192,7 +222,7 @@
|
|||||||
normalizePath(file.displayParentPath) === normalizePath(path) &&
|
normalizePath(file.displayParentPath) === normalizePath(path) &&
|
||||||
file.displayName.toLowerCase() !== "info.js",
|
file.displayName.toLowerCase() !== "info.js",
|
||||||
);
|
);
|
||||||
visibleEntries = [...visibleFolders, ...visibleFiles];
|
applyOrdering(path);
|
||||||
breadcrumbs = computeBreadcrumbs(path);
|
breadcrumbs = computeBreadcrumbs(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,6 +502,96 @@
|
|||||||
selectedItems = new Set();
|
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(
|
function updateUrlPath(
|
||||||
path,
|
path,
|
||||||
originalPath = currentOriginalPath,
|
originalPath = currentOriginalPath,
|
||||||
@@ -1489,7 +1609,12 @@
|
|||||||
<div style="font-weight:700">No media found</div>
|
<div style="font-weight:700">No media found</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{#if isCreatingFolder}
|
||||||
<div class="creating-folder" class:list-view={viewMode === "list"}>
|
<div class="creating-folder" class:list-view={viewMode === "list"}>
|
||||||
<div class="folder-thumb">
|
<div class="folder-thumb">
|
||||||
@@ -1508,144 +1633,124 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each visibleFolders as folder (folder.name)}
|
{#each visibleEntries as entry (entry.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)}
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
class="media-card"
|
class="media-card"
|
||||||
|
class:folder-card={entry.isDirectory}
|
||||||
class:list-view={viewMode === "list"}
|
class:list-view={viewMode === "list"}
|
||||||
class:is-selected={selectedItems.has(f.name)}
|
class:is-selected={selectedItems.has(entry.name)}
|
||||||
on:click={() => handleEntryClick(f)}
|
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}
|
||||||
<img
|
<div class="folder-thumb">
|
||||||
src={`${API}${f.thumbnail}?token=${localStorage.getItem("token")}&t=${Date.now()}`}
|
<img src={FOLDER_ICON_PATH} alt={`${entry.displayName} klasörü`} />
|
||||||
alt={f.name}
|
</div>
|
||||||
class="thumb"
|
<div class="folder-info">
|
||||||
on:load={(e) => e.target.classList.add("loaded")}
|
<div class="folder-name">{cleanFileName(entry.displayName)}</div>
|
||||||
/>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="thumb placeholder">
|
{#if entry.thumbnail}
|
||||||
<i class="fa-regular fa-image"></i>
|
<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>
|
</div>
|
||||||
{/if}
|
{/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
|
<button
|
||||||
class="selection-toggle"
|
class="selection-toggle"
|
||||||
class:is-selected={selectedItems.has(f.name)}
|
class:is-selected={selectedItems.has(entry.name)}
|
||||||
type="button"
|
type="button"
|
||||||
on:click|stopPropagation={() => toggleSelection(f)}
|
on:click|stopPropagation={() => toggleSelection(entry)}
|
||||||
aria-label={selectedItems.has(f.name)
|
aria-label={selectedItems.has(entry.name)
|
||||||
? "Seçimi kaldır"
|
? "Seçimi kaldır"
|
||||||
: "Bu öğeyi seç"}
|
: "Bu öğeyi seç"}
|
||||||
>
|
>
|
||||||
{#if selectedItems.has(f.name)}
|
{#if selectedItems.has(entry.name)}
|
||||||
<i class="fa-solid fa-circle-check"></i>
|
<i class="fa-solid fa-circle-check"></i>
|
||||||
{:else}
|
{:else}
|
||||||
<i class="fa-regular fa-circle"></i>
|
<i class="fa-regular fa-circle"></i>
|
||||||
@@ -1654,7 +1759,7 @@
|
|||||||
<button
|
<button
|
||||||
class="menu-toggle"
|
class="menu-toggle"
|
||||||
type="button"
|
type="button"
|
||||||
on:click|stopPropagation={(e) => toggleMenu(f, e)}
|
on:click|stopPropagation={(e) => toggleMenu(entry, e)}
|
||||||
aria-label="Menü"
|
aria-label="Menü"
|
||||||
>
|
>
|
||||||
<i class="fa-solid fa-ellipsis"></i>
|
<i class="fa-solid fa-ellipsis"></i>
|
||||||
@@ -2609,6 +2714,17 @@
|
|||||||
font-weight: 600;
|
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 {
|
.floating-delete {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 28px;
|
right: 28px;
|
||||||
|
|||||||
Reference in New Issue
Block a user