Feat: Extension Settings & Library Filtering (#73)

This commit is contained in:
Youwes09
2026-05-15 19:29:00 -05:00
parent 5af80213c7
commit cbf8a7fe13
5 changed files with 597 additions and 142 deletions
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { X } from "phosphor-svelte"; import { X } from "phosphor-svelte";
import { setPref } from "@features/series/lib/mangaPrefs"; import { setPref } from "@features/series/lib/mangaPrefs";
import { store } from "@store/state.svelte"; import { store, DEFAULT_MANGA_PREFS } from "@store/state.svelte";
import { DEFAULT_MANGA_PREFS } from "@store/state.svelte"; import { resolvedCover } from "@core/cover/coverResolver";
import type { MangaPrefs } from "@store/state.svelte"; import type { MangaPrefs } from "@store/state.svelte";
let { ids, onClose }: { let { ids, onClose }: {
@@ -42,6 +42,14 @@
const get = <K extends keyof MangaPrefs>(key: K): MangaPrefs[K] => draft[key]; const get = <K extends keyof MangaPrefs>(key: K): MangaPrefs[K] => draft[key];
const set = <K extends keyof MangaPrefs>(key: K, value: MangaPrefs[K]) => { draft = { ...draft, [key]: value }; }; const set = <K extends keyof MangaPrefs>(key: K, value: MangaPrefs[K]) => { draft = { ...draft, [key]: value }; };
const mosaicCovers = $derived.by(() => {
const idArr = [...ids].slice(0, 9);
return idArr
.map(id => store.library?.find(m => m.id === id))
.filter(Boolean)
.map(m => resolvedCover(m!.id, m!.thumbnailUrl));
});
function apply() { function apply() {
for (const id of ids) { for (const id of ids) {
for (const key of Object.keys(draft) as (keyof MangaPrefs)[]) { for (const key of Object.keys(draft) as (keyof MangaPrefs)[]) {
@@ -60,12 +68,26 @@
<div class="modal" role="dialog" aria-modal="true" aria-label="Bulk Automation"> <div class="modal" role="dialog" aria-modal="true" aria-label="Bulk Automation">
<div class="modal-header"> <div class="modal-header">
<div class="header-inner">
<div class="header-left"> <div class="header-left">
{#if mosaicCovers.length > 0}
<div class="mosaic" aria-hidden="true">
{#each mosaicCovers.slice(0, 5) as src}
<img class="mosaic-tile" {src} alt="" />
{/each}
{#if ids.size > 5}
<span class="mosaic-overflow">+{ids.size - 5}</span>
{/if}
</div>
{/if}
<div class="header-text">
<span class="modal-title">Automation</span> <span class="modal-title">Automation</span>
<span class="modal-subtitle">{ids.size} series selected</span> <span class="modal-subtitle">{ids.size} series selected</span>
</div> </div>
</div>
<button class="close-btn" onclick={onClose} aria-label="Close"><X size={16} weight="light" /></button> <button class="close-btn" onclick={onClose} aria-label="Close"><X size={16} weight="light" /></button>
</div> </div>
</div>
<div class="modal-body"> <div class="modal-body">
@@ -214,13 +236,37 @@
animation: scaleIn 0.15s ease both; animation: scaleIn 0.15s ease both;
} }
.modal-header { .modal-header { border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
.header-inner {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; padding: var(--sp-4) var(--sp-5); gap: var(--sp-3);
} }
.header-left { display: flex; flex-direction: column; gap: 2px; }
.header-left { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; }
.mosaic {
display: flex; align-items: center; flex-shrink: 0;
}
.mosaic-tile {
width: 28px; height: 38px;
object-fit: cover; border-radius: var(--radius-sm);
border: 1px solid var(--border-dim);
margin-left: -6px; box-shadow: -1px 0 0 var(--bg-surface);
}
.mosaic-tile:first-child { margin-left: 0; }
.mosaic-overflow {
font-family: var(--font-ui); font-size: var(--text-2xs);
color: var(--text-faint); letter-spacing: var(--tracking-wide);
margin-left: var(--sp-1); flex-shrink: 0;
}
.header-text { display: flex; flex-direction: column; gap: 2px; }
.modal-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); } .modal-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); }
.modal-subtitle { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); } .modal-subtitle { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.close-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; } .close-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; }
.close-btn:hover { color: var(--text-muted); background: var(--bg-raised); } .close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }
@@ -705,7 +705,7 @@
{/if} {/if}
{#if autoOpen && store.activeManga} {#if autoOpen && store.activeManga}
<AutomationPanel mangaId={store.activeManga.id} onClose={() => autoOpen = false} /> <AutomationPanel mangaId={store.activeManga.id} manga={store.activeManga} onClose={() => autoOpen = false} />
{/if} {/if}
{#if markersOpen && store.activeManga} {#if markersOpen && store.activeManga}
+110 -25
View File
@@ -1,10 +1,15 @@
<script lang="ts"> <script lang="ts">
import { X } from "phosphor-svelte"; import { X } from "phosphor-svelte";
import { getPref, setPref } from "../lib/mangaPrefs"; import { getPref, setPref } from "../lib/mangaPrefs";
import { store } from "@store/state.svelte";
import { resolvedCover } from "@core/cover/coverResolver";
import Thumbnail from "@shared/manga/Thumbnail.svelte";
import type { MangaPrefs } from "@store/state.svelte"; import type { MangaPrefs } from "@store/state.svelte";
import type { Manga } from "@types/index";
let { mangaId, onClose }: { let { mangaId, manga: mangaProp = null, onClose }: {
mangaId: number; mangaId: number;
manga?: Manga | null;
onClose: () => void; onClose: () => void;
} = $props(); } = $props();
@@ -35,9 +40,19 @@
{ value: "manual", label: "Manual" }, { value: "manual", label: "Manual" },
]; ];
const get = <K extends keyof MangaPrefs>(key: K) => getPref(mangaId, key); const defaults = $derived(store.settings.automationDefaults);
function get<K extends keyof MangaPrefs>(key: K): MangaPrefs[K] {
const pref = getPref(mangaId, key);
if (pref !== undefined) return pref;
return (defaults as MangaPrefs | undefined)?.[key] ?? getPref(mangaId, key);
}
const set = <K extends keyof MangaPrefs>(key: K, value: MangaPrefs[K]) => setPref(mangaId, key, value); const set = <K extends keyof MangaPrefs>(key: K, value: MangaPrefs[K]) => setPref(mangaId, key, value);
const manga = $derived(store.library?.find(m => m.id === mangaId) ?? mangaProp);
const coverSrc = $derived(manga ? resolvedCover(manga.id, manga.thumbnailUrl) : null);
function onBackdrop(e: MouseEvent) { function onBackdrop(e: MouseEvent) {
if (e.target === e.currentTarget) onClose(); if (e.target === e.currentTarget) onClose();
} }
@@ -46,15 +61,28 @@
<div class="backdrop" role="presentation" tabindex="-1" onmousedown={onBackdrop}> <div class="backdrop" role="presentation" tabindex="-1" onmousedown={onBackdrop}>
<div class="modal" role="dialog" aria-modal="true" aria-label="Automation"> <div class="modal" role="dialog" aria-modal="true" aria-label="Automation">
<div class="modal-header"> <div class="cover-col">
<div class="header-left"> {#if coverSrc}
<span class="modal-title">Automation</span> <div class="cover-wrap">
<span class="modal-subtitle">Per-series rules</span> <Thumbnail src={coverSrc} alt={manga?.title} class="cover" />
</div> </div>
<button class="close-btn" onclick={onClose} aria-label="Close"><X size={16} weight="light" /></button> {:else}
<div class="cover-placeholder"></div>
{/if}
</div> </div>
<div class="modal-body"> <div class="content">
<div class="content-header">
<div class="title-block">
<span class="title">{manga?.title ?? "Automation"}</span>
<span class="subtitle">Per-series rules</span>
</div>
<button class="close-btn" onclick={onClose} aria-label="Close">
<X size={16} weight="light" />
</button>
</div>
<div class="content-body">
<p class="section-label">Downloads</p> <p class="section-label">Downloads</p>
@@ -73,7 +101,7 @@
><span class="auto-toggle-thumb"></span></button> ><span class="auto-toggle-thumb"></span></button>
</div> </div>
<div class="auto-row"> <div class="auto-row auto-row-col">
<div class="auto-info"> <div class="auto-info">
<span class="auto-label">Download ahead</span> <span class="auto-label">Download ahead</span>
<span class="auto-desc">Pre-fetch chapters while reading</span> <span class="auto-desc">Pre-fetch chapters while reading</span>
@@ -89,7 +117,7 @@
</div> </div>
</div> </div>
<div class="auto-row"> <div class="auto-row auto-row-col">
<div class="auto-info"> <div class="auto-info">
<span class="auto-label">Max chapters to keep</span> <span class="auto-label">Max chapters to keep</span>
<span class="auto-desc">Delete oldest downloads when limit is exceeded</span> <span class="auto-desc">Delete oldest downloads when limit is exceeded</span>
@@ -158,7 +186,7 @@
><span class="auto-toggle-thumb"></span></button> ><span class="auto-toggle-thumb"></span></button>
</div> </div>
<div class="auto-row"> <div class="auto-row auto-row-col">
<div class="auto-info"> <div class="auto-info">
<span class="auto-label">Refresh interval</span> <span class="auto-label">Refresh interval</span>
<span class="auto-desc">How often to check for new chapters</span> <span class="auto-desc">How often to check for new chapters</span>
@@ -176,6 +204,8 @@
</div> </div>
</div> </div>
</div>
</div> </div>
<style> <style>
@@ -187,31 +217,84 @@
} }
.modal { .modal {
width: 420px; max-width: calc(100vw - var(--sp-6)); display: flex; flex-direction: row;
max-height: 80vh; width: 600px; max-width: calc(100vw - var(--sp-6));
display: flex; flex-direction: column; height: 480px; max-height: 85vh;
background: var(--bg-surface); border: 1px solid var(--border-base); background: var(--bg-surface); border: 1px solid var(--border-base);
border-radius: var(--radius-xl); overflow: hidden; border-radius: var(--radius-xl); overflow: hidden;
box-shadow: 0 0 0 1px var(--border-dim), 0 24px 64px rgba(0,0,0,0.6); box-shadow: 0 0 0 1px var(--border-dim), 0 24px 64px rgba(0,0,0,0.6);
animation: scaleIn 0.15s ease both; animation: scaleIn 0.15s ease both;
} }
.modal-header { .cover-col {
display: flex; align-items: center; justify-content: space-between; width: 200px; flex-shrink: 0;
padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; background: var(--bg-raised);
border-right: 1px solid var(--border-dim);
display: flex; flex-direction: column;
padding: var(--sp-4);
overflow: hidden;
}
.cover-wrap { position: relative; width: 100%; flex: 1; min-height: 0; }
:global(.cover) {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover; object-position: center top;
border-radius: var(--radius-md);
border: 1px solid var(--border-dim);
display: block;
}
.cover-placeholder {
position: absolute; inset: 0;
background: var(--bg-overlay);
border-radius: var(--radius-md);
}
.content {
flex: 1; min-width: 0;
display: flex; flex-direction: column;
overflow: hidden;
border-left: 1px solid var(--border-dim);
}
.content-header {
display: flex; align-items: flex-start; justify-content: space-between;
gap: var(--sp-4); padding: var(--sp-5) var(--sp-6) var(--sp-4);
border-bottom: 1px solid var(--border-dim);
flex-shrink: 0;
}
.title-block { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.title {
font-size: var(--text-base); font-weight: var(--weight-medium);
color: var(--text-primary); letter-spacing: var(--tracking-tight);
line-height: var(--leading-tight);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.subtitle {
font-family: var(--font-ui); font-size: var(--text-xs);
color: var(--text-faint); letter-spacing: var(--tracking-wide);
}
.close-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; flex-shrink: 0;
border-radius: var(--radius-sm); color: var(--text-faint);
background: none; border: none; cursor: pointer;
transition: color var(--t-base), background var(--t-base);
} }
.header-left { display: flex; flex-direction: column; gap: 2px; }
.modal-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); letter-spacing: var(--tracking-tight); }
.modal-subtitle { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.close-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: var(--radius-sm); color: var(--text-faint); background: none; border: none; cursor: pointer; transition: color var(--t-base), background var(--t-base); flex-shrink: 0; }
.close-btn:hover { color: var(--text-muted); background: var(--bg-raised); } .close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }
.modal-body { .content-body {
flex: 1; overflow-y: auto; scrollbar-width: none; flex: 1; overflow-y: auto; scrollbar-width: none;
display: flex; flex-direction: column; gap: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-3);
padding: var(--sp-4) var(--sp-5); padding: var(--sp-5) var(--sp-6);
} }
.modal-body::-webkit-scrollbar { display: none; } .content-body::-webkit-scrollbar { display: none; }
.section-label { .section-label {
font-family: var(--font-ui); font-size: var(--text-2xs); font-family: var(--font-ui); font-size: var(--text-2xs);
@@ -222,7 +305,9 @@
.divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) 0; } .divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) 0; }
.auto-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); } .auto-row { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.auto-row-col { flex-direction: column; align-items: flex-start; gap: var(--sp-2); }
.auto-row-sub { padding-left: var(--sp-3); border-left: 2px solid var(--border-dim); } .auto-row-sub { padding-left: var(--sp-3); border-left: 2px solid var(--border-dim); }
.auto-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; } .auto-info { display: flex; flex-direction: column; gap: 2px; flex: 1; min-width: 0; }
.auto-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); } .auto-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); letter-spacing: var(--tracking-wide); }
.auto-desc { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); line-height: var(--leading-snug); } .auto-desc { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); line-height: var(--leading-snug); }
@@ -232,7 +317,7 @@
.auto-toggle-thumb { position: absolute; top: 1px; left: 1px; width: 12px; height: 12px; border-radius: 50%; background: var(--text-faint); transition: transform var(--t-base), background var(--t-base); } .auto-toggle-thumb { position: absolute; top: 1px; left: 1px; width: 12px; height: 12px; border-radius: 50%; background: var(--text-faint); transition: transform var(--t-base), background var(--t-base); }
.auto-toggle-on .auto-toggle-thumb { transform: translateX(12px); background: var(--bg-base); } .auto-toggle-on .auto-toggle-thumb { transform: translateX(12px); background: var(--bg-base); }
.auto-chip-group { display: flex; flex-direction: row; gap: 4px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; } .auto-chip-group { display: flex; flex-direction: row; gap: 4px; flex-wrap: wrap; }
.auto-chip { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 2px 7px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; white-space: nowrap; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); } .auto-chip { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); padding: 2px 7px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; white-space: nowrap; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
.auto-chip:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); } .auto-chip:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); }
.auto-chip-on { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); } .auto-chip-on { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { tick } from "svelte"; import { tick } from "svelte";
import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives, FolderSimple, Wrench, PaintBrush, ListChecks, Lock, ShieldCheck } from "phosphor-svelte"; import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives, FolderSimple, Wrench, PaintBrush, ListChecks, Lock, ShieldCheck, Robot } from "phosphor-svelte";
import { store, setSettingsOpen, updateSettings } from "@store/state.svelte"; import { store, setSettingsOpen, updateSettings } from "@store/state.svelte";
import { eventToKeybind } from "@core/keybinds/keybindEngine"; import { eventToKeybind } from "@core/keybinds/keybindEngine";
import type { Keybinds } from "@types/settings"; import type { Keybinds } from "@types/settings";
@@ -19,16 +19,18 @@
import ContentSettings from "../sections/ContentSettings.svelte"; import ContentSettings from "../sections/ContentSettings.svelte";
import AboutSettings from "../sections/AboutSettings.svelte"; import AboutSettings from "../sections/AboutSettings.svelte";
import DevtoolsSettings from "../sections/DevtoolsSettings.svelte"; import DevtoolsSettings from "../sections/DevtoolsSettings.svelte";
import AutomationSettings from "../sections/AutomationSettings.svelte";
interface Props { onOpenThemeEditor?: (id?: string | null) => void; } interface Props { onOpenThemeEditor?: (id?: string | null) => void; }
let { onOpenThemeEditor }: Props = $props(); let { onOpenThemeEditor }: Props = $props();
type Tab = "general"|"appearance"|"reader"|"library"|"performance"|"keybinds"|"storage"|"folders"|"tracking"|"security"|"content"|"about"|"devtools"; type Tab = "general"|"appearance"|"reader"|"library"|"automation"|"performance"|"keybinds"|"storage"|"folders"|"tracking"|"security"|"content"|"about"|"devtools";
const TABS: { id: Tab; label: string; icon: any }[] = [ const TABS: { id: Tab; label: string; icon: any }[] = [
{ id: "general", label: "General", icon: Gear }, { id: "general", label: "General", icon: Gear },
{ id: "appearance", label: "Appearance", icon: PaintBrush }, { id: "appearance", label: "Appearance", icon: PaintBrush },
{ id: "reader", label: "Reader", icon: Book }, { id: "reader", label: "Reader", icon: Book },
{ id: "library", label: "Library", icon: Image }, { id: "library", label: "Library", icon: Image },
{ id: "automation", label: "Automation", icon: Robot },
{ id: "performance", label: "Performance", icon: Sliders }, { id: "performance", label: "Performance", icon: Sliders },
{ id: "keybinds", label: "Keybinds", icon: Keyboard }, { id: "keybinds", label: "Keybinds", icon: Keyboard },
{ id: "storage", label: "Storage", icon: HardDrives }, { id: "storage", label: "Storage", icon: HardDrives },
@@ -60,7 +62,7 @@
} }
function close() { setSettingsOpen(false); } function close() { setSettingsOpen(false); }
1
let listeningKey: keyof Keybinds | null = $state(null); let listeningKey: keyof Keybinds | null = $state(null);
$effect(() => { $effect(() => {
@@ -163,6 +165,8 @@
<ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {anims} /> <ReaderSettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
{:else if tab === "library"} {:else if tab === "library"}
<LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {anims} /> <LibrarySettings {selectOpen} {closingSelect} {toggleSelect} {anims} />
{:else if tab === "automation"}
<AutomationSettings />
{:else if tab === "performance"} {:else if tab === "performance"}
<PerformanceSettings /> <PerformanceSettings />
{:else if tab === "keybinds"} {:else if tab === "keybinds"}
@@ -0,0 +1,320 @@
<script lang="ts">
import { ArrowCounterClockwise, LockSimple, Warning } from "phosphor-svelte";
import { store, updateSettings, DEFAULT_MANGA_PREFS } from "@store/state.svelte";
import type { MangaPrefs } from "@store/state.svelte";
const DOWNLOAD_AHEAD_OPTIONS = [
{ value: 0, label: "Off" },
{ value: 2, label: "2" },
{ value: 5, label: "5" },
{ value: 10, label: "10" },
];
const MAX_KEEP_OPTIONS = [
{ value: 0, label: "Off" },
{ value: 5, label: "5" },
{ value: 10, label: "10" },
{ value: 25, label: "25" },
];
const DELETE_DELAY_OPTIONS = [
{ value: 0, label: "Now" },
{ value: 24, label: "1 day" },
{ value: 168, label: "1 week" },
];
const REFRESH_INTERVAL_OPTIONS = [
{ value: "daily", label: "Daily" },
{ value: "weekly", label: "Weekly" },
{ value: "manual", label: "Manual" },
];
type GlobalDefaults = Omit<MangaPrefs, "refreshInterval"> & {
refreshInterval: "daily" | "weekly" | "manual";
};
const fallback: GlobalDefaults = {
autoDownload: false,
downloadAhead: 0,
maxKeepChapters: 0,
deleteOnRead: false,
deleteDelayHours: 0,
pauseUpdates: false,
refreshInterval: "weekly",
};
function getGlobal<K extends keyof GlobalDefaults>(key: K): GlobalDefaults[K] {
return (store.settings.automationDefaults as GlobalDefaults | undefined)?.[key] ?? fallback[key];
}
function setGlobal<K extends keyof GlobalDefaults>(key: K, value: GlobalDefaults[K]) {
updateSettings({
automationDefaults: {
...(store.settings.automationDefaults ?? fallback),
[key]: value,
},
});
}
const enforceGlobal = $derived(store.settings.automationEnforceGlobal ?? false);
function toggleEnforce() {
updateSettings({ automationEnforceGlobal: !enforceGlobal });
}
const customCount = $derived(
Object.keys(store.mangaPrefs ?? {}).filter((id) => {
const prefs = (store.mangaPrefs as Record<string, Partial<MangaPrefs>>)[id];
return prefs && Object.keys(prefs).length > 0;
}).length
);
let confirmReset = $state(false);
function resetAllCustoms() {
if (!confirmReset) { confirmReset = true; return; }
const ids = Object.keys(store.mangaPrefs ?? {});
const blank = { ...DEFAULT_MANGA_PREFS };
for (const id of ids) {
for (const key of Object.keys(blank) as (keyof MangaPrefs)[]) {
// setPref(Number(id), key, blank[key] as any)
}
}
updateSettings({ _resetMangaPrefs: Date.now() } as any);
confirmReset = false;
}
function cancelReset() { confirmReset = false; }
</script>
<div class="s-panel">
<div class="s-section">
<p class="s-section-title">Behaviour</p>
<div class="s-section-body">
<label class="s-row">
<div class="s-row-info">
<span class="s-label">Enable automation</span>
<span class="s-desc">Allow per-series and global automation rules to run</span>
</div>
<button
role="switch"
aria-checked={store.settings.automationEnabled ?? false}
aria-label="Enable automation"
class="s-toggle"
class:on={store.settings.automationEnabled ?? false}
onclick={() => updateSettings({ automationEnabled: !(store.settings.automationEnabled ?? false) })}
><span class="s-toggle-thumb"></span></button>
</label>
<label class="s-row">
<div class="s-row-info">
<span class="s-label">Enforce global defaults</span>
<span class="s-desc">Ignore per-series overrides — all series use the global settings below</span>
</div>
<button
role="switch"
aria-checked={enforceGlobal}
aria-label="Enforce global defaults"
class="s-toggle"
class:on={enforceGlobal}
onclick={toggleEnforce}
><span class="s-toggle-thumb"></span></button>
</label>
{#if enforceGlobal}
<div class="s-banner s-banner-info enforce-banner">
<LockSimple size={12} weight="fill" />
<span>Per-series overrides are paused. Disable enforce to allow custom rules.</span>
</div>
{/if}
</div>
</div>
<div class="s-section">
<p class="s-section-title">Global Defaults</p>
<div class="s-section-body">
<p class="sub-head">Downloads</p>
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Auto-download new chapters</span>
<span class="s-desc">Queue new chapters when a series refreshes</span>
</div>
<button
role="switch"
aria-checked={getGlobal("autoDownload")}
aria-label="Auto-download new chapters"
class="s-toggle"
class:on={getGlobal("autoDownload")}
onclick={() => setGlobal("autoDownload", !getGlobal("autoDownload"))}
><span class="s-toggle-thumb"></span></button>
</div>
<div class="s-row chip-row">
<div class="s-row-info">
<span class="s-label">Download ahead</span>
<span class="s-desc">Pre-fetch chapters while reading</span>
</div>
<div class="chip-group">
{#each DOWNLOAD_AHEAD_OPTIONS as opt}
<button
class="s-preset"
class:active={getGlobal("downloadAhead") === opt.value}
onclick={() => setGlobal("downloadAhead", opt.value)}
>{opt.label}</button>
{/each}
</div>
</div>
<div class="s-row chip-row">
<div class="s-row-info">
<span class="s-label">Max chapters to keep</span>
<span class="s-desc">Delete oldest downloads when limit is exceeded</span>
</div>
<div class="chip-group">
{#each MAX_KEEP_OPTIONS as opt}
<button
class="s-preset"
class:active={getGlobal("maxKeepChapters") === opt.value}
onclick={() => setGlobal("maxKeepChapters", opt.value)}
>{opt.label}</button>
{/each}
</div>
</div>
<p class="sub-head sub-head-rule">On Read</p>
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Delete after reading</span>
<span class="s-desc">Remove download when a chapter is marked read</span>
</div>
<button
role="switch"
aria-checked={getGlobal("deleteOnRead")}
aria-label="Delete after reading"
class="s-toggle"
class:on={getGlobal("deleteOnRead")}
onclick={() => setGlobal("deleteOnRead", !getGlobal("deleteOnRead"))}
><span class="s-toggle-thumb"></span></button>
</div>
{#if getGlobal("deleteOnRead")}
<div class="s-row chip-row sub-row">
<span class="s-label">Delete delay</span>
<div class="chip-group">
{#each DELETE_DELAY_OPTIONS as opt}
<button
class="s-preset"
class:active={getGlobal("deleteDelayHours") === opt.value}
onclick={() => setGlobal("deleteDelayHours", opt.value)}
>{opt.label}</button>
{/each}
</div>
</div>
{/if}
<p class="sub-head sub-head-rule">Updates</p>
<div class="s-row chip-row">
<div class="s-row-info">
<span class="s-label">Default refresh interval</span>
<span class="s-desc">How often series check for new chapters by default</span>
</div>
<div class="chip-group">
{#each REFRESH_INTERVAL_OPTIONS as opt}
<button
class="s-preset"
class:active={getGlobal("refreshInterval") === opt.value}
onclick={() => setGlobal("refreshInterval", opt.value as GlobalDefaults["refreshInterval"])}
>{opt.label}</button>
{/each}
</div>
</div>
</div>
</div>
<div class="s-section">
<p class="s-section-title">Custom Overrides</p>
<div class="s-section-body">
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Series with custom rules</span>
<span class="s-desc">Per-series settings set via the series automation panel</span>
</div>
<span class="s-pill" class:on={customCount > 0}>{customCount}</span>
</div>
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Reset all custom rules</span>
<span class="s-desc">Revert every series to the global defaults above</span>
</div>
{#if confirmReset}
<div class="s-btn-row">
<button class="s-btn s-btn-danger" onclick={resetAllCustoms}>
<Warning size={11} weight="fill" /> Confirm reset
</button>
<button class="s-btn" onclick={cancelReset}>Cancel</button>
</div>
{:else}
<button class="s-btn" disabled={customCount === 0} onclick={resetAllCustoms}>
<ArrowCounterClockwise size={11} weight="regular" /> Reset
</button>
{/if}
</div>
</div>
</div>
</div>
<style>
.enforce-banner {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.sub-head {
font-family: var(--font-ui);
font-size: var(--text-2xs);
letter-spacing: var(--tracking-widest);
text-transform: uppercase;
color: var(--text-faint);
margin: 0;
padding: var(--sp-2) var(--sp-4) 0;
}
.sub-head-rule {
border-top: 1px solid var(--border-dim);
padding-top: var(--sp-3);
margin-top: var(--sp-1);
}
.chip-row {
align-items: flex-start;
padding-top: 8px;
padding-bottom: 8px;
}
.chip-group {
display: flex;
flex-direction: row;
gap: 4px;
flex-shrink: 0;
flex-wrap: wrap;
justify-content: flex-end;
}
.sub-row {
padding-left: calc(var(--sp-4) + var(--sp-2));
border-left: 2px solid var(--border-dim);
}
</style>