mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: Library FolderSetting Re-Vamp
This commit is contained in:
@@ -85,10 +85,40 @@
|
||||
const hasActiveFilters = $derived(tabStatus !== "ALL" || Object.values(tabFilters).some(Boolean));
|
||||
const cols = $derived(Math.max(1, Math.floor((containerWidth + CARD_GAP) / (CARD_MIN_W + CARD_GAP))));
|
||||
|
||||
const BUILTIN_TABS = ["library", "downloaded"] as const;
|
||||
|
||||
const completedCatId = $derived(
|
||||
store.categories.find(c => c.name === COMPLETED_NAME && c.id !== 0)?.id ?? null
|
||||
);
|
||||
|
||||
const allTabIds = $derived((() => {
|
||||
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 inOrder = new Set(ordered);
|
||||
for (const id of [...BUILTIN_TABS, ...catIds]) {
|
||||
if (!inOrder.has(id)) ordered.push(id);
|
||||
}
|
||||
return ordered;
|
||||
})());
|
||||
|
||||
const hiddenTabs = $derived(new Set(store.settings.hiddenLibraryTabs ?? []));
|
||||
|
||||
const visibleTabIds = $derived(allTabIds.filter(id => !hiddenTabs.has(id)));
|
||||
|
||||
const virtualTabIds = $derived(visibleTabIds.filter(id =>
|
||||
id === "library" || id === "downloaded" || (completedCatId !== null && id === String(completedCatId))
|
||||
));
|
||||
|
||||
const folderTabIds = $derived(visibleTabIds.filter(id =>
|
||||
id !== "library" && id !== "downloaded" && (completedCatId === null || id !== String(completedCatId))
|
||||
));
|
||||
|
||||
const visibleCategories = $derived((() => {
|
||||
const defaultId = store.settings.defaultLibraryCategoryId ?? null;
|
||||
return store.categories
|
||||
.filter(c => c.id !== 0 && !(store.settings.hiddenCategoryIds ?? []).includes(c.id))
|
||||
.filter(c => c.id !== 0 && !hiddenTabs.has(String(c.id)))
|
||||
.sort((a, b) => {
|
||||
if (a.id === defaultId) return -1;
|
||||
if (b.id === defaultId) return 1;
|
||||
@@ -172,7 +202,7 @@
|
||||
|
||||
$effect(() => { filtered; untrack(() => { renderVisible = paginator.reset(); }); });
|
||||
$effect(() => { retryCount; loading = true; error = null; if (retryCount > 0) cache.clear(CACHE_KEYS.LIBRARY); untrack(() => loadData()); });
|
||||
let prevTab = tab;
|
||||
let prevTab = $state(tab);
|
||||
$effect(() => {
|
||||
const nextTab = tab;
|
||||
if (scrollEl && nextTab !== prevTab) {
|
||||
@@ -605,6 +635,10 @@
|
||||
{hasActiveFilters}
|
||||
{anims}
|
||||
{visibleCategories}
|
||||
{visibleTabIds}
|
||||
{virtualTabIds}
|
||||
{folderTabIds}
|
||||
{completedCatId}
|
||||
{counts}
|
||||
{search}
|
||||
{refreshing}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
MagnifyingGlass, Books, DownloadSimple, Folder, FolderSimple,
|
||||
SortAscending, CaretUp, CaretDown, ArrowsClockwise, Star, X,
|
||||
SortAscending, CaretUp, CaretDown, ArrowsClockwise, Star, X, CheckSquare,
|
||||
} from "phosphor-svelte";
|
||||
import LibraryFilters from "./LibraryFilters.svelte";
|
||||
import type { Category } from "@types";
|
||||
@@ -16,6 +16,10 @@
|
||||
hasActiveFilters: boolean;
|
||||
anims: boolean;
|
||||
visibleCategories: Category[];
|
||||
visibleTabIds: string[];
|
||||
virtualTabIds: string[];
|
||||
folderTabIds: string[];
|
||||
completedCatId: number | null;
|
||||
counts: Record<string, number>;
|
||||
search: string;
|
||||
activeDragKind: "tab" | null;
|
||||
@@ -44,8 +48,8 @@
|
||||
|
||||
let {
|
||||
tab, tabSortMode, tabSortDir, tabStatus, tabFilters, hasActiveFilters,
|
||||
anims, visibleCategories, counts, search, refreshing,
|
||||
refreshProgress, refreshDone, activeDragKind, dragInsertIdx,
|
||||
anims, visibleCategories, visibleTabIds, virtualTabIds, folderTabIds, completedCatId,
|
||||
counts, search, refreshing, refreshProgress, refreshDone, activeDragKind, dragInsertIdx,
|
||||
dragTabId, dragOverTabId, sortPanelOpen, filterPanelOpen,
|
||||
tabsEl = $bindable(),
|
||||
onSearchChange, onTabChange, onSortChange, onSortDirToggle, onStatusChange,
|
||||
@@ -67,53 +71,43 @@
|
||||
const ALL_SORT_MODES: LibrarySortMode[] = [
|
||||
"az", "unreadCount", "totalChapters", "recentlyAdded", "recentlyRead", "latestFetched", "latestUploaded",
|
||||
];
|
||||
|
||||
const activeCatId = $derived(
|
||||
tab !== "library" && tab !== "downloaded" ? Number(tab) : null
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="header">
|
||||
<span class="heading">Library</span>
|
||||
|
||||
<div class="tabs" class:tabs-anims={anims} bind:this={tabsEl}>
|
||||
{#each [["library", "Saved"], ["downloaded", "Downloaded"]] as [f, label]}
|
||||
<button class="tab" class:active={tab === f} onclick={() => onTabChange(f)}>
|
||||
{#if f === "library"}<Books size={11} weight="bold" />
|
||||
{:else if f === "downloaded"}<DownloadSimple size={11} weight="bold" />{/if}
|
||||
{label}
|
||||
<span class="tab-count">{counts[f] ?? 0}</span>
|
||||
</button>
|
||||
{#each visibleTabIds as id, idx}
|
||||
{@const cat = visibleCategories.find(c => String(c.id) === id)}
|
||||
{#if id === "library" || id === "downloaded" || cat}
|
||||
{#if cat && dragInsertIdx === idx && activeDragKind === "tab"}
|
||||
<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)}
|
||||
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}
|
||||
>
|
||||
{#if id === "library"}<Books size={11} weight="bold" />
|
||||
{:else if id === "downloaded"}<DownloadSimple size={11} weight="bold" />
|
||||
{:else if cat && id === String(completedCatId)}<CheckSquare size={11} weight="bold" />
|
||||
{:else if cat}<Folder size={11} weight="bold" />
|
||||
{/if}
|
||||
{id === "library" ? "Saved" : id === "downloaded" ? "Downloaded" : (cat?.name ?? id)}
|
||||
<span class="tab-count">{counts[id] ?? 0}</span>
|
||||
</button>
|
||||
{#if cat && id !== String(completedCatId) && dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleTabIds.length - 1}
|
||||
<div class="tab-insert-bar" aria-hidden="true"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
{#if visibleCategories.length > 0}
|
||||
<div class="tab-separator" aria-hidden="true"></div>
|
||||
<div class="tabs-scroll">
|
||||
{#each visibleCategories as cat, idx}
|
||||
{#if dragInsertIdx === idx && activeDragKind === "tab"}
|
||||
<div class="tab-insert-bar" aria-hidden="true"></div>
|
||||
{/if}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={tab === String(cat.id)}
|
||||
class:tab-dragging={dragTabId === cat.id}
|
||||
draggable="true"
|
||||
onclick={() => onTabChange(String(cat.id))}
|
||||
ondragstart={(e) => onTabDragStart(e, cat)}
|
||||
ondragover={(e) => onTabDragOver(e, cat, idx)}
|
||||
ondragleave={onTabDragLeave}
|
||||
ondrop={(e) => onTabDrop(e, cat)}
|
||||
ondragend={onTabDragEnd}
|
||||
>
|
||||
<Folder size={11} weight="bold" />
|
||||
{cat.name}
|
||||
<span class="tab-count">{counts[String(cat.id)] ?? 0}</span>
|
||||
</button>
|
||||
{#if dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleCategories.length - 1}
|
||||
<div class="tab-insert-bar" aria-hidden="true"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
@@ -204,10 +198,8 @@
|
||||
.header { position: relative; z-index: 100; display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; min-width: 0; }
|
||||
.header-right { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; flex-shrink: 0; }
|
||||
.heading { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; flex-shrink: 0; }
|
||||
.tabs { display: flex; align-items: center; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; position: relative; flex-shrink: 1; min-width: 0; overflow: hidden; }
|
||||
.tabs-scroll { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; min-width: 0; flex-shrink: 1; }
|
||||
.tabs-scroll::-webkit-scrollbar { display: none; }
|
||||
.tab-separator { width: 1px; height: 16px; background: var(--border-dim); flex-shrink: 0; margin: 0 2px; }
|
||||
.tabs { display: flex; align-items: center; gap: 2px; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 2px; position: relative; flex-shrink: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
.tab { position: relative; z-index: 1; display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid transparent; color: var(--text-faint); white-space: nowrap; transition: background var(--t-base), color var(--t-base), border-color var(--t-base); cursor: grab; flex-shrink: 0; }
|
||||
.tab:hover { color: var(--text-muted); }
|
||||
.tab.active { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); }
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { FolderSimple, Plus, Trash, Star, Eye, EyeSlash, ArrowsClockwise, ArrowsCounterClockwise, DownloadSimple, DotsSixVertical } from "phosphor-svelte";
|
||||
import { FolderSimple, Plus, Trash, Star, Eye, EyeSlash, ArrowsClockwise, ArrowsCounterClockwise, DownloadSimple, DotsSixVertical, BookmarkSimple, Lock, CheckSquare } from "phosphor-svelte";
|
||||
import { gql } from "@api/client";
|
||||
import { GET_CATEGORIES } from "@api/queries/manga";
|
||||
import { CREATE_CATEGORY, UPDATE_CATEGORY, UPDATE_CATEGORIES, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER } from "@api/mutations/manga";
|
||||
import type { Category } from "@types";
|
||||
import { store, updateSettings, toggleHiddenCategory, setCategories } from "@store/state.svelte";
|
||||
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))];
|
||||
});
|
||||
|
||||
let catsLoading = $state(false);
|
||||
let catsError = $state<string | null>(null);
|
||||
@@ -16,6 +25,15 @@
|
||||
let dragOverId = $state<number | null>(null);
|
||||
let dropPosition = $state<"above" | "below" | null>(null);
|
||||
|
||||
function isHidden(id: string) {
|
||||
return (store.settings.hiddenLibraryTabs ?? []).includes(id);
|
||||
}
|
||||
|
||||
function toggleHidden(id: string) {
|
||||
const current = store.settings.hiddenLibraryTabs ?? [];
|
||||
updateSettings({ hiddenLibraryTabs: current.includes(id) ? current.filter(x => x !== id) : [...current, id] });
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
catsLoading = true; catsError = null;
|
||||
try {
|
||||
@@ -84,6 +102,10 @@
|
||||
const [moved] = reordered.splice(fromIdx, 1);
|
||||
reordered.splice(toIdx, 0, moved);
|
||||
setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
|
||||
|
||||
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);
|
||||
@@ -136,9 +158,115 @@
|
||||
<div class="s-row">
|
||||
<span class="s-desc">Folders are stored as Suwayomi categories. Changes sync across all clients.</span>
|
||||
</div>
|
||||
|
||||
{#if catsError}
|
||||
<div class="s-banner s-banner-error">{catsError}</div>
|
||||
{/if}
|
||||
|
||||
{#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-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}
|
||||
<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; } }}
|
||||
>
|
||||
{#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>
|
||||
|
||||
<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}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if store.categories.filter(c => c.id !== 0 && c.name !== "Completed").length === 0}
|
||||
<p class="s-empty">No custom folders yet. Create one below.</p>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="s-folder-create">
|
||||
<input class="s-input full" placeholder="New folder name…" bind:value={newFolderName}
|
||||
onkeydown={(e) => e.key === "Enter" && createFolder()} />
|
||||
@@ -146,97 +274,6 @@
|
||||
<Plus size={13} weight="bold" /> Create
|
||||
</button>
|
||||
</div>
|
||||
{#if catsLoading}
|
||||
<p class="s-empty">Loading folders…</p>
|
||||
{:else if store.categories.filter(c => c.id !== 0).length === 0}
|
||||
<p class="s-empty">No folders yet. Create one above.</p>
|
||||
{:else}
|
||||
{@const displayCats = store.categories
|
||||
.filter(c => c.id !== 0)
|
||||
.sort((a, b) => {
|
||||
const defaultId = store.settings.defaultLibraryCategoryId ?? null;
|
||||
if (a.id === defaultId) return -1;
|
||||
if (b.id === defaultId) return 1;
|
||||
return a.order - b.order;
|
||||
})}
|
||||
<div class="s-folder-list" class:is-dragging={dragId !== null}>
|
||||
{#each displayCats as 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; } }}
|
||||
>
|
||||
{#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}
|
||||
{@const isDefault = (store.settings.defaultLibraryCategoryId ?? null) === cat.id}
|
||||
{@const isHidden = (store.settings.hiddenCategoryIds ?? []).includes(cat.id)}
|
||||
{@const inUpdate = cat.includeInUpdate !== false}
|
||||
{@const inDl = cat.includeInDownload !== false}
|
||||
|
||||
<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>
|
||||
|
||||
<div class="s-folder-actions">
|
||||
<button class="s-btn-icon" class:active={isDefault}
|
||||
onclick={() => updateSettings({ defaultLibraryCategoryId: isDefault ? null : cat.id })}
|
||||
title={isDefault ? "Remove as default folder" : "Set as default folder"}>
|
||||
<Star size={13} weight={isDefault ? "fill" : "light"} />
|
||||
</button>
|
||||
|
||||
<button class="s-btn-icon" class:muted={isHidden}
|
||||
onclick={() => toggleHiddenCategory(cat.id)}
|
||||
title={isHidden ? "Show in library" : "Hide from library"}>
|
||||
{#if isHidden}
|
||||
<EyeSlash size={13} weight="light" />
|
||||
{:else}
|
||||
<Eye size={13} weight="light" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="s-btn-icon" class:active={inUpdate} class:inactive={!inUpdate}
|
||||
onclick={() => toggleCategoryFlag(cat.id, "includeInUpdate")}
|
||||
title={inUpdate ? "Included in updates — click to exclude" : "Excluded from updates — click to include"}>
|
||||
{#if inUpdate}
|
||||
<ArrowsClockwise size={13} weight="bold" />
|
||||
{:else}
|
||||
<ArrowsCounterClockwise size={13} weight="light" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button class="s-btn-icon" class:active={inDl} class:inactive={!inDl}
|
||||
onclick={() => toggleCategoryFlag(cat.id, "includeInDownload")}
|
||||
title={inDl ? "Included in auto-downloads — click to exclude" : "Excluded from auto-downloads — click to include"}>
|
||||
<DownloadSimple size={13} weight={inDl ? "bold" : "light"} />
|
||||
</button>
|
||||
|
||||
<button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete folder">
|
||||
<Trash size={12} weight="light" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -287,8 +324,16 @@
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.s-folder-identity:active {
|
||||
cursor: grabbing;
|
||||
.s-folder-row-static {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.s-folder-icon-static {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-faint);
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.s-folder-icon {
|
||||
@@ -326,6 +371,15 @@
|
||||
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;
|
||||
align-items: center;
|
||||
@@ -334,6 +388,24 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.s-folder-badge {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-faint);
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-dim);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.s-folder-divider {
|
||||
height: 1px;
|
||||
background: var(--border-dim);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.s-btn-icon.active {
|
||||
color: var(--accent, #6c8ef5);
|
||||
}
|
||||
@@ -351,4 +423,14 @@
|
||||
color: var(--text-faint);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.s-btn-icon-lock {
|
||||
opacity: 0.25;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.s-btn-icon-lock:hover {
|
||||
opacity: 0.25;
|
||||
color: inherit;
|
||||
}
|
||||
</style>
|
||||
@@ -43,6 +43,7 @@ function mergeSettings(saved: any): Settings {
|
||||
mangaPrefs: saved?.settings?.mangaPrefs ?? {},
|
||||
customThemes: saved?.settings?.customThemes ?? [],
|
||||
hiddenCategoryIds: saved?.settings?.hiddenCategoryIds ?? [],
|
||||
nsfwFilteredTags: saved?.settings?.nsfwFilteredTags ?? DEFAULT_SETTINGS.nsfwFilteredTags,
|
||||
nsfwAllowedSourceIds: saved?.settings?.nsfwAllowedSourceIds ?? [],
|
||||
nsfwBlockedSourceIds: saved?.settings?.nsfwBlockedSourceIds ?? [],
|
||||
libraryTabSort: saved?.settings?.libraryTabSort ?? {},
|
||||
@@ -53,6 +54,8 @@ function mergeSettings(saved: any): Settings {
|
||||
readerPresets: saved?.settings?.readerPresets ?? [],
|
||||
mangaReaderSettings: saved?.settings?.mangaReaderSettings ?? {},
|
||||
categoryFrecency: saved?.settings?.categoryFrecency ?? {},
|
||||
hiddenLibraryTabs: saved?.settings?.hiddenLibraryTabs ?? [],
|
||||
libraryPinnedTabOrder: saved?.settings?.libraryPinnedTabOrder ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,8 @@ export interface Settings {
|
||||
autoLinkOnOpen: boolean;
|
||||
downloadToastsEnabled: boolean;
|
||||
downloadAutoRetry: boolean;
|
||||
hiddenLibraryTabs: string[];
|
||||
libraryPinnedTabOrder: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
@@ -167,4 +169,6 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
autoLinkOnOpen: false,
|
||||
downloadToastsEnabled: true,
|
||||
downloadAutoRetry: false,
|
||||
hiddenLibraryTabs: [],
|
||||
libraryPinnedTabOrder: [],
|
||||
};
|
||||
Reference in New Issue
Block a user