mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
@@ -24,6 +24,7 @@
|
|||||||
import LibraryToolbar from "./LibraryToolbar.svelte";
|
import LibraryToolbar from "./LibraryToolbar.svelte";
|
||||||
import LibraryGrid from "./LibraryGrid.svelte";
|
import LibraryGrid from "./LibraryGrid.svelte";
|
||||||
import LibraryFilters from "./LibraryFilters.svelte";
|
import LibraryFilters from "./LibraryFilters.svelte";
|
||||||
|
import BulkAutomationPanel from "../panels/BulkAutomationPanel.svelte";
|
||||||
import ContextMenu, { type MenuEntry } from "@shared/ui/ContextMenu.svelte";
|
import ContextMenu, { type MenuEntry } from "@shared/ui/ContextMenu.svelte";
|
||||||
|
|
||||||
import { Books, DownloadSimple, Folder, FolderSimple, FolderSimplePlus, Trash, Star, CheckSquare, ArrowSquareOut } from "phosphor-svelte";
|
import { Books, DownloadSimple, Folder, FolderSimple, FolderSimplePlus, Trash, Star, CheckSquare, ArrowSquareOut } from "phosphor-svelte";
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
let selectMode: boolean = $state(false);
|
let selectMode: boolean = $state(false);
|
||||||
let bulkWorking: boolean = $state(false);
|
let bulkWorking: boolean = $state(false);
|
||||||
let bulkMoveOpen: boolean = $state(false);
|
let bulkMoveOpen: boolean = $state(false);
|
||||||
|
let bulkAutomateOpen: boolean = $state(false);
|
||||||
|
|
||||||
let sortPanelOpen: boolean = $state(false);
|
let sortPanelOpen: boolean = $state(false);
|
||||||
let filterPanelOpen: boolean = $state(false);
|
let filterPanelOpen: boolean = $state(false);
|
||||||
@@ -308,6 +310,11 @@
|
|||||||
finally { bulkWorking = false; exitSelectMode(); }
|
finally { bulkWorking = false; exitSelectMode(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bulkAutomate() {
|
||||||
|
if (selectedIds.size === 0) return;
|
||||||
|
bulkAutomateOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
function sanitize(s: string) { return s.replace(/[\/\\?%*:|"<>]/g, "_"); }
|
function sanitize(s: string) { return s.replace(/[\/\\?%*:|"<>]/g, "_"); }
|
||||||
|
|
||||||
async function openMangaFolder(m: Manga) {
|
async function openMangaFolder(m: Manga) {
|
||||||
@@ -507,6 +514,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<LibraryToolbar
|
<LibraryToolbar
|
||||||
|
onclick={(e: MouseEvent) => { if (selectMode && !(e.target as HTMLElement).closest("button, input")) exitSelectMode(); }}
|
||||||
{tab}
|
{tab}
|
||||||
{tabSortMode}
|
{tabSortMode}
|
||||||
{tabSortDir}
|
{tabSortDir}
|
||||||
@@ -577,6 +585,7 @@
|
|||||||
onSelectAll={selectAll}
|
onSelectAll={selectAll}
|
||||||
onBulkMove={(cat) => { bulkMoveOpen = !bulkMoveOpen; }}
|
onBulkMove={(cat) => { bulkMoveOpen = !bulkMoveOpen; }}
|
||||||
onBulkRemove={bulkRemoveFromLibrary}
|
onBulkRemove={bulkRemoveFromLibrary}
|
||||||
|
onBulkAutomate={bulkAutomate}
|
||||||
{bulkWorking}
|
{bulkWorking}
|
||||||
{bulkMoveOpen}
|
{bulkMoveOpen}
|
||||||
{visibleCategories}
|
{visibleCategories}
|
||||||
@@ -591,6 +600,12 @@
|
|||||||
{#if emptyCtx}
|
{#if emptyCtx}
|
||||||
<ContextMenu x={emptyCtx.x} y={emptyCtx.y} items={buildEmptyCtx()} onClose={() => emptyCtx = null} />
|
<ContextMenu x={emptyCtx.x} y={emptyCtx.y} items={buildEmptyCtx()} onClose={() => emptyCtx = null} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if bulkAutomateOpen}
|
||||||
|
<BulkAutomationPanel
|
||||||
|
ids={selectedIds}
|
||||||
|
onClose={() => { bulkAutomateOpen = false; exitSelectMode(); }}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.root { position: relative; display: flex; flex-direction: column; height: 100%; overflow: visible; animation: fadeIn 0.14s ease both; }
|
.root { position: relative; display: flex; flex-direction: column; height: 100%; overflow: visible; animation: fadeIn 0.14s ease both; }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Folder, Trash, CheckSquare, X } from "phosphor-svelte";
|
import { Folder, Trash, CheckSquare, Robot } from "phosphor-svelte";
|
||||||
import Thumbnail from "@shared/manga/Thumbnail.svelte";
|
import Thumbnail from "@shared/manga/Thumbnail.svelte";
|
||||||
import type { Manga, Category } from "@types";
|
import type { Manga, Category } from "@types";
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
onSelectAll: () => void;
|
onSelectAll: () => void;
|
||||||
onBulkMove: (cat: Category) => void;
|
onBulkMove: (cat: Category) => void;
|
||||||
onBulkRemove: () => void;
|
onBulkRemove: () => void;
|
||||||
|
onBulkAutomate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
hasMore, remainingCount, renderLimit, cropCovers, libraryFilter,
|
hasMore, remainingCount, renderLimit, cropCovers, libraryFilter,
|
||||||
bulkWorking, visibleCategories,
|
bulkWorking, visibleCategories,
|
||||||
onCardClick, onCardContextMenu, onCardPointerDown, onCardPointerUp, onCardPointerLeave,
|
onCardClick, onCardContextMenu, onCardPointerDown, onCardPointerUp, onCardPointerLeave,
|
||||||
onLoadMore, onRetry, onExitSelectMode, onSelectAll, onBulkMove, onBulkRemove,
|
onLoadMore, onRetry, onExitSelectMode, onSelectAll, onBulkMove, onBulkRemove, onBulkAutomate,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let bulkMoveOpen: boolean = $state(false);
|
let bulkMoveOpen: boolean = $state(false);
|
||||||
@@ -55,23 +56,19 @@
|
|||||||
|
|
||||||
{#if selectMode}
|
{#if selectMode}
|
||||||
<div class="select-bar">
|
<div class="select-bar">
|
||||||
<div class="select-bar-left">
|
|
||||||
<button class="sel-btn sel-cancel" onclick={onExitSelectMode} title="Cancel (Esc)">
|
|
||||||
<X size={13} weight="bold" />
|
|
||||||
</button>
|
|
||||||
<span class="sel-count">{selectedIds.size} selected</span>
|
<span class="sel-count">{selectedIds.size} selected</span>
|
||||||
<button class="sel-btn sel-all" onclick={onSelectAll} title="Select all (⌘A)">Select all</button>
|
<button class="sel-text-btn" onclick={onSelectAll} title="Select all (⌘A)">Select all</button>
|
||||||
</div>
|
|
||||||
<div class="select-bar-right">
|
<div class="select-bar-right">
|
||||||
{#if visibleCategories.length}
|
{#if visibleCategories.length}
|
||||||
<div class="bulk-move-wrap">
|
<div class="bulk-move-wrap">
|
||||||
<button
|
<button
|
||||||
class="sel-btn sel-move"
|
class="sel-action-btn"
|
||||||
disabled={selectedIds.size === 0 || bulkWorking}
|
disabled={selectedIds.size === 0 || bulkWorking}
|
||||||
onclick={() => bulkMoveOpen = !bulkMoveOpen}
|
onclick={() => bulkMoveOpen = !bulkMoveOpen}
|
||||||
>
|
>
|
||||||
<Folder size={13} weight="bold" />
|
<Folder size={13} weight="bold" />
|
||||||
Move to folder
|
Move
|
||||||
</button>
|
</button>
|
||||||
{#if bulkMoveOpen}
|
{#if bulkMoveOpen}
|
||||||
<div class="bulk-folder-list">
|
<div class="bulk-folder-list">
|
||||||
@@ -85,7 +82,11 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="sel-btn sel-remove" disabled={selectedIds.size === 0 || bulkWorking} onclick={onBulkRemove}>
|
<button class="sel-action-btn" disabled={selectedIds.size === 0 || bulkWorking} onclick={onBulkAutomate}>
|
||||||
|
<Robot size={13} weight="bold" />
|
||||||
|
Automate
|
||||||
|
</button>
|
||||||
|
<button class="sel-action-btn sel-action-danger" disabled={selectedIds.size === 0 || bulkWorking} onclick={onBulkRemove}>
|
||||||
<Trash size={13} weight="bold" />
|
<Trash size={13} weight="bold" />
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="content">
|
<div class="content" onclick={(e) => { if (selectMode && !(e.target as HTMLElement).closest(".card")) onExitSelectMode(); }}>
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#each Array(12) as _}
|
{#each Array(12) as _}
|
||||||
@@ -174,22 +175,17 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content { flex: 1; overflow-y: auto; padding: var(--sp-5) var(--sp-6) var(--sp-6); will-change: scroll-position; -webkit-overflow-scrolling: touch; }
|
.content { flex: 1; overflow-y: auto; padding: var(--sp-5) var(--sp-6) var(--sp-6); will-change: scroll-position; -webkit-overflow-scrolling: touch; }
|
||||||
.select-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); padding: var(--sp-2) var(--sp-6); background: var(--bg-raised); border-bottom: 1px solid var(--accent-dim); flex-shrink: 0; animation: fadeIn 0.1s ease both; }
|
.select-bar { display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2) var(--sp-6); background: var(--bg-raised); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; animation: fadeIn 0.1s ease both; position: relative; z-index: 10; }
|
||||||
.select-bar-left { display: flex; align-items: center; gap: var(--sp-3); }
|
.select-bar-right { display: flex; align-items: center; gap: var(--sp-2); margin-left: auto; position: relative; }
|
||||||
.select-bar-right { display: flex; align-items: center; gap: var(--sp-2); position: relative; }
|
.sel-count { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); white-space: nowrap; }
|
||||||
.sel-count { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--accent-fg); letter-spacing: var(--tracking-wide); }
|
.sel-text-btn { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); background: none; border: none; cursor: pointer; padding: 2px 4px; border-radius: var(--radius-sm); transition: color var(--t-base); }
|
||||||
.sel-btn { 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 var(--border-dim); background: var(--bg-base); color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); white-space: nowrap; }
|
.sel-text-btn:hover { color: var(--text-primary); }
|
||||||
.sel-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
|
.sel-action-btn { display: flex; align-items: center; gap: 5px; font-family: var(--font-ui); font-size: var(--text-xs); padding: 5px 10px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); white-space: nowrap; }
|
||||||
.sel-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
.sel-action-btn:hover:not(:disabled) { color: var(--text-primary); border-color: var(--border-strong); }
|
||||||
.sel-cancel { border-color: transparent; background: transparent; }
|
.sel-action-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||||
.sel-cancel:hover { background: var(--bg-raised); border-color: var(--border-dim); }
|
.sel-action-danger:hover:not(:disabled) { color: var(--color-error, #e05c5c); border-color: color-mix(in srgb, var(--color-error, #e05c5c) 40%, transparent); background: color-mix(in srgb, var(--color-error, #e05c5c) 8%, transparent); }
|
||||||
.sel-move { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
|
||||||
.sel-move:hover:not(:disabled) { background: var(--accent-dim); }
|
|
||||||
.sel-remove { color: var(--color-error, #e05c5c); border-color: color-mix(in srgb, var(--color-error, #e05c5c) 30%, transparent); }
|
|
||||||
.sel-remove:hover:not(:disabled) { background: color-mix(in srgb, var(--color-error, #e05c5c) 12%, transparent); }
|
|
||||||
.sel-all { border-color: transparent; background: transparent; }
|
|
||||||
.bulk-move-wrap { position: relative; }
|
.bulk-move-wrap { position: relative; }
|
||||||
.bulk-folder-list { position: absolute; top: calc(100% + 4px); right: 0; z-index: 200; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 4px; min-width: 160px; box-shadow: 0 8px 24px rgba(0,0,0,0.35); animation: fadeIn 0.1s ease both; }
|
.bulk-folder-list { position: absolute; top: calc(100% + 4px); right: 0; z-index: 9999; background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 4px; min-width: 160px; box-shadow: 0 8px 24px rgba(0,0,0,0.35); animation: fadeIn 0.1s ease both; }
|
||||||
.bulk-folder-item { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); cursor: pointer; text-align: left; transition: background var(--t-base), color var(--t-base); }
|
.bulk-folder-item { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; border-radius: var(--radius-sm); border: none; background: transparent; color: var(--text-muted); font-family: var(--font-ui); font-size: var(--text-xs); cursor: pointer; text-align: left; transition: background var(--t-base), color var(--t-base); }
|
||||||
.bulk-folder-item:hover { background: var(--bg-hover, var(--bg-base)); color: var(--text-primary); }
|
.bulk-folder-item:hover { background: var(--bg-hover, var(--bg-base)); color: var(--text-primary); }
|
||||||
.grid { position: relative; z-index: 1; isolation: isolate; display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); }
|
.grid { position: relative; z-index: 1; isolation: isolate; display: grid; grid-template-columns: repeat(var(--cols, auto-fill), minmax(130px, 1fr)); gap: var(--sp-4); }
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { X } from "phosphor-svelte";
|
||||||
|
import { setPref } from "@features/series/lib/mangaPrefs";
|
||||||
|
import { store } from "@store/state.svelte";
|
||||||
|
import { DEFAULT_MANGA_PREFS } from "@store/state.svelte";
|
||||||
|
import type { MangaPrefs } from "@store/state.svelte";
|
||||||
|
|
||||||
|
let { ids, onClose }: {
|
||||||
|
ids: Set<number>;
|
||||||
|
onClose: () => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
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: "global", label: "Default" },
|
||||||
|
{ value: "daily", label: "Daily" },
|
||||||
|
{ value: "weekly", label: "Weekly" },
|
||||||
|
{ value: "manual", label: "Manual" },
|
||||||
|
];
|
||||||
|
|
||||||
|
let draft: MangaPrefs = $state({ ...DEFAULT_MANGA_PREFS });
|
||||||
|
|
||||||
|
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 }; };
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
for (const id of ids) {
|
||||||
|
for (const key of Object.keys(draft) as (keyof MangaPrefs)[]) {
|
||||||
|
setPref(id, key, draft[key] as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBackdrop(e: MouseEvent) {
|
||||||
|
if (e.target === e.currentTarget) onClose();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="backdrop" role="presentation" tabindex="-1" onmousedown={onBackdrop}>
|
||||||
|
<div class="modal" role="dialog" aria-modal="true" aria-label="Bulk Automation">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<span class="modal-title">Automation</span>
|
||||||
|
<span class="modal-subtitle">{ids.size} series selected</span>
|
||||||
|
</div>
|
||||||
|
<button class="close-btn" onclick={onClose} aria-label="Close"><X size={16} weight="light" /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<p class="section-label">Downloads</p>
|
||||||
|
|
||||||
|
<div class="auto-row">
|
||||||
|
<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 class="auto-row">
|
||||||
|
<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">
|
||||||
|
<div class="auto-info">
|
||||||
|
<span class="auto-label">Max chapters to keep</span>
|
||||||
|
<span class="auto-desc">Delete oldest downloads when limit is exceeded</span>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<p class="section-label">On Read</p>
|
||||||
|
|
||||||
|
<div class="auto-row">
|
||||||
|
<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">
|
||||||
|
{#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">
|
||||||
|
<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 class="modal-footer">
|
||||||
|
<button class="apply-btn" onclick={apply}>Apply to {ids.size} series</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.backdrop {
|
||||||
|
position: fixed; inset: 0; z-index: 300;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
animation: fadeIn 0.1s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
width: 420px; max-width: calc(100vw - var(--sp-6));
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
background: var(--bg-surface); border: 1px solid var(--border-base);
|
||||||
|
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);
|
||||||
|
animation: scaleIn 0.15s ease both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
.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); }
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
flex: 1; overflow-y: auto; scrollbar-width: none;
|
||||||
|
display: flex; flex-direction: column; gap: var(--sp-3);
|
||||||
|
padding: var(--sp-4) var(--sp-5);
|
||||||
|
}
|
||||||
|
.modal-body::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--border-dim); flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.apply-btn {
|
||||||
|
width: 100%; padding: 8px; border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--accent-dim); background: var(--accent-muted);
|
||||||
|
color: var(--accent-fg); font-family: var(--font-ui); font-size: var(--text-xs);
|
||||||
|
letter-spacing: var(--tracking-wide); cursor: pointer;
|
||||||
|
transition: background var(--t-base), border-color var(--t-base);
|
||||||
|
}
|
||||||
|
.apply-btn:hover { background: var(--accent-dim); border-color: var(--accent); }
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-family: var(--font-ui); font-size: var(--text-2xs);
|
||||||
|
letter-spacing: var(--tracking-widest); color: var(--text-faint);
|
||||||
|
text-transform: uppercase; margin: 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-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-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-toggle { width: 28px; height: 16px; border-radius: var(--radius-full); border: 1px solid var(--border-strong); background: var(--bg-overlay); cursor: pointer; padding: 0; flex-shrink: 0; position: relative; transition: background var(--t-base), border-color var(--t-base); }
|
||||||
|
.auto-toggle-on { background: var(--accent); border-color: var(--accent); }
|
||||||
|
.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-chip-group { display: flex; flex-direction: row; gap: 4px; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
||||||
|
.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-on { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||||
|
|
||||||
|
@keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } }
|
||||||
|
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
||||||
|
</style>
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-header-right">
|
<div class="list-header-right">
|
||||||
<!-- Jump to chapter -->
|
|
||||||
<div class="jump-wrap">
|
<div class="jump-wrap">
|
||||||
<button class="icon-btn" class:active={jumpOpen} onclick={() => { jumpOpen = !jumpOpen; jumpInput = ""; }} title="Jump to chapter">
|
<button class="icon-btn" class:active={jumpOpen} onclick={() => { jumpOpen = !jumpOpen; jumpInput = ""; }} title="Jump to chapter">
|
||||||
<MagnifyingGlass size={14} weight="light" />
|
<MagnifyingGlass size={14} weight="light" />
|
||||||
@@ -191,7 +191,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scanlator filter -->
|
|
||||||
{#if availableScanlators.length > 1}
|
{#if availableScanlators.length > 1}
|
||||||
<div class="scan-filter-wrap">
|
<div class="scan-filter-wrap">
|
||||||
<button class="icon-btn" class:active={scanlatorFilter.length > 0 || scanlatorBlacklist.length > 0} onclick={() => scanFilterOpen = !scanFilterOpen} title="Filter by scanlator">
|
<button class="icon-btn" class:active={scanlatorFilter.length > 0 || scanlatorBlacklist.length > 0} onclick={() => scanFilterOpen = !scanFilterOpen} title="Filter by scanlator">
|
||||||
@@ -245,12 +245,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Refresh -->
|
|
||||||
<button class="icon-btn" onclick={onRefresh} disabled={refreshing}>
|
<button class="icon-btn" onclick={onRefresh} disabled={refreshing}>
|
||||||
<ArrowsClockwise size={14} weight="light" class={refreshing ? "anim-spin" : ""} />
|
<ArrowsClockwise size={14} weight="light" class={refreshing ? "anim-spin" : ""} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Folder picker -->
|
|
||||||
<div class="fp-wrap" bind:this={folderPickerRef}>
|
<div class="fp-wrap" bind:this={folderPickerRef}>
|
||||||
<button class="icon-btn" class:active={hasFolders} onclick={() => folderPickerOpen = !folderPickerOpen}>
|
<button class="icon-btn" class:active={hasFolders} onclick={() => folderPickerOpen = !folderPickerOpen}>
|
||||||
<FolderSimplePlus size={14} weight={hasFolders ? "fill" : "light"} />
|
<FolderSimplePlus size={14} weight={hasFolders ? "fill" : "light"} />
|
||||||
@@ -283,7 +283,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Download dropdown -->
|
|
||||||
{#if chapters.length > 0}
|
{#if chapters.length > 0}
|
||||||
<div class="dl-wrap" bind:this={dlDropRef}>
|
<div class="dl-wrap" bind:this={dlDropRef}>
|
||||||
<button class="icon-btn dl-unified-btn" class:active={dlOpen} class:dl-has-count={downloadedCount > 0} onclick={() => dlOpen = !dlOpen} title="Download options">
|
<button class="icon-btn dl-unified-btn" class:active={dlOpen} class:dl-has-count={downloadedCount > 0} onclick={() => dlOpen = !dlOpen} title="Download options">
|
||||||
@@ -343,7 +343,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Top pagination -->
|
|
||||||
{#if totalPages > 1}
|
{#if totalPages > 1}
|
||||||
<div class="pagination">
|
<div class="pagination">
|
||||||
<button class="page-btn" onclick={() => onPageChange(Math.max(1, chapterPage - 1))} disabled={chapterPage === 1}>←</button>
|
<button class="page-btn" onclick={() => onPageChange(Math.max(1, chapterPage - 1))} disabled={chapterPage === 1}>←</button>
|
||||||
@@ -355,7 +355,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* ─── Header bar ──────────────────────────────────────────── */
|
|
||||||
.list-header {
|
.list-header {
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border-dim);
|
padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--border-dim);
|
||||||
@@ -364,7 +363,6 @@
|
|||||||
.list-header-left,
|
.list-header-left,
|
||||||
.list-header-right { display: flex; align-items: center; gap: var(--sp-1); }
|
.list-header-right { display: flex; align-items: center; gap: var(--sp-1); }
|
||||||
|
|
||||||
/* ─── Sort ────────────────────────────────────────────────── */
|
|
||||||
.sort-btn {
|
.sort-btn {
|
||||||
display: flex; align-items: center; gap: 5px;
|
display: flex; align-items: center; gap: 5px;
|
||||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||||
@@ -390,7 +388,6 @@
|
|||||||
.sort-option.active { color: var(--accent-fg); }
|
.sort-option.active { color: var(--accent-fg); }
|
||||||
.sort-divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) var(--sp-2); }
|
.sort-divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) var(--sp-2); }
|
||||||
|
|
||||||
/* ─── Icon buttons ────────────────────────────────────────── */
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
width: 28px; height: 28px; border-radius: var(--radius-md);
|
width: 28px; height: 28px; border-radius: var(--radius-md);
|
||||||
@@ -402,7 +399,6 @@
|
|||||||
.icon-btn.active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
.icon-btn.active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||||
.icon-btn:disabled { opacity: 0.3; cursor: default; }
|
.icon-btn:disabled { opacity: 0.3; cursor: default; }
|
||||||
|
|
||||||
/* ─── Jump ────────────────────────────────────────────────── */
|
|
||||||
.jump-wrap { position: relative; }
|
.jump-wrap { position: relative; }
|
||||||
.jump-popover {
|
.jump-popover {
|
||||||
position: absolute; top: calc(100% + 4px); right: 0; width: 220px;
|
position: absolute; top: calc(100% + 4px); right: 0; width: 220px;
|
||||||
@@ -429,7 +425,6 @@
|
|||||||
.jump-go:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast, #fff); }
|
.jump-go:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-contrast, #fff); }
|
||||||
.jump-none { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); padding: 4px var(--sp-1); letter-spacing: var(--tracking-wide); }
|
.jump-none { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); padding: 4px var(--sp-1); letter-spacing: var(--tracking-wide); }
|
||||||
|
|
||||||
/* ─── Folder picker ───────────────────────────────────────── */
|
|
||||||
.fp-wrap { position: relative; }
|
.fp-wrap { position: relative; }
|
||||||
.fp-menu {
|
.fp-menu {
|
||||||
position: absolute; top: calc(100% + 4px); right: 0; min-width: 180px;
|
position: absolute; top: calc(100% + 4px); right: 0; min-width: 180px;
|
||||||
@@ -476,7 +471,6 @@
|
|||||||
}
|
}
|
||||||
.fp-new:hover { color: var(--text-secondary); background: var(--bg-overlay); }
|
.fp-new:hover { color: var(--text-secondary); background: var(--bg-overlay); }
|
||||||
|
|
||||||
/* ─── Download dropdown ───────────────────────────────────── */
|
|
||||||
.dl-wrap { position: relative; }
|
.dl-wrap { position: relative; }
|
||||||
.dl-dropdown {
|
.dl-dropdown {
|
||||||
position: absolute; top: calc(100% + 4px); right: 0; min-width: 220px;
|
position: absolute; top: calc(100% + 4px); right: 0; min-width: 220px;
|
||||||
@@ -545,7 +539,6 @@
|
|||||||
.dl-unified-btn.dl-has-count:hover { background: var(--accent-muted); border-color: var(--accent); opacity: 0.9; }
|
.dl-unified-btn.dl-has-count:hover { background: var(--accent-muted); border-color: var(--accent); opacity: 0.9; }
|
||||||
.dl-unified-btn.active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
.dl-unified-btn.active { color: var(--accent-fg); border-color: var(--accent-dim); background: var(--accent-muted); }
|
||||||
|
|
||||||
/* ─── Pagination (top) ────────────────────────────────────── */
|
|
||||||
.pagination { display: flex; align-items: center; gap: var(--sp-2); }
|
.pagination { display: flex; align-items: center; gap: var(--sp-2); }
|
||||||
.page-btn {
|
.page-btn {
|
||||||
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
|
||||||
@@ -557,7 +550,6 @@
|
|||||||
.page-btn:disabled { opacity: 0.3; cursor: default; }
|
.page-btn:disabled { opacity: 0.3; cursor: default; }
|
||||||
.page-num { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
.page-num { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||||
|
|
||||||
/* ─── Selection toolbar ───────────────────────────────────── */
|
|
||||||
.sel-count {
|
.sel-count {
|
||||||
font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted);
|
font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted);
|
||||||
letter-spacing: var(--tracking-wide); padding: 0 var(--sp-1);
|
letter-spacing: var(--tracking-wide); padding: 0 var(--sp-1);
|
||||||
@@ -572,7 +564,6 @@
|
|||||||
.sel-action-danger { color: var(--color-error) !important; }
|
.sel-action-danger { color: var(--color-error) !important; }
|
||||||
.sel-action-danger:hover { background: var(--color-error-bg) !important; border-color: var(--color-error) !important; }
|
.sel-action-danger:hover { background: var(--color-error-bg) !important; border-color: var(--color-error) !important; }
|
||||||
|
|
||||||
/* ─── Scanlator filter ────────────────────────────────────── */
|
|
||||||
.scan-filter-wrap { position: relative; }
|
.scan-filter-wrap { position: relative; }
|
||||||
.scan-filter-panel {
|
.scan-filter-panel {
|
||||||
position: absolute; top: calc(100% + 6px); right: 0; z-index: 200; min-width: 200px;
|
position: absolute; top: calc(100% + 6px); right: 0; z-index: 200; min-width: 200px;
|
||||||
@@ -637,6 +628,5 @@
|
|||||||
.scan-filter-item-block { color: var(--color-error) !important; background: color-mix(in srgb, var(--color-error) 8%, transparent) !important; }
|
.scan-filter-item-block { color: var(--color-error) !important; background: color-mix(in srgb, var(--color-error) 8%, transparent) !important; }
|
||||||
.scan-filter-item-block:hover { background: color-mix(in srgb, var(--color-error) 14%, transparent) !important; }
|
.scan-filter-item-block:hover { background: color-mix(in srgb, var(--color-error) 14%, transparent) !important; }
|
||||||
|
|
||||||
/* ─── Shared animation (used by dropdowns/popovers) ───────── */
|
|
||||||
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
@keyframes scaleIn { from { opacity: 0; transform: scale(0.97) } to { opacity: 1; transform: scale(1) } }
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user