mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 17:29:55 -05:00
Feat: Re-Arrangement of Folders (#86)
This commit is contained in:
@@ -71,8 +71,8 @@
|
||||
|
||||
let activeDragKind: "tab" | null = $state(null);
|
||||
let dragInsertIdx: number = $state(-1);
|
||||
let dragTabId: number | null = $state(null);
|
||||
let dragOverTabId: number | null = $state(null);
|
||||
let dragTabId: string | null = $state(null);
|
||||
let dragOverTabId: string | null = $state(null);
|
||||
|
||||
const DT_TAB = "application/x-moku-tab";
|
||||
const anims = $derived(store.settings.qolAnimations ?? true);
|
||||
@@ -95,7 +95,8 @@
|
||||
const catIds = store.categories.filter(c => c.id !== 0).map(c => String(c.id));
|
||||
const pinned = store.settings.libraryPinnedTabOrder ?? [];
|
||||
const known = new Set([...BUILTIN_TABS, ...catIds]);
|
||||
const ordered = [...pinned.filter(id => known.has(id))];
|
||||
const eligible = pinned.filter(id => known.has(id));
|
||||
const ordered = [...eligible];
|
||||
const inOrder = new Set(ordered);
|
||||
for (const id of [...BUILTIN_TABS, ...catIds]) {
|
||||
if (!inOrder.has(id)) ordered.push(id);
|
||||
@@ -517,45 +518,54 @@
|
||||
});
|
||||
}
|
||||
|
||||
function onTabDragStart(e: DragEvent, cat: Category) {
|
||||
activeDragKind = "tab"; dragTabId = cat.id;
|
||||
function onTabDragStart(e: DragEvent, id: string) {
|
||||
activeDragKind = "tab"; dragTabId = id;
|
||||
e.dataTransfer!.effectAllowed = "move";
|
||||
e.dataTransfer!.setData(DT_TAB, String(cat.id));
|
||||
e.dataTransfer!.setData("text/plain", `tab:${cat.id}`);
|
||||
e.dataTransfer!.setData(DT_TAB, id);
|
||||
e.dataTransfer!.setData("text/plain", `tab:${id}`);
|
||||
}
|
||||
|
||||
function onTabDragOver(e: DragEvent, cat: Category, idx: number) {
|
||||
if (activeDragKind !== "tab" || dragTabId === null || dragTabId === cat.id) return;
|
||||
function onTabDragOver(e: DragEvent, id: string, idx: number) {
|
||||
if (activeDragKind !== "tab" || dragTabId === null || dragTabId === id) return;
|
||||
e.preventDefault(); e.dataTransfer!.dropEffect = "move";
|
||||
dragOverTabId = cat.id;
|
||||
dragOverTabId = id;
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
dragInsertIdx = e.clientX < rect.left + rect.width / 2 ? idx : idx + 1;
|
||||
}
|
||||
|
||||
function onTabDragLeave() { dragOverTabId = null; }
|
||||
|
||||
async function onTabDrop(e: DragEvent, dropCat: Category) {
|
||||
async function onTabDrop(e: DragEvent, dropId: string) {
|
||||
e.preventDefault(); dragOverTabId = null;
|
||||
const insertAt = dragInsertIdx;
|
||||
dragInsertIdx = -1;
|
||||
if (activeDragKind !== "tab" || dragTabId === null || dragTabId === dropCat.id) { dragTabId = null; return; }
|
||||
const dragId = dragTabId; dragTabId = null; activeDragKind = null;
|
||||
const dragStrId = String(dragId);
|
||||
const tabs = [...visibleTabIds];
|
||||
if (activeDragKind !== "tab" || dragTabId === null || dragTabId === dropId) { dragTabId = null; return; }
|
||||
const dragStrId = dragTabId; dragTabId = null; activeDragKind = null;
|
||||
|
||||
const tabs = [...allTabIds];
|
||||
const fromIdx = tabs.indexOf(dragStrId);
|
||||
if (fromIdx < 0) return;
|
||||
const dropIdx = tabs.indexOf(dropId);
|
||||
if (fromIdx < 0 || dropIdx < 0) return;
|
||||
|
||||
const visibleDrop = visibleTabIds[insertAt] ?? null;
|
||||
const destIdx = visibleDrop ? tabs.indexOf(visibleDrop) : tabs.length;
|
||||
|
||||
tabs.splice(fromIdx, 1);
|
||||
const dest = Math.max(0, Math.min(insertAt > fromIdx ? insertAt - 1 : insertAt, tabs.length));
|
||||
tabs.splice(dest, 0, dragStrId);
|
||||
const adjustedDest = Math.max(0, Math.min(destIdx > fromIdx ? destIdx - 1 : destIdx, tabs.length));
|
||||
tabs.splice(adjustedDest, 0, dragStrId);
|
||||
|
||||
updateSettings({ libraryPinnedTabOrder: tabs });
|
||||
const catIds = tabs.filter(id => id !== "library" && id !== "downloaded");
|
||||
const zeroCat = store.categories.filter(c => c.id === 0);
|
||||
const reordered = catIds.map((id, i) => { const c = store.categories.find(x => String(x.id) === id)!; return { ...c, order: i + 1 }; });
|
||||
setCategories([...zeroCat, ...reordered]);
|
||||
const serverPos = catIds.indexOf(dragStrId) + 1;
|
||||
try {
|
||||
await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: dragId, position: serverPos });
|
||||
} catch (err) { console.error("Tab reorder failed:", err); await reloadCategories(); }
|
||||
const dragIsBuiltin = dragStrId === "library" || dragStrId === "downloaded";
|
||||
if (!dragIsBuiltin) {
|
||||
const serverPos = catIds.indexOf(dragStrId) + 1;
|
||||
try {
|
||||
await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: Number(dragStrId), position: serverPos });
|
||||
} catch (err) { console.error("Tab reorder failed:", err); await reloadCategories(); }
|
||||
}
|
||||
}
|
||||
|
||||
function onTabDragEnd() { activeDragKind = null; dragTabId = null; dragOverTabId = null; dragInsertIdx = -1; }
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
search: string;
|
||||
activeDragKind: "tab" | null;
|
||||
dragInsertIdx: number;
|
||||
dragTabId: number | null;
|
||||
dragOverTabId: number | null;
|
||||
dragTabId: string | null;
|
||||
dragOverTabId: string | null;
|
||||
sortPanelOpen: boolean;
|
||||
filterPanelOpen: boolean;
|
||||
tabsEl: HTMLDivElement;
|
||||
@@ -39,10 +39,10 @@
|
||||
onSortPanelToggle: () => void;
|
||||
onFilterPanelToggle: () => void;
|
||||
onOpenDownloadsFolder: () => void;
|
||||
onTabDragStart: (e: DragEvent, cat: Category) => void;
|
||||
onTabDragOver: (e: DragEvent, cat: Category, idx: number) => void;
|
||||
onTabDragStart: (e: DragEvent, id: string) => void;
|
||||
onTabDragOver: (e: DragEvent, id: string, idx: number) => void;
|
||||
onTabDragLeave: () => void;
|
||||
onTabDrop: (e: DragEvent, cat: Category) => void;
|
||||
onTabDrop: (e: DragEvent, id: string) => void;
|
||||
onTabDragEnd: () => void;
|
||||
}
|
||||
|
||||
@@ -100,20 +100,23 @@
|
||||
{#each visibleTabIds as id, idx}
|
||||
{@const cat = visibleCategories.find(c => String(c.id) === id)}
|
||||
{#if id === "library" || id === "downloaded" || cat}
|
||||
{@const isBuiltin = id === "library" || id === "downloaded"}
|
||||
{@const isCompleted = cat && id === String(completedCatId)}
|
||||
{@const isDraggable = true}
|
||||
{#if activeDragKind === "tab" && dragInsertIdx === idx}
|
||||
<div class="tab-insert-bar" aria-hidden="true"></div>
|
||||
{/if}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={tab === id}
|
||||
class:tab-dragging={cat && dragTabId === cat.id}
|
||||
draggable={!!cat && id !== String(completedCatId)}
|
||||
class:tab-dragging={isDraggable && dragTabId === id}
|
||||
draggable={isDraggable}
|
||||
onclick={() => onTabChange(id)}
|
||||
ondragstart={cat && id !== String(completedCatId) ? (e) => onTabDragStart(e, cat) : undefined}
|
||||
ondragover={cat && id !== String(completedCatId) ? (e) => onTabDragOver(e, cat, idx) : undefined}
|
||||
ondragleave={cat && id !== String(completedCatId) ? onTabDragLeave : undefined}
|
||||
ondrop={cat && id !== String(completedCatId) ? (e) => onTabDrop(e, cat) : undefined}
|
||||
ondragend={cat && id !== String(completedCatId) ? onTabDragEnd : undefined}
|
||||
ondragstart={isDraggable ? (e) => onTabDragStart(e, id) : undefined}
|
||||
ondragover={isDraggable ? (e) => onTabDragOver(e, id, idx) : undefined}
|
||||
ondragleave={isDraggable ? onTabDragLeave : undefined}
|
||||
ondrop={isDraggable ? (e) => onTabDrop(e, id) : undefined}
|
||||
ondragend={isDraggable ? onTabDragEnd : undefined}
|
||||
>
|
||||
{#if id === "library"}<Books size={11} weight="bold" />
|
||||
{:else if id === "downloaded"}<DownloadSimple size={11} weight="bold" />
|
||||
|
||||
@@ -6,13 +6,15 @@
|
||||
import type { Category } from "@types";
|
||||
import { store, updateSettings, setCategories } from "@store/state.svelte";
|
||||
|
||||
const completedCat = $derived(store.categories.find(c => c.name === "Completed" && c.id !== 0) ?? null);
|
||||
const completedId = $derived(completedCat ? String(completedCat.id) : null);
|
||||
const sortedCatIds = $derived(store.categories.filter(c => c.id !== 0).map(c => String(c.id)));
|
||||
const orderedCatIds = $derived.by(() => {
|
||||
const order = store.settings.libraryPinnedTabOrder ?? [];
|
||||
const known = new Set(sortedCatIds);
|
||||
return [...order.filter(id => known.has(id)), ...sortedCatIds.filter(id => !order.includes(id))];
|
||||
const completedCat = $derived(store.categories.find(c => c.name === "Completed" && c.id !== 0) ?? null);
|
||||
const completedId = $derived(completedCat ? String(completedCat.id) : null);
|
||||
const sortedCatIds = $derived(store.categories.filter(c => c.id !== 0).map(c => String(c.id)));
|
||||
|
||||
const orderedAllIds = $derived.by(() => {
|
||||
const order = store.settings.libraryPinnedTabOrder ?? [];
|
||||
const allIds = ["library", "downloaded", ...sortedCatIds];
|
||||
const known = new Set(allIds);
|
||||
return [...new Set([...order.filter(id => known.has(id)), ...allIds])];
|
||||
});
|
||||
|
||||
let catsLoading = $state(false);
|
||||
@@ -21,9 +23,9 @@
|
||||
let editingId = $state<number | null>(null);
|
||||
let editingName = $state("");
|
||||
|
||||
let dragId = $state<number | null>(null);
|
||||
let dragOverId = $state<number | null>(null);
|
||||
let dropPosition = $state<"above" | "below" | null>(null);
|
||||
let dragStrId = $state<string | null>(null);
|
||||
let dragOverStrId = $state<string | null>(null);
|
||||
let dropPosition = $state<"above" | "below" | null>(null);
|
||||
|
||||
function isHidden(id: string) {
|
||||
return (store.settings.hiddenLibraryTabs ?? []).includes(id);
|
||||
@@ -92,57 +94,69 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function applyReorder(fromId: number, toId: number) {
|
||||
const zeroCat = store.categories.filter(c => c.id === 0);
|
||||
const sortable = store.categories.filter(c => c.id !== 0).sort((a, b) => a.order - b.order);
|
||||
const fromIdx = sortable.findIndex(c => c.id === fromId);
|
||||
const toIdx = sortable.findIndex(c => c.id === toId);
|
||||
function applyReorder(fromStrId: string, toStrId: string) {
|
||||
const catIds = store.categories.filter(c => c.id !== 0).map(c => String(c.id));
|
||||
const allIds = ["library", "downloaded", ...catIds];
|
||||
const current = store.settings.libraryPinnedTabOrder ?? [];
|
||||
const base = [...new Set([...current.filter(id => allIds.includes(id)), ...allIds])];
|
||||
const fromIdx = base.indexOf(fromStrId);
|
||||
const toIdx = base.indexOf(toStrId);
|
||||
if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx) return;
|
||||
const reordered = [...sortable];
|
||||
const [moved] = reordered.splice(fromIdx, 1);
|
||||
reordered.splice(toIdx, 0, moved);
|
||||
setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
|
||||
base.splice(fromIdx, 1);
|
||||
base.splice(toIdx, 0, fromStrId);
|
||||
updateSettings({ libraryPinnedTabOrder: base });
|
||||
|
||||
const catIds = reordered.map(c => String(c.id));
|
||||
updateSettings({ libraryPinnedTabOrder: ["library", "downloaded", ...catIds] });
|
||||
|
||||
try {
|
||||
const res = await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: fromId, position: toIdx + 1 });
|
||||
const updated = res.updateCategoryOrder.categories.filter(c => c.id !== 0);
|
||||
setCategories([
|
||||
...zeroCat,
|
||||
...updated.sort((a, b) => a.order - b.order).map(fresh => {
|
||||
const existing = store.categories.find(c => c.id === fresh.id);
|
||||
return existing ? { ...existing, ...fresh } : fresh;
|
||||
}),
|
||||
]);
|
||||
} catch (e: any) {
|
||||
catsError = e?.message ?? "Failed to reorder";
|
||||
await loadCategories();
|
||||
const fromNumId = Number(fromStrId);
|
||||
if (!isNaN(fromNumId) && fromStrId !== "library" && fromStrId !== "downloaded") {
|
||||
const zeroCat = store.categories.filter(c => c.id === 0);
|
||||
const sortable = store.categories.filter(c => c.id !== 0).sort((a, b) => a.order - b.order);
|
||||
const sFromIdx = sortable.findIndex(c => c.id === fromNumId);
|
||||
const sToIdx = sortable.findIndex(c => String(c.id) === toStrId);
|
||||
if (sFromIdx >= 0 && sToIdx >= 0 && sFromIdx !== sToIdx) {
|
||||
const reordered = [...sortable];
|
||||
const [moved] = reordered.splice(sFromIdx, 1);
|
||||
reordered.splice(sToIdx, 0, moved);
|
||||
setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
|
||||
gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: fromNumId, position: sToIdx + 1 })
|
||||
.then(res => {
|
||||
const updated = res.updateCategoryOrder.categories.filter(c => c.id !== 0);
|
||||
setCategories([
|
||||
...zeroCat,
|
||||
...updated.sort((a, b) => a.order - b.order).map(fresh => {
|
||||
const existing = store.categories.find(c => c.id === fresh.id);
|
||||
return existing ? { ...existing, ...fresh } : fresh;
|
||||
}),
|
||||
]);
|
||||
})
|
||||
.catch(async (e: any) => {
|
||||
catsError = e?.message ?? "Failed to reorder";
|
||||
await loadCategories();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent, id: number) {
|
||||
dragId = id;
|
||||
if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", String(id)); }
|
||||
function onDragStart(e: DragEvent, id: string) {
|
||||
dragStrId = id;
|
||||
if (e.dataTransfer) { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", id); }
|
||||
}
|
||||
|
||||
function onDragOver(e: DragEvent, id: number) {
|
||||
function onDragOver(e: DragEvent, id: string) {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||
if (dragId === id) return;
|
||||
dragOverId = id;
|
||||
if (dragStrId === id) return;
|
||||
dragOverStrId = id;
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
dropPosition = e.clientY < rect.top + rect.height / 2 ? "above" : "below";
|
||||
}
|
||||
|
||||
function onDrop(e: DragEvent, id: number) {
|
||||
function onDrop(e: DragEvent, id: string) {
|
||||
e.preventDefault();
|
||||
if (dragId !== null && dragId !== id) applyReorder(dragId, id);
|
||||
dragId = null; dragOverId = null; dropPosition = null;
|
||||
if (dragStrId !== null && dragStrId !== id) applyReorder(dragStrId, id);
|
||||
dragStrId = null; dragOverStrId = null; dropPosition = null;
|
||||
}
|
||||
|
||||
function onDragEnd() { dragId = null; dragOverId = null; dropPosition = null; }
|
||||
function onDragEnd() { dragStrId = null; dragOverStrId = null; dropPosition = null; }
|
||||
|
||||
function focusInput(node: HTMLElement) { node.focus(); }
|
||||
|
||||
@@ -166,96 +180,94 @@
|
||||
{#if catsLoading}
|
||||
<p class="s-empty">Loading folders…</p>
|
||||
{:else}
|
||||
<div class="s-folder-row s-folder-row-static">
|
||||
<span class="s-folder-icon-static"><BookmarkSimple size={14} weight="light" /></span>
|
||||
<span class="s-folder-name s-folder-name-static">Saved</span>
|
||||
<span class="s-folder-badge">built-in</span>
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:muted={isHidden("library")} onclick={() => toggleHidden("library")} title={isHidden("library") ? "Show tab in library" : "Hide tab from library"}>
|
||||
{#if isHidden("library")}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon s-btn-icon-lock" disabled title="Built-in tab — cannot be deleted"><Lock size={12} weight="light" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-folder-list" class:is-dragging={dragStrId !== null}>
|
||||
{#each orderedAllIds as id}
|
||||
{@const isBuiltin = id === "library" || id === "downloaded"}
|
||||
{@const isCompleted = id === completedId}
|
||||
{@const cat = isBuiltin ? null : (store.categories.find(c => String(c.id) === id) ?? null)}
|
||||
{@const hidden = isHidden(id)}
|
||||
|
||||
<div class="s-folder-row s-folder-row-static">
|
||||
<span class="s-folder-icon-static"><DownloadSimple size={14} weight="light" /></span>
|
||||
<span class="s-folder-name s-folder-name-static">Downloaded</span>
|
||||
<span class="s-folder-badge">built-in</span>
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:muted={isHidden("downloaded")} onclick={() => toggleHidden("downloaded")} title={isHidden("downloaded") ? "Show tab in library" : "Hide tab from library"}>
|
||||
{#if isHidden("downloaded")}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon s-btn-icon-lock" disabled title="Built-in tab — cannot be deleted"><Lock size={12} weight="light" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if completedCat}
|
||||
<div class="s-folder-row s-folder-row-static">
|
||||
<span class="s-folder-icon-static"><CheckSquare size={14} weight="light" /></span>
|
||||
<span class="s-folder-name s-folder-name-static">{completedCat.name}</span>
|
||||
<span class="s-folder-count">{completedCat.mangas?.nodes.length ?? 0} manga</span>
|
||||
<span class="s-folder-badge">built-in</span>
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:muted={isHidden(String(completedCat.id))} onclick={() => toggleHidden(String(completedCat!.id))} title={isHidden(String(completedCat.id)) ? "Show tab in library" : "Hide tab from library"}>
|
||||
{#if isHidden(String(completedCat.id))}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon s-btn-icon-lock" disabled title="Built-in tab — cannot be deleted"><Lock size={12} weight="light" /></button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="s-folder-divider" aria-hidden="true"></div>
|
||||
|
||||
<div class="s-folder-list" class:is-dragging={dragId !== null}>
|
||||
{#each orderedCatIds.filter(id => id !== completedId) as id}
|
||||
{@const cat = store.categories.find(c => String(c.id) === id) ?? null}
|
||||
{@const hidden = isHidden(id)}
|
||||
{#if cat}
|
||||
{#if isBuiltin || cat}
|
||||
<div
|
||||
class="s-folder-row"
|
||||
class:dragging={dragId === cat.id}
|
||||
class:drop-above={dragOverId === cat.id && dragId !== cat.id && dropPosition === "above"}
|
||||
class:drop-below={dragOverId === cat.id && dragId !== cat.id && dropPosition === "below"}
|
||||
ondragover={(e) => onDragOver(e, cat.id)}
|
||||
ondrop={(e) => onDrop(e, cat.id)}
|
||||
ondragleave={() => { if (dragOverId === cat.id) { dragOverId = null; dropPosition = null; } }}
|
||||
class:dragging={dragStrId === id}
|
||||
class:drop-above={dragOverStrId === id && dragStrId !== id && dropPosition === "above"}
|
||||
class:drop-below={dragOverStrId === id && dragStrId !== id && dropPosition === "below"}
|
||||
draggable="true"
|
||||
ondragstart={(e) => onDragStart(e, id)}
|
||||
ondragover={(e) => onDragOver(e, id)}
|
||||
ondragleave={() => { if (dragOverStrId === id) { dragOverStrId = null; dropPosition = null; } }}
|
||||
ondrop={(e) => onDrop(e, id)}
|
||||
ondragend={onDragEnd}
|
||||
>
|
||||
{#if editingId === cat.id}
|
||||
<input class="s-input full" bind:value={editingName}
|
||||
onkeydown={(e) => { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") { editingId = null; } }}
|
||||
onblur={commitEdit} use:focusInput />
|
||||
<button class="s-btn-icon" onclick={commitEdit} title="Save">✓</button>
|
||||
{:else}
|
||||
<div class="s-folder-identity" draggable="true"
|
||||
ondragstart={(e) => onDragStart(e, cat.id)}
|
||||
ondragend={onDragEnd}>
|
||||
<span class="s-folder-icon">
|
||||
<FolderSimple size={14} weight="light" />
|
||||
<DotsSixVertical size={14} weight="bold" />
|
||||
</span>
|
||||
<span class="s-folder-name" onclick={(e) => { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename">{cat.name}</span>
|
||||
</div>
|
||||
|
||||
<span class="s-folder-count">{cat.mangas?.nodes.length ?? 0} manga</span>
|
||||
{#if isCompleted}
|
||||
|
||||
<span class="s-folder-icon">
|
||||
<CheckSquare size={14} weight="light" />
|
||||
<DotsSixVertical size={14} weight="bold" />
|
||||
</span>
|
||||
<span class="s-folder-name">{cat?.name ?? "Completed"}</span>
|
||||
<span class="s-folder-count">{cat?.mangas?.nodes.length ?? 0} manga</span>
|
||||
<span class="s-folder-badge">built-in</span>
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:active={(store.settings.defaultLibraryCategoryId ?? null) === cat.id} onclick={() => updateSettings({ defaultLibraryCategoryId: (store.settings.defaultLibraryCategoryId ?? null) === cat.id ? null : cat.id })} title={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "Remove as default folder" : "Set as default folder"}>
|
||||
<Star size={13} weight={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "fill" : "light"} />
|
||||
</button>
|
||||
<button class="s-btn-icon" class:muted={hidden} onclick={() => toggleHidden(id)} title={hidden ? "Show in library" : "Hide from library"}>
|
||||
<button class="s-btn-icon" class:muted={hidden} onclick={() => toggleHidden(id)} title={hidden ? "Show tab in library" : "Hide tab from library"}>
|
||||
{#if hidden}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon" class:active={cat.includeInUpdate !== false} class:inactive={cat.includeInUpdate === false} onclick={() => toggleCategoryFlag(cat.id, "includeInUpdate")} title={cat.includeInUpdate !== false ? "Included in updates — click to exclude" : "Excluded from updates — click to include"}>
|
||||
{#if cat.includeInUpdate !== false}<ArrowsClockwise size={13} weight="bold" />{:else}<ArrowsCounterClockwise size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon" class:active={cat.includeInDownload !== false} class:inactive={cat.includeInDownload === false} onclick={() => toggleCategoryFlag(cat.id, "includeInDownload")} title={cat.includeInDownload !== false ? "Included in auto-downloads — click to exclude" : "Excluded from auto-downloads — click to include"}>
|
||||
<DownloadSimple size={13} weight={cat.includeInDownload !== false ? "bold" : "light"} />
|
||||
</button>
|
||||
<button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete folder">
|
||||
<Trash size={12} weight="light" />
|
||||
</button>
|
||||
<button class="s-btn-icon s-btn-icon-lock" disabled title="Built-in tab — cannot be deleted"><Lock size={12} weight="light" /></button>
|
||||
</div>
|
||||
|
||||
{:else if isBuiltin}
|
||||
<span class="s-folder-icon">
|
||||
{#if id === "library"}<BookmarkSimple size={14} weight="light" />{:else}<DownloadSimple size={14} weight="light" />{/if}
|
||||
<DotsSixVertical size={14} weight="bold" />
|
||||
</span>
|
||||
<span class="s-folder-name">{id === "library" ? "Saved" : "Downloaded"}</span>
|
||||
<span class="s-folder-badge">built-in</span>
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:muted={hidden} onclick={() => toggleHidden(id)} title={hidden ? "Show tab in library" : "Hide tab from library"}>
|
||||
{#if hidden}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon s-btn-icon-lock" disabled title="Built-in tab — cannot be deleted"><Lock size={12} weight="light" /></button>
|
||||
</div>
|
||||
|
||||
{:else if cat}
|
||||
{#if editingId === cat.id}
|
||||
<input class="s-input full" bind:value={editingName}
|
||||
onkeydown={(e) => { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") { editingId = null; } }}
|
||||
onblur={commitEdit} use:focusInput />
|
||||
<button class="s-btn-icon" onclick={commitEdit} title="Save">✓</button>
|
||||
{:else}
|
||||
<div class="s-folder-identity" draggable="true"
|
||||
ondragstart={(e) => onDragStart(e, id)}
|
||||
ondragend={onDragEnd}>
|
||||
<span class="s-folder-icon">
|
||||
<FolderSimple size={14} weight="light" />
|
||||
<DotsSixVertical size={14} weight="bold" />
|
||||
</span>
|
||||
<span class="s-folder-name" onclick={(e) => { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename">{cat.name}</span>
|
||||
</div>
|
||||
|
||||
<span class="s-folder-count">{cat.mangas?.nodes.length ?? 0} manga</span>
|
||||
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:active={(store.settings.defaultLibraryCategoryId ?? null) === cat.id} onclick={() => updateSettings({ defaultLibraryCategoryId: (store.settings.defaultLibraryCategoryId ?? null) === cat.id ? null : cat.id })} title={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "Remove as default folder" : "Set as default folder"}>
|
||||
<Star size={13} weight={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "fill" : "light"} />
|
||||
</button>
|
||||
<button class="s-btn-icon" class:muted={hidden} onclick={() => toggleHidden(id)} title={hidden ? "Show in library" : "Hide from library"}>
|
||||
{#if hidden}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon" class:active={cat.includeInUpdate !== false} class:inactive={cat.includeInUpdate === false} onclick={() => toggleCategoryFlag(cat.id, "includeInUpdate")} title={cat.includeInUpdate !== false ? "Included in updates — click to exclude" : "Excluded from updates — click to include"}>
|
||||
{#if cat.includeInUpdate !== false}<ArrowsClockwise size={13} weight="bold" />{:else}<ArrowsCounterClockwise size={13} weight="light" />{/if}
|
||||
</button>
|
||||
<button class="s-btn-icon" class:active={cat.includeInDownload !== false} class:inactive={cat.includeInDownload === false} onclick={() => toggleCategoryFlag(cat.id, "includeInDownload")} title={cat.includeInDownload !== false ? "Included in auto-downloads — click to exclude" : "Excluded from auto-downloads — click to include"}>
|
||||
<DownloadSimple size={13} weight={cat.includeInDownload !== false ? "bold" : "light"} />
|
||||
</button>
|
||||
<button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete folder">
|
||||
<Trash size={12} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -314,28 +326,24 @@
|
||||
.s-folder-row.drop-above::before { top: -1px; }
|
||||
.s-folder-row.drop-below::after { bottom: -1px; }
|
||||
|
||||
.s-folder-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--text-faint);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.s-folder-row-static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.s-folder-icon-static {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-faint);
|
||||
color: var(--text-primary);
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.s-folder-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--text-primary);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.s-folder-icon {
|
||||
display: grid;
|
||||
flex-shrink: 0;
|
||||
@@ -371,14 +379,6 @@
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.s-folder-name-static {
|
||||
cursor: default;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.s-folder-name-static:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.s-folder-actions {
|
||||
display: flex;
|
||||
@@ -400,12 +400,6 @@
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.s-folder-divider {
|
||||
height: 1px;
|
||||
background: var(--border-dim);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.s-btn-icon.active {
|
||||
color: var(--accent, #6c8ef5);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user