mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Feat: Extension Settings & Library Filtering (#73)
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
<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 }: {
|
||||||
ids: Set<number>;
|
ids: Set<number>;
|
||||||
@@ -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,11 +68,25 @@
|
|||||||
<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-left">
|
<div class="header-inner">
|
||||||
<span class="modal-title">Automation</span>
|
<div class="header-left">
|
||||||
<span class="modal-subtitle">{ids.size} series selected</span>
|
{#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-subtitle">{ids.size} series selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="close-btn" onclick={onClose} aria-label="Close"><X size={16} weight="light" /></button>
|
||||||
</div>
|
</div>
|
||||||
<button class="close-btn" onclick={onClose} aria-label="Close"><X size={16} weight="light" /></button>
|
|
||||||
</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}
|
||||||
|
|||||||
@@ -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 type { MangaPrefs } from "@store/state.svelte";
|
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 { 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,135 +61,150 @@
|
|||||||
<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">
|
||||||
<p class="section-label">Downloads</p>
|
<div class="title-block">
|
||||||
|
<span class="title">{manga?.title ?? "Automation"}</span>
|
||||||
<div class="auto-row">
|
<span class="subtitle">Per-series rules</span>
|
||||||
<div class="auto-info">
|
|
||||||
<span class="auto-label">Auto-download new chapters</span>
|
|
||||||
<span class="auto-desc">Queue new chapters when this series refreshes</span>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button class="close-btn" onclick={onClose} aria-label="Close">
|
||||||
role="switch"
|
<X size={16} weight="light" />
|
||||||
aria-checked={get("autoDownload")}
|
</button>
|
||||||
aria-label="Auto-download new chapters"
|
|
||||||
class="auto-toggle"
|
|
||||||
class:auto-toggle-on={get("autoDownload")}
|
|
||||||
onclick={() => set("autoDownload", !get("autoDownload"))}
|
|
||||||
><span class="auto-toggle-thumb"></span></button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auto-row">
|
<div class="content-body">
|
||||||
<div class="auto-info">
|
|
||||||
<span class="auto-label">Download ahead</span>
|
|
||||||
<span class="auto-desc">Pre-fetch chapters while reading</span>
|
|
||||||
</div>
|
|
||||||
<div class="auto-chip-group">
|
|
||||||
{#each DOWNLOAD_AHEAD_OPTIONS as opt}
|
|
||||||
<button
|
|
||||||
class="auto-chip"
|
|
||||||
class:auto-chip-on={get("downloadAhead") === opt.value}
|
|
||||||
onclick={() => set("downloadAhead", opt.value)}
|
|
||||||
>{opt.label}</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="auto-row">
|
<p class="section-label">Downloads</p>
|
||||||
<div class="auto-info">
|
|
||||||
<span class="auto-label">Max chapters to keep</span>
|
<div class="auto-row">
|
||||||
<span class="auto-desc">Delete oldest downloads when limit is exceeded</span>
|
<div class="auto-info">
|
||||||
|
<span class="auto-label">Auto-download new chapters</span>
|
||||||
|
<span class="auto-desc">Queue new chapters when this series refreshes</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={get("autoDownload")}
|
||||||
|
aria-label="Auto-download new chapters"
|
||||||
|
class="auto-toggle"
|
||||||
|
class:auto-toggle-on={get("autoDownload")}
|
||||||
|
onclick={() => set("autoDownload", !get("autoDownload"))}
|
||||||
|
><span class="auto-toggle-thumb"></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="auto-chip-group">
|
|
||||||
{#each MAX_KEEP_OPTIONS as opt}
|
|
||||||
<button
|
|
||||||
class="auto-chip"
|
|
||||||
class:auto-chip-on={get("maxKeepChapters") === opt.value}
|
|
||||||
onclick={() => set("maxKeepChapters", opt.value)}
|
|
||||||
>{opt.label}</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="auto-row auto-row-col">
|
||||||
|
<div class="auto-info">
|
||||||
<p class="section-label">On Read</p>
|
<span class="auto-label">Download ahead</span>
|
||||||
|
<span class="auto-desc">Pre-fetch chapters while reading</span>
|
||||||
<div class="auto-row">
|
</div>
|
||||||
<div class="auto-info">
|
|
||||||
<span class="auto-label">Delete after reading</span>
|
|
||||||
<span class="auto-desc">Remove download when chapter is marked read</span>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
role="switch"
|
|
||||||
aria-checked={get("deleteOnRead")}
|
|
||||||
aria-label="Delete after reading"
|
|
||||||
class="auto-toggle"
|
|
||||||
class:auto-toggle-on={get("deleteOnRead")}
|
|
||||||
onclick={() => set("deleteOnRead", !get("deleteOnRead"))}
|
|
||||||
><span class="auto-toggle-thumb"></span></button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if get("deleteOnRead")}
|
|
||||||
<div class="auto-row auto-row-sub">
|
|
||||||
<span class="auto-label">Delete delay</span>
|
|
||||||
<div class="auto-chip-group">
|
<div class="auto-chip-group">
|
||||||
{#each DELETE_DELAY_OPTIONS as opt}
|
{#each DOWNLOAD_AHEAD_OPTIONS as opt}
|
||||||
<button
|
<button
|
||||||
class="auto-chip"
|
class="auto-chip"
|
||||||
class:auto-chip-on={get("deleteDelayHours") === opt.value}
|
class:auto-chip-on={get("downloadAhead") === opt.value}
|
||||||
onclick={() => set("deleteDelayHours", opt.value)}
|
onclick={() => set("downloadAhead", opt.value)}
|
||||||
>{opt.label}</button>
|
>{opt.label}</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="auto-row auto-row-col">
|
||||||
|
<div class="auto-info">
|
||||||
<p class="section-label">Updates</p>
|
<span class="auto-label">Max chapters to keep</span>
|
||||||
|
<span class="auto-desc">Delete oldest downloads when limit is exceeded</span>
|
||||||
<div class="auto-row">
|
</div>
|
||||||
<div class="auto-info">
|
<div class="auto-chip-group">
|
||||||
<span class="auto-label">Pause updates</span>
|
{#each MAX_KEEP_OPTIONS as opt}
|
||||||
<span class="auto-desc">Skip this series during global refresh</span>
|
<button
|
||||||
|
class="auto-chip"
|
||||||
|
class:auto-chip-on={get("maxKeepChapters") === opt.value}
|
||||||
|
onclick={() => set("maxKeepChapters", opt.value)}
|
||||||
|
>{opt.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
role="switch"
|
<div class="divider"></div>
|
||||||
aria-checked={get("pauseUpdates")}
|
|
||||||
aria-label="Pause updates"
|
<p class="section-label">On Read</p>
|
||||||
class="auto-toggle"
|
|
||||||
class:auto-toggle-on={get("pauseUpdates")}
|
<div class="auto-row">
|
||||||
onclick={() => set("pauseUpdates", !get("pauseUpdates"))}
|
<div class="auto-info">
|
||||||
><span class="auto-toggle-thumb"></span></button>
|
<span class="auto-label">Delete after reading</span>
|
||||||
|
<span class="auto-desc">Remove download when chapter is marked read</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={get("deleteOnRead")}
|
||||||
|
aria-label="Delete after reading"
|
||||||
|
class="auto-toggle"
|
||||||
|
class:auto-toggle-on={get("deleteOnRead")}
|
||||||
|
onclick={() => set("deleteOnRead", !get("deleteOnRead"))}
|
||||||
|
><span class="auto-toggle-thumb"></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if get("deleteOnRead")}
|
||||||
|
<div class="auto-row auto-row-sub">
|
||||||
|
<span class="auto-label">Delete delay</span>
|
||||||
|
<div class="auto-chip-group">
|
||||||
|
{#each DELETE_DELAY_OPTIONS as opt}
|
||||||
|
<button
|
||||||
|
class="auto-chip"
|
||||||
|
class:auto-chip-on={get("deleteDelayHours") === opt.value}
|
||||||
|
onclick={() => set("deleteDelayHours", opt.value)}
|
||||||
|
>{opt.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<p class="section-label">Updates</p>
|
||||||
|
|
||||||
|
<div class="auto-row">
|
||||||
|
<div class="auto-info">
|
||||||
|
<span class="auto-label">Pause updates</span>
|
||||||
|
<span class="auto-desc">Skip this series during global refresh</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
role="switch"
|
||||||
|
aria-checked={get("pauseUpdates")}
|
||||||
|
aria-label="Pause updates"
|
||||||
|
class="auto-toggle"
|
||||||
|
class:auto-toggle-on={get("pauseUpdates")}
|
||||||
|
onclick={() => set("pauseUpdates", !get("pauseUpdates"))}
|
||||||
|
><span class="auto-toggle-thumb"></span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="auto-row auto-row-col">
|
||||||
|
<div class="auto-info">
|
||||||
|
<span class="auto-label">Refresh interval</span>
|
||||||
|
<span class="auto-desc">How often to check for new chapters</span>
|
||||||
|
</div>
|
||||||
|
<div class="auto-chip-group">
|
||||||
|
{#each REFRESH_INTERVAL_OPTIONS as opt}
|
||||||
|
<button
|
||||||
|
class="auto-chip"
|
||||||
|
class:auto-chip-on={get("refreshInterval") === opt.value}
|
||||||
|
onclick={() => set("refreshInterval", opt.value as MangaPrefs["refreshInterval"])}
|
||||||
|
>{opt.label}</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="auto-row">
|
|
||||||
<div class="auto-info">
|
|
||||||
<span class="auto-label">Refresh interval</span>
|
|
||||||
<span class="auto-desc">How often to check for new chapters</span>
|
|
||||||
</div>
|
|
||||||
<div class="auto-chip-group">
|
|
||||||
{#each REFRESH_INTERVAL_OPTIONS as opt}
|
|
||||||
<button
|
|
||||||
class="auto-chip"
|
|
||||||
class:auto-chip-on={get("refreshInterval") === opt.value}
|
|
||||||
onclick={() => set("refreshInterval", opt.value as MangaPrefs["refreshInterval"])}
|
|
||||||
>{opt.label}</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user