Fix: Library FolderSetting Re-Vamp

This commit is contained in:
Youwes09
2026-05-10 12:07:00 -05:00
parent 244447da9b
commit 6d921944ac
5 changed files with 259 additions and 144 deletions
+36 -2
View File
@@ -85,10 +85,40 @@
const hasActiveFilters = $derived(tabStatus !== "ALL" || Object.values(tabFilters).some(Boolean)); 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 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 visibleCategories = $derived((() => {
const defaultId = store.settings.defaultLibraryCategoryId ?? null; const defaultId = store.settings.defaultLibraryCategoryId ?? null;
return store.categories 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) => { .sort((a, b) => {
if (a.id === defaultId) return -1; if (a.id === defaultId) return -1;
if (b.id === defaultId) return 1; if (b.id === defaultId) return 1;
@@ -172,7 +202,7 @@
$effect(() => { filtered; untrack(() => { renderVisible = paginator.reset(); }); }); $effect(() => { filtered; untrack(() => { renderVisible = paginator.reset(); }); });
$effect(() => { retryCount; loading = true; error = null; if (retryCount > 0) cache.clear(CACHE_KEYS.LIBRARY); untrack(() => loadData()); }); $effect(() => { retryCount; loading = true; error = null; if (retryCount > 0) cache.clear(CACHE_KEYS.LIBRARY); untrack(() => loadData()); });
let prevTab = tab; let prevTab = $state(tab);
$effect(() => { $effect(() => {
const nextTab = tab; const nextTab = tab;
if (scrollEl && nextTab !== prevTab) { if (scrollEl && nextTab !== prevTab) {
@@ -605,6 +635,10 @@
{hasActiveFilters} {hasActiveFilters}
{anims} {anims}
{visibleCategories} {visibleCategories}
{visibleTabIds}
{virtualTabIds}
{folderTabIds}
{completedCatId}
{counts} {counts}
{search} {search}
{refreshing} {refreshing}
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { import {
MagnifyingGlass, Books, DownloadSimple, Folder, FolderSimple, MagnifyingGlass, Books, DownloadSimple, Folder, FolderSimple,
SortAscending, CaretUp, CaretDown, ArrowsClockwise, Star, X, SortAscending, CaretUp, CaretDown, ArrowsClockwise, Star, X, CheckSquare,
} from "phosphor-svelte"; } from "phosphor-svelte";
import LibraryFilters from "./LibraryFilters.svelte"; import LibraryFilters from "./LibraryFilters.svelte";
import type { Category } from "@types"; import type { Category } from "@types";
@@ -16,6 +16,10 @@
hasActiveFilters: boolean; hasActiveFilters: boolean;
anims: boolean; anims: boolean;
visibleCategories: Category[]; visibleCategories: Category[];
visibleTabIds: string[];
virtualTabIds: string[];
folderTabIds: string[];
completedCatId: number | null;
counts: Record<string, number>; counts: Record<string, number>;
search: string; search: string;
activeDragKind: "tab" | null; activeDragKind: "tab" | null;
@@ -44,8 +48,8 @@
let { let {
tab, tabSortMode, tabSortDir, tabStatus, tabFilters, hasActiveFilters, tab, tabSortMode, tabSortDir, tabStatus, tabFilters, hasActiveFilters,
anims, visibleCategories, counts, search, refreshing, anims, visibleCategories, visibleTabIds, virtualTabIds, folderTabIds, completedCatId,
refreshProgress, refreshDone, activeDragKind, dragInsertIdx, counts, search, refreshing, refreshProgress, refreshDone, activeDragKind, dragInsertIdx,
dragTabId, dragOverTabId, sortPanelOpen, filterPanelOpen, dragTabId, dragOverTabId, sortPanelOpen, filterPanelOpen,
tabsEl = $bindable(), tabsEl = $bindable(),
onSearchChange, onTabChange, onSortChange, onSortDirToggle, onStatusChange, onSearchChange, onTabChange, onSortChange, onSortDirToggle, onStatusChange,
@@ -67,53 +71,43 @@
const ALL_SORT_MODES: LibrarySortMode[] = [ const ALL_SORT_MODES: LibrarySortMode[] = [
"az", "unreadCount", "totalChapters", "recentlyAdded", "recentlyRead", "latestFetched", "latestUploaded", "az", "unreadCount", "totalChapters", "recentlyAdded", "recentlyRead", "latestFetched", "latestUploaded",
]; ];
const activeCatId = $derived(
tab !== "library" && tab !== "downloaded" ? Number(tab) : null
);
</script> </script>
<div class="header"> <div class="header">
<span class="heading">Library</span> <span class="heading">Library</span>
<div class="tabs" class:tabs-anims={anims} bind:this={tabsEl}> <div class="tabs" class:tabs-anims={anims} bind:this={tabsEl}>
{#each [["library", "Saved"], ["downloaded", "Downloaded"]] as [f, label]} {#each visibleTabIds as id, idx}
<button class="tab" class:active={tab === f} onclick={() => onTabChange(f)}> {@const cat = visibleCategories.find(c => String(c.id) === id)}
{#if f === "library"}<Books size={11} weight="bold" /> {#if id === "library" || id === "downloaded" || cat}
{:else if f === "downloaded"}<DownloadSimple size={11} weight="bold" />{/if} {#if cat && dragInsertIdx === idx && activeDragKind === "tab"}
{label}
<span class="tab-count">{counts[f] ?? 0}</span>
</button>
{/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> <div class="tab-insert-bar" aria-hidden="true"></div>
{/if} {/if}
<button <button
class="tab" class="tab"
class:active={tab === String(cat.id)} class:active={tab === id}
class:tab-dragging={dragTabId === cat.id} class:tab-dragging={cat && dragTabId === cat.id}
draggable="true" draggable={!!cat && id !== String(completedCatId)}
onclick={() => onTabChange(String(cat.id))} onclick={() => onTabChange(id)}
ondragstart={(e) => onTabDragStart(e, cat)} ondragstart={cat && id !== String(completedCatId) ? (e) => onTabDragStart(e, cat) : undefined}
ondragover={(e) => onTabDragOver(e, cat, idx)} ondragover={cat && id !== String(completedCatId) ? (e) => onTabDragOver(e, cat, idx) : undefined}
ondragleave={onTabDragLeave} ondragleave={cat && id !== String(completedCatId) ? onTabDragLeave : undefined}
ondrop={(e) => onTabDrop(e, cat)} ondrop={cat && id !== String(completedCatId) ? (e) => onTabDrop(e, cat) : undefined}
ondragend={onTabDragEnd} ondragend={cat && id !== String(completedCatId) ? onTabDragEnd : undefined}
> >
<Folder size={11} weight="bold" /> {#if id === "library"}<Books size={11} weight="bold" />
{cat.name} {:else if id === "downloaded"}<DownloadSimple size={11} weight="bold" />
<span class="tab-count">{counts[String(cat.id)] ?? 0}</span> {: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> </button>
{#if dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleCategories.length - 1} {#if cat && id !== String(completedCatId) && dragInsertIdx === idx + 1 && activeDragKind === "tab" && idx === visibleTabIds.length - 1}
<div class="tab-insert-bar" aria-hidden="true"></div> <div class="tab-insert-bar" aria-hidden="true"></div>
{/if} {/if}
{/each}
</div>
{/if} {/if}
{/each}
</div> </div>
<div class="header-right"> <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 { 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; } .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; } .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 { 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-scroll { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; min-width: 0; flex-shrink: 1; } .tabs::-webkit-scrollbar { display: none; }
.tabs-scroll::-webkit-scrollbar { display: none; }
.tab-separator { width: 1px; height: 16px; background: var(--border-dim); flex-shrink: 0; margin: 0 2px; }
.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 { 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:hover { color: var(--text-muted); }
.tab.active { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); } .tab.active { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); }
@@ -1,10 +1,19 @@
<script lang="ts"> <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 { gql } from "@api/client";
import { GET_CATEGORIES } from "@api/queries/manga"; import { GET_CATEGORIES } from "@api/queries/manga";
import { CREATE_CATEGORY, UPDATE_CATEGORY, UPDATE_CATEGORIES, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER } from "@api/mutations/manga"; import { CREATE_CATEGORY, UPDATE_CATEGORY, UPDATE_CATEGORIES, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER } from "@api/mutations/manga";
import type { Category } from "@types"; 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 catsLoading = $state(false);
let catsError = $state<string | null>(null); let catsError = $state<string | null>(null);
@@ -16,6 +25,15 @@
let dragOverId = $state<number | null>(null); let dragOverId = $state<number | null>(null);
let dropPosition = $state<"above" | "below" | 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() { async function loadCategories() {
catsLoading = true; catsError = null; catsLoading = true; catsError = null;
try { try {
@@ -84,6 +102,10 @@
const [moved] = reordered.splice(fromIdx, 1); const [moved] = reordered.splice(fromIdx, 1);
reordered.splice(toIdx, 0, moved); reordered.splice(toIdx, 0, moved);
setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]); setCategories([...zeroCat, ...reordered.map((c, i) => ({ ...c, order: i + 1 }))]);
const catIds = reordered.map(c => String(c.id));
updateSettings({ libraryPinnedTabOrder: ["library", "downloaded", ...catIds] });
try { try {
const res = await gql<{ updateCategoryOrder: { categories: Category[] } }>(UPDATE_CATEGORY_ORDER, { id: fromId, position: toIdx + 1 }); 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); const updated = res.updateCategoryOrder.categories.filter(c => c.id !== 0);
@@ -136,31 +158,60 @@
<div class="s-row"> <div class="s-row">
<span class="s-desc">Folders are stored as Suwayomi categories. Changes sync across all clients.</span> <span class="s-desc">Folders are stored as Suwayomi categories. Changes sync across all clients.</span>
</div> </div>
{#if catsError} {#if catsError}
<div class="s-banner s-banner-error">{catsError}</div> <div class="s-banner s-banner-error">{catsError}</div>
{/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()} />
<button class="s-btn s-btn-accent" onclick={createFolder} disabled={!newFolderName.trim()}>
<Plus size={13} weight="bold" /> Create
</button>
</div>
{#if catsLoading} {#if catsLoading}
<p class="s-empty">Loading folders…</p> <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} {:else}
{@const displayCats = store.categories <div class="s-folder-row s-folder-row-static">
.filter(c => c.id !== 0) <span class="s-folder-icon-static"><BookmarkSimple size={14} weight="light" /></span>
.sort((a, b) => { <span class="s-folder-name s-folder-name-static">Saved</span>
const defaultId = store.settings.defaultLibraryCategoryId ?? null; <span class="s-folder-badge">built-in</span>
if (a.id === defaultId) return -1; <div class="s-folder-actions">
if (b.id === defaultId) return 1; <button class="s-btn-icon" class:muted={isHidden("library")} onclick={() => toggleHidden("library")} title={isHidden("library") ? "Show tab in library" : "Hide tab from library"}>
return a.order - b.order; {#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}> <div class="s-folder-list" class:is-dragging={dragId !== null}>
{#each displayCats as cat} {#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 <div
class="s-folder-row" class="s-folder-row"
class:dragging={dragId === cat.id} class:dragging={dragId === cat.id}
@@ -176,11 +227,6 @@
onblur={commitEdit} use:focusInput /> onblur={commitEdit} use:focusInput />
<button class="s-btn-icon" onclick={commitEdit} title="Save"></button> <button class="s-btn-icon" onclick={commitEdit} title="Save"></button>
{:else} {: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" <div class="s-folder-identity" draggable="true"
ondragstart={(e) => onDragStart(e, cat.id)} ondragstart={(e) => onDragStart(e, cat.id)}
ondragend={onDragEnd}> ondragend={onDragEnd}>
@@ -188,55 +234,46 @@
<FolderSimple size={14} weight="light" /> <FolderSimple size={14} weight="light" />
<DotsSixVertical size={14} weight="bold" /> <DotsSixVertical size={14} weight="bold" />
</span> </span>
<span class="s-folder-name" onclick={(e) => { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename"> <span class="s-folder-name" onclick={(e) => { e.stopPropagation(); startEdit(cat.id, cat.name); }} title="Click to rename">{cat.name}</span>
{cat.name}
</span>
</div> </div>
<span class="s-folder-count">{cat.mangas?.nodes.length ?? 0} manga</span> <span class="s-folder-count">{cat.mangas?.nodes.length ?? 0} manga</span>
<div class="s-folder-actions"> <div class="s-folder-actions">
<button class="s-btn-icon" class:active={isDefault} <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"}>
onclick={() => updateSettings({ defaultLibraryCategoryId: isDefault ? null : cat.id })} <Star size={13} weight={(store.settings.defaultLibraryCategoryId ?? null) === cat.id ? "fill" : "light"} />
title={isDefault ? "Remove as default folder" : "Set as default folder"}>
<Star size={13} weight={isDefault ? "fill" : "light"} />
</button> </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={isHidden} {#if hidden}<EyeSlash size={13} weight="light" />{:else}<Eye size={13} weight="light" />{/if}
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>
<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"}>
<button class="s-btn-icon" class:active={inUpdate} class:inactive={!inUpdate} {#if cat.includeInUpdate !== false}<ArrowsClockwise size={13} weight="bold" />{:else}<ArrowsCounterClockwise size={13} weight="light" />{/if}
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>
<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"}>
<button class="s-btn-icon" class:active={inDl} class:inactive={!inDl} <DownloadSimple size={13} weight={cat.includeInDownload !== false ? "bold" : "light"} />
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>
<button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete folder"> <button class="s-btn-icon danger" onclick={() => deleteFolder(cat.id)} title="Delete folder">
<Trash size={12} weight="light" /> <Trash size={12} weight="light" />
</button> </button>
</div> </div>
{/if} {/if}
</div> </div>
{/if}
{/each} {/each}
</div> </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}
{/if}
<div class="s-folder-create">
<input class="s-input full" placeholder="New folder name…" bind:value={newFolderName}
onkeydown={(e) => e.key === "Enter" && createFolder()} />
<button class="s-btn s-btn-accent" onclick={createFolder} disabled={!newFolderName.trim()}>
<Plus size={13} weight="bold" /> Create
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -287,8 +324,16 @@
cursor: grab; cursor: grab;
} }
.s-folder-identity:active { .s-folder-row-static {
cursor: grabbing; cursor: default;
}
.s-folder-icon-static {
display: flex;
align-items: center;
flex-shrink: 0;
color: var(--text-faint);
width: 14px;
} }
.s-folder-icon { .s-folder-icon {
@@ -326,6 +371,15 @@
text-underline-offset: 3px; 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 { .s-folder-actions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -334,6 +388,24 @@
flex-shrink: 0; 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 { .s-btn-icon.active {
color: var(--accent, #6c8ef5); color: var(--accent, #6c8ef5);
} }
@@ -351,4 +423,14 @@
color: var(--text-faint); color: var(--text-faint);
opacity: 0.5; opacity: 0.5;
} }
.s-btn-icon-lock {
opacity: 0.25;
cursor: not-allowed;
}
.s-btn-icon-lock:hover {
opacity: 0.25;
color: inherit;
}
</style> </style>
+3
View File
@@ -43,6 +43,7 @@ function mergeSettings(saved: any): Settings {
mangaPrefs: saved?.settings?.mangaPrefs ?? {}, mangaPrefs: saved?.settings?.mangaPrefs ?? {},
customThemes: saved?.settings?.customThemes ?? [], customThemes: saved?.settings?.customThemes ?? [],
hiddenCategoryIds: saved?.settings?.hiddenCategoryIds ?? [], hiddenCategoryIds: saved?.settings?.hiddenCategoryIds ?? [],
nsfwFilteredTags: saved?.settings?.nsfwFilteredTags ?? DEFAULT_SETTINGS.nsfwFilteredTags,
nsfwAllowedSourceIds: saved?.settings?.nsfwAllowedSourceIds ?? [], nsfwAllowedSourceIds: saved?.settings?.nsfwAllowedSourceIds ?? [],
nsfwBlockedSourceIds: saved?.settings?.nsfwBlockedSourceIds ?? [], nsfwBlockedSourceIds: saved?.settings?.nsfwBlockedSourceIds ?? [],
libraryTabSort: saved?.settings?.libraryTabSort ?? {}, libraryTabSort: saved?.settings?.libraryTabSort ?? {},
@@ -53,6 +54,8 @@ function mergeSettings(saved: any): Settings {
readerPresets: saved?.settings?.readerPresets ?? [], readerPresets: saved?.settings?.readerPresets ?? [],
mangaReaderSettings: saved?.settings?.mangaReaderSettings ?? {}, mangaReaderSettings: saved?.settings?.mangaReaderSettings ?? {},
categoryFrecency: saved?.settings?.categoryFrecency ?? {}, categoryFrecency: saved?.settings?.categoryFrecency ?? {},
hiddenLibraryTabs: saved?.settings?.hiddenLibraryTabs ?? [],
libraryPinnedTabOrder: saved?.settings?.libraryPinnedTabOrder ?? [],
}; };
} }
+4
View File
@@ -126,6 +126,8 @@ export interface Settings {
autoLinkOnOpen: boolean; autoLinkOnOpen: boolean;
downloadToastsEnabled: boolean; downloadToastsEnabled: boolean;
downloadAutoRetry: boolean; downloadAutoRetry: boolean;
hiddenLibraryTabs: string[];
libraryPinnedTabOrder: string[];
} }
export const DEFAULT_SETTINGS: Settings = { export const DEFAULT_SETTINGS: Settings = {
@@ -167,4 +169,6 @@ export const DEFAULT_SETTINGS: Settings = {
autoLinkOnOpen: false, autoLinkOnOpen: false,
downloadToastsEnabled: true, downloadToastsEnabled: true,
downloadAutoRetry: false, downloadAutoRetry: false,
hiddenLibraryTabs: [],
libraryPinnedTabOrder: [],
}; };