mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Feat: Touch Gestures (Pinch Zoom) for Reader (#29)
This commit is contained in:
@@ -3,6 +3,8 @@
|
|||||||
import { store } from "@store/state.svelte";
|
import { store } from "@store/state.svelte";
|
||||||
import { readerState } from "../store/readerState.svelte";
|
import { readerState } from "../store/readerState.svelte";
|
||||||
import type { StripChapter } from "../lib/scrollHandler";
|
import type { StripChapter } from "../lib/scrollHandler";
|
||||||
|
import { createPinchTracker } from "../lib/pinchZoom";
|
||||||
|
import type { PinchTracker } from "../lib/pinchZoom";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
style: string;
|
style: string;
|
||||||
@@ -16,6 +18,9 @@
|
|||||||
stripToRender: StripChapter[];
|
stripToRender: StripChapter[];
|
||||||
fadingOut: boolean;
|
fadingOut: boolean;
|
||||||
tapToToggleBar: boolean;
|
tapToToggleBar: boolean;
|
||||||
|
pinchZoomEnabled: boolean;
|
||||||
|
onGetZoom: () => number;
|
||||||
|
onSetZoom: (z: number) => void;
|
||||||
resolveUrl: (url: string, priority?: number) => Promise<string>;
|
resolveUrl: (url: string, priority?: number) => Promise<string>;
|
||||||
onTap: (e: MouseEvent) => void;
|
onTap: (e: MouseEvent) => void;
|
||||||
onWheel: (e: WheelEvent) => void;
|
onWheel: (e: WheelEvent) => void;
|
||||||
@@ -26,7 +31,8 @@
|
|||||||
const {
|
const {
|
||||||
style, imgCls, effectiveWidth, loading, error, pageReady,
|
style, imgCls, effectiveWidth, loading, error, pageReady,
|
||||||
pageGroups, currentGroup, stripToRender, fadingOut,
|
pageGroups, currentGroup, stripToRender, fadingOut,
|
||||||
tapToToggleBar, resolveUrl, onTap, onWheel, onToggleUi, bindContainer,
|
tapToToggleBar, pinchZoomEnabled, onGetZoom, onSetZoom,
|
||||||
|
resolveUrl, onTap, onWheel, onToggleUi, bindContainer,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const INSPECT_ZOOM_STEP = 0.15;
|
const INSPECT_ZOOM_STEP = 0.15;
|
||||||
@@ -57,12 +63,28 @@
|
|||||||
let inspectPanStartX = 0;
|
let inspectPanStartX = 0;
|
||||||
let inspectPanStartY = 0;
|
let inspectPanStartY = 0;
|
||||||
|
|
||||||
// Drag-to-scroll state for longstrip mode
|
|
||||||
let stripDragging = false;
|
let stripDragging = false;
|
||||||
let stripDragMoved = false;
|
let stripDragMoved = false;
|
||||||
let stripDragStartY = 0;
|
let stripDragStartY = 0;
|
||||||
let stripScrollStart = 0;
|
let stripScrollStart = 0;
|
||||||
|
|
||||||
|
let pinch: PinchTracker | null = null;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (pinchZoomEnabled) {
|
||||||
|
pinch = createPinchTracker({
|
||||||
|
getZoom: onGetZoom,
|
||||||
|
setZoom: onSetZoom,
|
||||||
|
getInspectScale: () => readerState.inspectScale,
|
||||||
|
setInspectScale: (s) => { readerState.inspectScale = s; },
|
||||||
|
resetInspectPan: () => { readerState.inspectPanX = 0; readerState.inspectPanY = 0; },
|
||||||
|
isLongstrip: () => style === "longstrip",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pinch = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export function onInspectMouseDown(e: MouseEvent) {
|
export function onInspectMouseDown(e: MouseEvent) {
|
||||||
if (style === "longstrip") {
|
if (style === "longstrip") {
|
||||||
stripDragging = true;
|
stripDragging = true;
|
||||||
@@ -103,13 +125,43 @@
|
|||||||
inspectDragging = false;
|
inspectDragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function onPointerDown(e: PointerEvent) {
|
||||||
|
pinch?.onPointerDown(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPointerMove(e: PointerEvent) {
|
||||||
|
if (pinch?.isPinching()) {
|
||||||
|
pinch.onPointerMove(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stripDragging) {
|
||||||
|
const dy = e.clientY - stripDragStartY;
|
||||||
|
if (!stripDragMoved && Math.abs(dy) > 4) stripDragMoved = true;
|
||||||
|
if (containerEl) containerEl.scrollTop = stripScrollStart - dy;
|
||||||
|
}
|
||||||
|
if (inspectDragging) {
|
||||||
|
if (!inspectDragMoved && Math.abs(e.clientX - inspectDragStartX) + Math.abs(e.clientY - inspectDragStartY) > 4) inspectDragMoved = true;
|
||||||
|
const rawX = inspectPanStartX + (e.clientX - inspectDragStartX);
|
||||||
|
const rawY = inspectPanStartY + (e.clientY - inspectDragStartY);
|
||||||
|
const [cx, cy] = clampInspectPan(readerState.inspectScale, rawX, rawY);
|
||||||
|
readerState.inspectPanX = cx;
|
||||||
|
readerState.inspectPanY = cy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onPointerUp(e: PointerEvent) {
|
||||||
|
pinch?.onPointerUp(e);
|
||||||
|
if (!pinch?.isPinching()) {
|
||||||
|
stripDragging = false;
|
||||||
|
inspectDragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function handleWheel(e: WheelEvent) {
|
export function handleWheel(e: WheelEvent) {
|
||||||
if (style === "longstrip") {
|
if (style === "longstrip") {
|
||||||
// In longstrip, Ctrl+scroll drives reader-level zoom; plain scroll scrolls naturally.
|
|
||||||
if (e.ctrlKey) { onWheel(e); }
|
if (e.ctrlKey) { onWheel(e); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// In paged modes, Ctrl+scroll drives inspect-zoom (magnify); plain scroll pages forward/back.
|
|
||||||
if (!e.ctrlKey) { onWheel(e); return; }
|
if (!e.ctrlKey) { onWheel(e); return; }
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const delta = e.deltaY < 0 ? INSPECT_ZOOM_STEP : -INSPECT_ZOOM_STEP;
|
const delta = e.deltaY < 0 ? INSPECT_ZOOM_STEP : -INSPECT_ZOOM_STEP;
|
||||||
@@ -154,6 +206,7 @@
|
|||||||
onclick={handleTap}
|
onclick={handleTap}
|
||||||
ondblclick={(e) => { if (tapToToggleBar) { const x = e.clientX / window.innerWidth; if (x >= 0.3 && x <= 0.7) onToggleUi(); } }}
|
ondblclick={(e) => { if (tapToToggleBar) { const x = e.clientX / window.innerWidth; if (x >= 0.3 && x <= 0.7) onToggleUi(); } }}
|
||||||
onmousedown={onInspectMouseDown}
|
onmousedown={onInspectMouseDown}
|
||||||
|
onpointerdown={pinchZoomEnabled ? onPointerDown : undefined}
|
||||||
onwheel={(e) => { if (e.ctrlKey || style !== "longstrip") e.preventDefault(); }}
|
onwheel={(e) => { if (e.ctrlKey || style !== "longstrip") e.preventDefault(); }}
|
||||||
style:cursor={style === "longstrip" ? (stripDragging ? "grabbing" : "grab") : undefined}
|
style:cursor={style === "longstrip" ? (stripDragging ? "grabbing" : "grab") : undefined}
|
||||||
onkeydown={(e) => { if (e.key === " " && style === "longstrip") { e.preventDefault(); containerEl?.scrollBy({ top: containerEl.clientHeight * 0.85, behavior: "smooth" }); } }}
|
onkeydown={(e) => { if (e.key === " " && style === "longstrip") { e.preventDefault(); containerEl?.scrollBy({ top: containerEl.clientHeight * 0.85, behavior: "smooth" }); } }}
|
||||||
@@ -217,12 +270,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.viewer { flex: 1; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; -webkit-overflow-scrolling: touch; position: relative; }
|
.viewer { flex: 1; overflow-y: auto; overflow-x: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; -webkit-overflow-scrolling: touch; position: relative; touch-action: pan-x pan-y; }
|
||||||
.viewer.strip { justify-content: flex-start; padding: var(--sp-4) 0; }
|
.viewer.strip { justify-content: flex-start; padding: var(--sp-4) 0; }
|
||||||
.viewer:focus { outline: none; }
|
.viewer:focus { outline: none; }
|
||||||
.viewer.inspect-active { cursor: grab; overflow: hidden; }
|
.viewer.inspect-active { cursor: grab; overflow: hidden; }
|
||||||
.viewer.inspect-active:active { cursor: grabbing; }
|
.viewer.inspect-active:active { cursor: grabbing; }
|
||||||
|
|
||||||
|
:global(.pinch-active) .viewer { touch-action: none; }
|
||||||
|
|
||||||
.inspect-wrap { display: flex; align-items: center; justify-content: center; transform-origin: center center; will-change: transform; }
|
.inspect-wrap { display: flex; align-items: center; justify-content: center; transform-origin: center center; will-change: transform; }
|
||||||
|
|
||||||
.img { display: block; user-select: none; image-rendering: auto; }
|
.img { display: block; user-select: none; image-rendering: auto; }
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
const lastPage = $derived(store.pageUrls.length);
|
const lastPage = $derived(store.pageUrls.length);
|
||||||
const effectiveWidth = $derived(readerState.containerWidth > 0 ? Math.round(readerState.containerWidth * zoom) : undefined);
|
const effectiveWidth = $derived(readerState.containerWidth > 0 ? Math.round(readerState.containerWidth * zoom) : undefined);
|
||||||
const zoomPct = $derived(Math.round(zoom * 100));
|
const zoomPct = $derived(Math.round(zoom * 100));
|
||||||
|
const pinchZoomEnabled = $derived(store.settings.pinchZoom ?? false);
|
||||||
|
|
||||||
const displayChapter = $derived(
|
const displayChapter = $derived(
|
||||||
style === "longstrip" && readerState.visibleChapterId
|
style === "longstrip" && readerState.visibleChapterId
|
||||||
@@ -195,8 +196,6 @@
|
|||||||
if (x > 0.6) goNext(); else if (x < 0.4) goPrev();
|
if (x > 0.6) goNext(); else if (x < 0.4) goPrev();
|
||||||
}
|
}
|
||||||
|
|
||||||
// onWheel is only invoked from PageView for longstrip Ctrl+scroll (reader-level zoom).
|
|
||||||
// In paged modes, Ctrl+scroll is handled inside PageView as inspect-zoom instead.
|
|
||||||
function handleWheel(e: WheelEvent) {
|
function handleWheel(e: WheelEvent) {
|
||||||
if (!e.ctrlKey) return;
|
if (!e.ctrlKey) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -481,6 +480,8 @@
|
|||||||
window.addEventListener("keydown", onKey);
|
window.addEventListener("keydown", onKey);
|
||||||
window.addEventListener("mousemove", pageViewRef.onInspectMouseMove);
|
window.addEventListener("mousemove", pageViewRef.onInspectMouseMove);
|
||||||
window.addEventListener("mouseup", pageViewRef.onInspectMouseUp);
|
window.addEventListener("mouseup", pageViewRef.onInspectMouseUp);
|
||||||
|
window.addEventListener("pointermove", pageViewRef.onPointerMove);
|
||||||
|
window.addEventListener("pointerup", pageViewRef.onPointerUp);
|
||||||
|
|
||||||
readerState.isFullscreen = await win.isFullscreen();
|
readerState.isFullscreen = await win.isFullscreen();
|
||||||
const unlistenFs = await win.onResized(async () => {
|
const unlistenFs = await win.onResized(async () => {
|
||||||
@@ -502,6 +503,8 @@
|
|||||||
window.removeEventListener("keydown", onKey);
|
window.removeEventListener("keydown", onKey);
|
||||||
window.removeEventListener("mousemove", pageViewRef.onInspectMouseMove);
|
window.removeEventListener("mousemove", pageViewRef.onInspectMouseMove);
|
||||||
window.removeEventListener("mouseup", pageViewRef.onInspectMouseUp);
|
window.removeEventListener("mouseup", pageViewRef.onInspectMouseUp);
|
||||||
|
window.removeEventListener("pointermove", pageViewRef.onPointerMove);
|
||||||
|
window.removeEventListener("pointerup", pageViewRef.onPointerUp);
|
||||||
cleanupScroll();
|
cleanupScroll();
|
||||||
unlistenFs();
|
unlistenFs();
|
||||||
ro.disconnect();
|
ro.disconnect();
|
||||||
@@ -514,6 +517,7 @@
|
|||||||
class:overlay-bars={overlayBars}
|
class:overlay-bars={overlayBars}
|
||||||
class:bar-left={barPosition === "left"}
|
class:bar-left={barPosition === "left"}
|
||||||
class:bar-right={barPosition === "right"}
|
class:bar-right={barPosition === "right"}
|
||||||
|
class:pinch-active={pinchZoomEnabled}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
onmousemove={(e) => {
|
onmousemove={(e) => {
|
||||||
if (!tapToToggleBar) {
|
if (!tapToToggleBar) {
|
||||||
@@ -582,6 +586,9 @@
|
|||||||
{currentGroup} {stripToRender}
|
{currentGroup} {stripToRender}
|
||||||
fadingOut={readerState.fadingOut}
|
fadingOut={readerState.fadingOut}
|
||||||
{tapToToggleBar}
|
{tapToToggleBar}
|
||||||
|
{pinchZoomEnabled}
|
||||||
|
onGetZoom={() => zoom}
|
||||||
|
onSetZoom={(z) => { captureZoomAnchor(containerEl, style, zoomAnchor); applySettings({ readerZoom: z }); restoreZoomAnchor(containerEl, zoomAnchor); }}
|
||||||
resolveUrl={(url, priority) => resolveUrl(url, useBlob, priority)}
|
resolveUrl={(url, priority) => resolveUrl(url, useBlob, priority)}
|
||||||
onTap={handleTap}
|
onTap={handleTap}
|
||||||
onWheel={handleWheel}
|
onWheel={handleWheel}
|
||||||
@@ -627,4 +634,6 @@
|
|||||||
|
|
||||||
.root.bar-left :global(.viewer) { margin-left: 40px; }
|
.root.bar-left :global(.viewer) { margin-left: 40px; }
|
||||||
.root.bar-right :global(.viewer) { margin-right: 40px; }
|
.root.bar-right :global(.viewer) { margin-right: 40px; }
|
||||||
|
|
||||||
|
.root.pinch-active :global(.viewer) { touch-action: none; }
|
||||||
</style>
|
</style>
|
||||||
@@ -266,6 +266,16 @@
|
|||||||
aria-checked={effectiveSettings.optimizeContrast}
|
aria-checked={effectiveSettings.optimizeContrast}
|
||||||
><span class="toggle-knob"></span></button>
|
><span class="toggle-knob"></span></button>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="toggle-row">
|
||||||
|
<span class="toggle-label">Pinch to zoom <span class="toggle-badge">experimental</span></span>
|
||||||
|
<button
|
||||||
|
class="toggle"
|
||||||
|
class:on={store.settings.pinchZoom ?? false}
|
||||||
|
onclick={() => updateSettings({ pinchZoom: !(store.settings.pinchZoom ?? false) })}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={store.settings.pinchZoom ?? false}
|
||||||
|
><span class="toggle-knob"></span></button>
|
||||||
|
</label>
|
||||||
<label class="toggle-row">
|
<label class="toggle-row">
|
||||||
<span class="toggle-label">Mark read on chapter advance</span>
|
<span class="toggle-label">Mark read on chapter advance</span>
|
||||||
<button
|
<button
|
||||||
@@ -534,6 +544,19 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggle-badge {
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 9px;
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-faint);
|
||||||
|
border: 1px solid var(--border-dim);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
margin-left: var(--sp-1);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { clampZoom } from "./zoomHelpers";
|
||||||
|
import { ZOOM_MIN, ZOOM_MAX } from "../store/readerState.svelte";
|
||||||
|
|
||||||
|
export interface PinchTrackerOptions {
|
||||||
|
getZoom: () => number;
|
||||||
|
setZoom: (z: number) => void;
|
||||||
|
getInspectScale: () => number;
|
||||||
|
setInspectScale: (s: number) => void;
|
||||||
|
resetInspectPan: () => void;
|
||||||
|
isLongstrip: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PinchTracker {
|
||||||
|
onPointerDown: (e: PointerEvent) => void;
|
||||||
|
onPointerMove: (e: PointerEvent) => void;
|
||||||
|
onPointerUp: (e: PointerEvent) => void;
|
||||||
|
isPinching: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INSPECT_ZOOM_MAX = 8;
|
||||||
|
|
||||||
|
export function createPinchTracker(opts: PinchTrackerOptions): PinchTracker {
|
||||||
|
const pointers = new Map<number, { x: number; y: number }>();
|
||||||
|
let startDist = 0;
|
||||||
|
let startZoom = 0;
|
||||||
|
let startInspect = 0;
|
||||||
|
let pinching = false;
|
||||||
|
|
||||||
|
function dist(a: { x: number; y: number }, b: { x: number; y: number }): number {
|
||||||
|
return Math.hypot(b.x - a.x, b.y - a.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(e: PointerEvent) {
|
||||||
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
if (pointers.size === 2) {
|
||||||
|
const [a, b] = [...pointers.values()];
|
||||||
|
startDist = dist(a, b);
|
||||||
|
startZoom = opts.getZoom();
|
||||||
|
startInspect = opts.getInspectScale();
|
||||||
|
pinching = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(e: PointerEvent) {
|
||||||
|
if (!pinching || !pointers.has(e.pointerId)) return;
|
||||||
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||||
|
if (pointers.size < 2) return;
|
||||||
|
|
||||||
|
const [a, b] = [...pointers.values()];
|
||||||
|
const current = dist(a, b);
|
||||||
|
if (startDist === 0) return;
|
||||||
|
const ratio = current / startDist;
|
||||||
|
|
||||||
|
if (opts.isLongstrip()) {
|
||||||
|
opts.setZoom(clampZoom(startZoom * ratio));
|
||||||
|
} else {
|
||||||
|
const next = Math.max(1, Math.min(INSPECT_ZOOM_MAX, startInspect * ratio));
|
||||||
|
if (next !== opts.getInspectScale()) {
|
||||||
|
if (next === 1) opts.resetInspectPan();
|
||||||
|
opts.setInspectScale(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(e: PointerEvent) {
|
||||||
|
pointers.delete(e.pointerId);
|
||||||
|
if (pointers.size < 2) {
|
||||||
|
pinching = false;
|
||||||
|
startDist = 0;
|
||||||
|
startZoom = 0;
|
||||||
|
startInspect = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPinching() { return pinching; }
|
||||||
|
|
||||||
|
return { onPointerDown, onPointerMove, onPointerUp, isPinching };
|
||||||
|
}
|
||||||
+50
-220
@@ -1,199 +1,29 @@
|
|||||||
import type { Manga, Chapter, Category, Source } from "../types";
|
import type { Manga, Chapter, Category, Source } from "../types";
|
||||||
import { DEFAULT_KEYBINDS, type Keybinds } from "../core/keybinds/defaultBinds";
|
import type { Settings, ReaderSettings, ReaderPreset, CustomTheme,
|
||||||
import { notifications } from "./notifications.svelte";
|
LibraryFilter } from "../types/settings";
|
||||||
import { app } from "./app.svelte";
|
import type { HistoryEntry, BookmarkEntry, MarkerEntry, MarkerColor,
|
||||||
|
ReadLogEntry, ReadingStats, LibraryUpdateEntry } from "../types/history";
|
||||||
|
import { DEFAULT_KEYBINDS } from "../core/keybinds/defaultBinds";
|
||||||
|
import { DEFAULT_SETTINGS } from "../types/settings";
|
||||||
|
import { DEFAULT_READING_STATS } from "../types/history";
|
||||||
|
import { notifications } from "./notifications.svelte";
|
||||||
|
import { app } from "./app.svelte";
|
||||||
|
|
||||||
export type { NavPage } from "./app.svelte";
|
export type { NavPage } from "./app.svelte";
|
||||||
export type { Toast, ActiveDownload } from "./notifications.svelte";
|
export type { Toast, ActiveDownload } from "./notifications.svelte";
|
||||||
|
export type { Settings, ReaderSettings, ReaderPreset, CustomTheme,
|
||||||
|
LibraryFilter, LibrarySortMode, LibrarySortDir,
|
||||||
|
LibraryStatusFilter, LibraryContentFilter,
|
||||||
|
PageStyle, FitMode, ReadingDirection,
|
||||||
|
ChapterSortDir, ChapterSortMode,
|
||||||
|
BuiltinTheme, Theme, ThemeTokens,
|
||||||
|
MangaPrefs } from "../types/settings";
|
||||||
|
export { DEFAULT_SETTINGS, DEFAULT_MANGA_PREFS,
|
||||||
|
DEFAULT_THEME_TOKENS } from "../types/settings";
|
||||||
|
export type { HistoryEntry, BookmarkEntry, MarkerEntry, MarkerColor,
|
||||||
|
ReadLogEntry, ReadingStats, LibraryUpdateEntry } from "../types/history";
|
||||||
|
|
||||||
export type PageStyle = "single" | "double" | "longstrip";
|
const STORE_VERSION = 3;
|
||||||
export type FitMode = "width" | "height" | "screen" | "original";
|
|
||||||
export type LibraryFilter = "all" | "library" | "downloaded" | string;
|
|
||||||
export type ReadingDirection = "ltr" | "rtl";
|
|
||||||
export type ChapterSortDir = "desc" | "asc";
|
|
||||||
export type ChapterSortMode = "source" | "chapterNumber" | "uploadDate";
|
|
||||||
|
|
||||||
export type LibrarySortMode =
|
|
||||||
| "az" | "unreadCount" | "totalChapters"
|
|
||||||
| "recentlyAdded" | "recentlyRead" | "latestFetched" | "latestUploaded";
|
|
||||||
|
|
||||||
export type LibrarySortDir = "asc" | "desc";
|
|
||||||
|
|
||||||
export type LibraryStatusFilter = "ALL" | "ONGOING" | "COMPLETED" | "CANCELLED" | "HIATUS" | "UNKNOWN";
|
|
||||||
export type LibraryContentFilter = "unread" | "started" | "downloaded" | "bookmarked" | "marked";
|
|
||||||
|
|
||||||
export type BuiltinTheme = "original" | "dark" | "light" | "light-contrast" | "midnight" | "warm" | "starry";
|
|
||||||
export type Theme = BuiltinTheme | string;
|
|
||||||
|
|
||||||
export interface ThemeTokens {
|
|
||||||
"bg-void": string; "bg-base": string; "bg-surface": string;
|
|
||||||
"bg-raised": string; "bg-overlay": string; "bg-subtle": string;
|
|
||||||
"border-dim": string; "border-base": string; "border-strong": string; "border-focus": string;
|
|
||||||
"text-primary": string; "text-secondary": string; "text-muted": string;
|
|
||||||
"text-faint": string; "text-disabled": string;
|
|
||||||
"accent": string; "accent-dim": string; "accent-muted": string;
|
|
||||||
"accent-fg": string; "accent-bright": string;
|
|
||||||
"color-error": string; "color-error-bg": string;
|
|
||||||
"color-success": string; "color-info": string; "color-info-bg": string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomTheme { id: string; name: string; tokens: ThemeTokens; }
|
|
||||||
|
|
||||||
export const DEFAULT_THEME_TOKENS: ThemeTokens = {
|
|
||||||
"bg-void": "#080808", "bg-base": "#0c0c0c", "bg-surface": "#101010",
|
|
||||||
"bg-raised": "#151515", "bg-overlay": "#1a1a1a", "bg-subtle": "#202020",
|
|
||||||
"border-dim": "#1c1c1c", "border-base": "#242424", "border-strong": "#2e2e2e", "border-focus": "#4a5c4a",
|
|
||||||
"text-primary": "#f0efec", "text-secondary": "#c8c6c0", "text-muted": "#8a8880",
|
|
||||||
"text-faint": "#4e4d4a", "text-disabled": "#2a2a28",
|
|
||||||
"accent": "#6b8f6b", "accent-dim": "#2a3d2a", "accent-muted": "#1a251a",
|
|
||||||
"accent-fg": "#a8c4a8", "accent-bright": "#8fb88f",
|
|
||||||
"color-error": "#c47a7a", "color-error-bg": "#1f1212",
|
|
||||||
"color-success": "#7aab7a", "color-info": "#7a9ec4", "color-info-bg": "#121a1f",
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface HistoryEntry {
|
|
||||||
mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
|
||||||
chapterId: number; chapterName: string; readAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookmarkEntry {
|
|
||||||
mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
|
||||||
chapterId: number; chapterName: string; pageNumber: number;
|
|
||||||
savedAt: number; label?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MarkerColor = "yellow" | "red" | "blue" | "green" | "purple";
|
|
||||||
|
|
||||||
export interface MarkerEntry {
|
|
||||||
id: string; mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
|
||||||
chapterId: number; chapterName: string; pageNumber: number;
|
|
||||||
note: string; color: MarkerColor; createdAt: number; updatedAt?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReadLogEntry { mangaId: number; chapterId: number; readAt: number; minutes: number; }
|
|
||||||
export interface ReadingStats {
|
|
||||||
totalChaptersRead: number; totalMangaRead: number; totalMinutesRead: number;
|
|
||||||
firstReadAt: number; lastReadAt: number;
|
|
||||||
currentStreakDays: number; longestStreakDays: number; lastStreakDate: string;
|
|
||||||
}
|
|
||||||
export interface LibraryUpdateEntry {
|
|
||||||
mangaId: number; mangaTitle: string; thumbnailUrl: string; newChapters: number; checkedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MangaPrefs {
|
|
||||||
autoDownload: boolean; downloadAhead: number; deleteOnRead: boolean;
|
|
||||||
deleteDelayHours: number; maxKeepChapters: number; pauseUpdates: boolean;
|
|
||||||
refreshInterval: "global" | "daily" | "weekly" | "manual";
|
|
||||||
preferredScanlator: string; scanlatorFilter: string[];
|
|
||||||
autoDownloadScanlators: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_MANGA_PREFS: MangaPrefs = {
|
|
||||||
autoDownload: false, downloadAhead: 0, deleteOnRead: false,
|
|
||||||
deleteDelayHours: 0, maxKeepChapters: 0, pauseUpdates: false,
|
|
||||||
refreshInterval: "global", preferredScanlator: "", scanlatorFilter: [],
|
|
||||||
autoDownloadScanlators: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ReaderSettings {
|
|
||||||
pageStyle: PageStyle;
|
|
||||||
fitMode: FitMode;
|
|
||||||
readingDirection: ReadingDirection;
|
|
||||||
readerZoom: number;
|
|
||||||
pageGap: boolean;
|
|
||||||
optimizeContrast: boolean;
|
|
||||||
offsetDoubleSpreads: boolean;
|
|
||||||
barPosition?: "top" | "left" | "right";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ReaderPreset {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
settings: ReaderSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Settings {
|
|
||||||
pageStyle: PageStyle; readingDirection: ReadingDirection; fitMode: FitMode;
|
|
||||||
readerZoom: number; pageGap: boolean; optimizeContrast: boolean;
|
|
||||||
offsetDoubleSpreads: boolean; preloadPages: number;
|
|
||||||
autoMarkRead: boolean; autoNextChapter: boolean;
|
|
||||||
libraryCropCovers: boolean; libraryPageSize: number;
|
|
||||||
showNsfw: boolean; discordRpc: boolean;
|
|
||||||
chapterSortDir: ChapterSortDir; chapterSortMode: ChapterSortMode; chapterPageSize: number;
|
|
||||||
uiZoom: number; compactSidebar: boolean; gpuAcceleration: boolean;
|
|
||||||
serverUrl: string; serverBinary: string; autoStartServer: boolean;
|
|
||||||
preferredExtensionLang: string; keybinds: Keybinds;
|
|
||||||
idleTimeoutMin?: number; splashCards?: boolean;
|
|
||||||
storageLimitGb: number | null; markReadOnNext: boolean; readerDebounceMs: number;
|
|
||||||
autoBookmark: boolean; theme: Theme; libraryBranches: boolean; renderLimit: number;
|
|
||||||
heroSlots: (number | null)[]; mangaLinks: Record<number, number[]>;
|
|
||||||
mangaPrefs: Record<number, Partial<MangaPrefs>>;
|
|
||||||
serverAuthUser: string; serverAuthPass: string;
|
|
||||||
serverAuthMode: "NONE" | "BASIC_AUTH" | "SIMPLE_LOGIN" | "UI_LOGIN";
|
|
||||||
socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string;
|
|
||||||
socksProxyVersion: number; socksProxyUsername: string; socksProxyPassword: string;
|
|
||||||
flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number;
|
|
||||||
flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrFallback: boolean;
|
|
||||||
appLockEnabled: boolean; appLockPin: string;
|
|
||||||
customThemes: CustomTheme[]; hiddenCategoryIds: number[];
|
|
||||||
defaultLibraryCategoryId: number | null; savedIsDefaultCategory: boolean;
|
|
||||||
nsfwFilteredTags: string[]; nsfwAllowedSourceIds: string[]; nsfwBlockedSourceIds: string[];
|
|
||||||
libraryTabSort: Record<string, { mode: LibrarySortMode; dir: LibrarySortDir }>;
|
|
||||||
libraryTabStatus: Record<string, LibraryStatusFilter>;
|
|
||||||
libraryTabFilters: Record<string, Partial<Record<LibraryContentFilter, boolean>>>;
|
|
||||||
maxPageWidth?: number; uiScale?: number;
|
|
||||||
extraScanDirs: string[]; serverDownloadsPath: string; serverLocalSourcePath: string;
|
|
||||||
qolAnimations: boolean;
|
|
||||||
pinnedSourceIds: string[];
|
|
||||||
readerPresets: ReaderPreset[];
|
|
||||||
mangaReaderSettings: Record<number, ReaderSettings>;
|
|
||||||
barPosition?: "top" | "left" | "right";
|
|
||||||
trackerSyncBack: boolean;
|
|
||||||
trackerSyncBackThreshold: number | null;
|
|
||||||
trackerRespectScanlatorFilter: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_READING_STATS: ReadingStats = {
|
|
||||||
totalChaptersRead: 0, totalMangaRead: 0, totalMinutesRead: 0,
|
|
||||||
firstReadAt: 0, lastReadAt: 0, currentStreakDays: 0, longestStreakDays: 0, lastStreakDate: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DEFAULT_SETTINGS: Settings = {
|
|
||||||
pageStyle: "longstrip", readingDirection: "ltr", fitMode: "width",
|
|
||||||
readerZoom: 1.0, pageGap: true, optimizeContrast: false, offsetDoubleSpreads: false,
|
|
||||||
preloadPages: 3, autoMarkRead: true, autoNextChapter: true,
|
|
||||||
libraryCropCovers: true, libraryPageSize: 48, showNsfw: false, discordRpc: false,
|
|
||||||
chapterSortDir: "desc", chapterSortMode: "source", chapterPageSize: 25,
|
|
||||||
uiZoom: 1.0, compactSidebar: false, gpuAcceleration: true,
|
|
||||||
serverUrl: "http://localhost:4567", serverBinary: "", autoStartServer: true,
|
|
||||||
preferredExtensionLang: "en", keybinds: DEFAULT_KEYBINDS,
|
|
||||||
idleTimeoutMin: 5, splashCards: true, storageLimitGb: null,
|
|
||||||
markReadOnNext: true, readerDebounceMs: 120, autoBookmark: true,
|
|
||||||
theme: "dark", libraryBranches: true, renderLimit: 48,
|
|
||||||
heroSlots: [null, null, null, null], mangaLinks: {}, mangaPrefs: {},
|
|
||||||
serverAuthUser: "", serverAuthPass: "", serverAuthMode: "NONE",
|
|
||||||
socksProxyEnabled: false, socksProxyHost: "", socksProxyPort: "1080",
|
|
||||||
socksProxyVersion: 5, socksProxyUsername: "", socksProxyPassword: "",
|
|
||||||
flareSolverrEnabled: false, flareSolverrUrl: "http://localhost:8191",
|
|
||||||
flareSolverrTimeout: 60, flareSolverrSessionName: "moku",
|
|
||||||
flareSolverrSessionTtl: 15, flareSolverrFallback: false,
|
|
||||||
appLockEnabled: false, appLockPin: "",
|
|
||||||
customThemes: [], hiddenCategoryIds: [], defaultLibraryCategoryId: null,
|
|
||||||
savedIsDefaultCategory: false,
|
|
||||||
nsfwFilteredTags: ["adult", "mature", "hentai", "ecchi", "erotic", "pornograph", "18+", "smut", "lemon", "explicit", "sexual violence"],
|
|
||||||
nsfwAllowedSourceIds: [], nsfwBlockedSourceIds: [],
|
|
||||||
libraryTabSort: {}, libraryTabStatus: {}, libraryTabFilters: {},
|
|
||||||
extraScanDirs: [], serverDownloadsPath: "", serverLocalSourcePath: "",
|
|
||||||
qolAnimations: true,
|
|
||||||
pinnedSourceIds: [],
|
|
||||||
readerPresets: [],
|
|
||||||
mangaReaderSettings: {},
|
|
||||||
trackerSyncBack: false,
|
|
||||||
trackerSyncBackThreshold: 20,
|
|
||||||
trackerRespectScanlatorFilter: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const STORE_VERSION = 3;
|
|
||||||
const AVG_MIN_PER_CHAPTER = 5;
|
const AVG_MIN_PER_CHAPTER = 5;
|
||||||
const RESET_ON_UPGRADE: (keyof Settings)[] = ["serverBinary", "readerZoom", "uiZoom"];
|
const RESET_ON_UPGRADE: (keyof Settings)[] = ["serverBinary", "readerZoom", "uiZoom"];
|
||||||
|
|
||||||
@@ -235,38 +65,38 @@ function mergeSettings(saved: any): Settings {
|
|||||||
libraryTabSort: saved?.settings?.libraryTabSort ?? {},
|
libraryTabSort: saved?.settings?.libraryTabSort ?? {},
|
||||||
libraryTabStatus: saved?.settings?.libraryTabStatus ?? {},
|
libraryTabStatus: saved?.settings?.libraryTabStatus ?? {},
|
||||||
libraryTabFilters: saved?.settings?.libraryTabFilters ?? {},
|
libraryTabFilters: saved?.settings?.libraryTabFilters ?? {},
|
||||||
extraScanDirs: saved?.settings?.extraScanDirs ?? [],
|
extraScanDirs: saved?.settings?.extraScanDirs ?? [],
|
||||||
pinnedSourceIds: saved?.settings?.pinnedSourceIds ?? [],
|
pinnedSourceIds: saved?.settings?.pinnedSourceIds ?? [],
|
||||||
readerPresets: saved?.settings?.readerPresets ?? [],
|
readerPresets: saved?.settings?.readerPresets ?? [],
|
||||||
mangaReaderSettings: saved?.settings?.mangaReaderSettings ?? {},
|
mangaReaderSettings: saved?.settings?.mangaReaderSettings ?? {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class Store {
|
class Store {
|
||||||
settings: Settings = $state(mergeSettings(saved));
|
settings: Settings = $state(mergeSettings(saved));
|
||||||
activeManga: Manga | null = $state(null);
|
activeManga: Manga | null = $state(null);
|
||||||
previewManga: Manga | null = $state(null);
|
previewManga: Manga | null = $state(null);
|
||||||
activeChapter: Chapter | null = $state(null);
|
activeChapter: Chapter | null = $state(null);
|
||||||
activeChapterList: Chapter[] = $state([]);
|
activeChapterList: Chapter[] = $state([]);
|
||||||
pageUrls: string[] = $state([]);
|
pageUrls: string[] = $state([]);
|
||||||
pageNumber: number = $state(1);
|
pageNumber: number = $state(1);
|
||||||
libraryFilter: LibraryFilter = $state("all");
|
libraryFilter: LibraryFilter = $state("all");
|
||||||
categories: Category[] = $state([]);
|
categories: Category[] = $state([]);
|
||||||
activeSource: Source | null = $state(null);
|
activeSource: Source | null = $state(null);
|
||||||
libraryTagFilter: string[] = $state([]);
|
libraryTagFilter: string[] = $state([]);
|
||||||
history: HistoryEntry[] = $state(saved?.history ?? []);
|
history: HistoryEntry[] = $state(saved?.history ?? []);
|
||||||
bookmarks: BookmarkEntry[]= $state(saved?.bookmarks ?? []);
|
bookmarks: BookmarkEntry[]= $state(saved?.bookmarks ?? []);
|
||||||
markers: MarkerEntry[] = $state(saved?.markers ?? []);
|
markers: MarkerEntry[] = $state(saved?.markers ?? []);
|
||||||
readLog: ReadLogEntry[] = $state(saved?.readLog ?? []);
|
readLog: ReadLogEntry[] = $state(saved?.readLog ?? []);
|
||||||
readingStats: ReadingStats = $state(saved?.readingStats ?? { ...DEFAULT_READING_STATS });
|
readingStats: ReadingStats = $state(saved?.readingStats ?? { ...DEFAULT_READING_STATS });
|
||||||
dailyReadCounts: Record<string, number> = $state(saved?.dailyReadCounts ?? {});
|
dailyReadCounts: Record<string, number> = $state(saved?.dailyReadCounts ?? {});
|
||||||
searchCache: Map<string, any> = $state(new Map());
|
searchCache: Map<string, any> = $state(new Map());
|
||||||
searchLibraryIds: Set<number> = $state(new Set());
|
searchLibraryIds: Set<number> = $state(new Set());
|
||||||
searchSrcOffset: number = $state(0);
|
searchSrcOffset: number = $state(0);
|
||||||
readerSessionId: number = $state(0);
|
readerSessionId: number = $state(0);
|
||||||
libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []);
|
libraryUpdates: LibraryUpdateEntry[] = $state(saved?.libraryUpdates ?? []);
|
||||||
lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0);
|
lastLibraryRefresh: number = $state(saved?.lastLibraryRefresh ?? 0);
|
||||||
acknowledgedUpdates: Set<number> = $state(new Set(saved?.acknowledgedUpdateIds ?? []));
|
acknowledgedUpdates: Set<number> = $state(new Set(saved?.acknowledgedUpdateIds ?? []));
|
||||||
|
|
||||||
get toasts() { return notifications.toasts; }
|
get toasts() { return notifications.toasts; }
|
||||||
get activeDownloads() { return notifications.activeDownloads; }
|
get activeDownloads() { return notifications.activeDownloads; }
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export interface HistoryEntry {
|
||||||
|
mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
||||||
|
chapterId: number; chapterName: string; readAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookmarkEntry {
|
||||||
|
mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
||||||
|
chapterId: number; chapterName: string; pageNumber: number;
|
||||||
|
savedAt: number; label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MarkerColor = "yellow" | "red" | "blue" | "green" | "purple";
|
||||||
|
|
||||||
|
export interface MarkerEntry {
|
||||||
|
id: string; mangaId: number; mangaTitle: string; thumbnailUrl: string;
|
||||||
|
chapterId: number; chapterName: string; pageNumber: number;
|
||||||
|
note: string; color: MarkerColor; createdAt: number; updatedAt?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReadLogEntry { mangaId: number; chapterId: number; readAt: number; minutes: number; }
|
||||||
|
|
||||||
|
export interface ReadingStats {
|
||||||
|
totalChaptersRead: number; totalMangaRead: number; totalMinutesRead: number;
|
||||||
|
firstReadAt: number; lastReadAt: number;
|
||||||
|
currentStreakDays: number; longestStreakDays: number; lastStreakDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_READING_STATS: ReadingStats = {
|
||||||
|
totalChaptersRead: 0, totalMangaRead: 0, totalMinutesRead: 0,
|
||||||
|
firstReadAt: 0, lastReadAt: 0, currentStreakDays: 0, longestStreakDays: 0, lastStreakDate: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LibraryUpdateEntry {
|
||||||
|
mangaId: number; mangaTitle: string; thumbnailUrl: string; newChapters: number; checkedAt: number;
|
||||||
|
}
|
||||||
@@ -3,3 +3,5 @@ export * from "./chapter";
|
|||||||
export * from "./extension";
|
export * from "./extension";
|
||||||
export * from "./tracking";
|
export * from "./tracking";
|
||||||
export * from "./api";
|
export * from "./api";
|
||||||
|
export * from "./settings";
|
||||||
|
export * from "./history";
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { DEFAULT_KEYBINDS, type Keybinds } from "../core/keybinds/defaultBinds";
|
||||||
|
|
||||||
|
export type PageStyle = "single" | "double" | "longstrip";
|
||||||
|
export type FitMode = "width" | "height" | "screen" | "original";
|
||||||
|
export type LibraryFilter = "all" | "library" | "downloaded" | string;
|
||||||
|
export type ReadingDirection = "ltr" | "rtl";
|
||||||
|
export type ChapterSortDir = "desc" | "asc";
|
||||||
|
export type ChapterSortMode = "source" | "chapterNumber" | "uploadDate";
|
||||||
|
|
||||||
|
export type LibrarySortMode =
|
||||||
|
| "az" | "unreadCount" | "totalChapters"
|
||||||
|
| "recentlyAdded" | "recentlyRead" | "latestFetched" | "latestUploaded";
|
||||||
|
|
||||||
|
export type LibrarySortDir = "asc" | "desc";
|
||||||
|
|
||||||
|
export type LibraryStatusFilter = "ALL" | "ONGOING" | "COMPLETED" | "CANCELLED" | "HIATUS" | "UNKNOWN";
|
||||||
|
export type LibraryContentFilter = "unread" | "started" | "downloaded" | "bookmarked" | "marked";
|
||||||
|
|
||||||
|
export type BuiltinTheme = "original" | "dark" | "light" | "light-contrast" | "midnight" | "warm" | "starry";
|
||||||
|
export type Theme = BuiltinTheme | string;
|
||||||
|
|
||||||
|
export interface ThemeTokens {
|
||||||
|
"bg-void": string; "bg-base": string; "bg-surface": string;
|
||||||
|
"bg-raised": string; "bg-overlay": string; "bg-subtle": string;
|
||||||
|
"border-dim": string; "border-base": string; "border-strong": string; "border-focus": string;
|
||||||
|
"text-primary": string; "text-secondary": string; "text-muted": string;
|
||||||
|
"text-faint": string; "text-disabled": string;
|
||||||
|
"accent": string; "accent-dim": string; "accent-muted": string;
|
||||||
|
"accent-fg": string; "accent-bright": string;
|
||||||
|
"color-error": string; "color-error-bg": string;
|
||||||
|
"color-success": string; "color-info": string; "color-info-bg": string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomTheme { id: string; name: string; tokens: ThemeTokens; }
|
||||||
|
|
||||||
|
export const DEFAULT_THEME_TOKENS: ThemeTokens = {
|
||||||
|
"bg-void": "#080808", "bg-base": "#0c0c0c", "bg-surface": "#101010",
|
||||||
|
"bg-raised": "#151515", "bg-overlay": "#1a1a1a", "bg-subtle": "#202020",
|
||||||
|
"border-dim": "#1c1c1c", "border-base": "#242424", "border-strong": "#2e2e2e", "border-focus": "#4a5c4a",
|
||||||
|
"text-primary": "#f0efec", "text-secondary": "#c8c6c0", "text-muted": "#8a8880",
|
||||||
|
"text-faint": "#4e4d4a", "text-disabled": "#2a2a28",
|
||||||
|
"accent": "#6b8f6b", "accent-dim": "#2a3d2a", "accent-muted": "#1a251a",
|
||||||
|
"accent-fg": "#a8c4a8", "accent-bright": "#8fb88f",
|
||||||
|
"color-error": "#c47a7a", "color-error-bg": "#1f1212",
|
||||||
|
"color-success": "#7aab7a", "color-info": "#7a9ec4", "color-info-bg": "#121a1f",
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MangaPrefs {
|
||||||
|
autoDownload: boolean; downloadAhead: number; deleteOnRead: boolean;
|
||||||
|
deleteDelayHours: number; maxKeepChapters: number; pauseUpdates: boolean;
|
||||||
|
refreshInterval: "global" | "daily" | "weekly" | "manual";
|
||||||
|
preferredScanlator: string; scanlatorFilter: string[];
|
||||||
|
autoDownloadScanlators: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MANGA_PREFS: MangaPrefs = {
|
||||||
|
autoDownload: false, downloadAhead: 0, deleteOnRead: false,
|
||||||
|
deleteDelayHours: 0, maxKeepChapters: 0, pauseUpdates: false,
|
||||||
|
refreshInterval: "global", preferredScanlator: "", scanlatorFilter: [],
|
||||||
|
autoDownloadScanlators: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReaderSettings {
|
||||||
|
pageStyle: PageStyle;
|
||||||
|
fitMode: FitMode;
|
||||||
|
readingDirection: ReadingDirection;
|
||||||
|
readerZoom: number;
|
||||||
|
pageGap: boolean;
|
||||||
|
optimizeContrast: boolean;
|
||||||
|
offsetDoubleSpreads: boolean;
|
||||||
|
barPosition?: "top" | "left" | "right";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReaderPreset {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
settings: ReaderSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
pageStyle: PageStyle; readingDirection: ReadingDirection; fitMode: FitMode;
|
||||||
|
readerZoom: number; pageGap: boolean; optimizeContrast: boolean;
|
||||||
|
offsetDoubleSpreads: boolean; preloadPages: number;
|
||||||
|
autoMarkRead: boolean; autoNextChapter: boolean;
|
||||||
|
libraryCropCovers: boolean; libraryPageSize: number;
|
||||||
|
showNsfw: boolean; discordRpc: boolean;
|
||||||
|
chapterSortDir: ChapterSortDir; chapterSortMode: ChapterSortMode; chapterPageSize: number;
|
||||||
|
uiZoom: number; compactSidebar: boolean; gpuAcceleration: boolean;
|
||||||
|
serverUrl: string; serverBinary: string; autoStartServer: boolean;
|
||||||
|
preferredExtensionLang: string; keybinds: Keybinds;
|
||||||
|
idleTimeoutMin?: number; splashCards?: boolean;
|
||||||
|
storageLimitGb: number | null; markReadOnNext: boolean; readerDebounceMs: number;
|
||||||
|
autoBookmark: boolean; theme: Theme; libraryBranches: boolean; renderLimit: number;
|
||||||
|
heroSlots: (number | null)[]; mangaLinks: Record<number, number[]>;
|
||||||
|
mangaPrefs: Record<number, Partial<MangaPrefs>>;
|
||||||
|
serverAuthUser: string; serverAuthPass: string;
|
||||||
|
serverAuthMode: "NONE" | "BASIC_AUTH" | "SIMPLE_LOGIN" | "UI_LOGIN";
|
||||||
|
socksProxyEnabled: boolean; socksProxyHost: string; socksProxyPort: string;
|
||||||
|
socksProxyVersion: number; socksProxyUsername: string; socksProxyPassword: string;
|
||||||
|
flareSolverrEnabled: boolean; flareSolverrUrl: string; flareSolverrTimeout: number;
|
||||||
|
flareSolverrSessionName: string; flareSolverrSessionTtl: number; flareSolverrFallback: boolean;
|
||||||
|
appLockEnabled: boolean; appLockPin: string;
|
||||||
|
customThemes: CustomTheme[]; hiddenCategoryIds: number[];
|
||||||
|
defaultLibraryCategoryId: number | null; savedIsDefaultCategory: boolean;
|
||||||
|
nsfwFilteredTags: string[]; nsfwAllowedSourceIds: string[]; nsfwBlockedSourceIds: string[];
|
||||||
|
libraryTabSort: Record<string, { mode: LibrarySortMode; dir: LibrarySortDir }>;
|
||||||
|
libraryTabStatus: Record<string, LibraryStatusFilter>;
|
||||||
|
libraryTabFilters: Record<string, Partial<Record<LibraryContentFilter, boolean>>>;
|
||||||
|
maxPageWidth?: number; uiScale?: number;
|
||||||
|
extraScanDirs: string[]; serverDownloadsPath: string; serverLocalSourcePath: string;
|
||||||
|
qolAnimations: boolean;
|
||||||
|
pinnedSourceIds: string[];
|
||||||
|
readerPresets: ReaderPreset[];
|
||||||
|
mangaReaderSettings: Record<number, ReaderSettings>;
|
||||||
|
barPosition?: "top" | "left" | "right";
|
||||||
|
trackerSyncBack: boolean;
|
||||||
|
trackerSyncBackThreshold: number | null;
|
||||||
|
trackerRespectScanlatorFilter: boolean;
|
||||||
|
pinchZoom?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: Settings = {
|
||||||
|
pageStyle: "longstrip", readingDirection: "ltr", fitMode: "width",
|
||||||
|
readerZoom: 1.0, pageGap: true, optimizeContrast: false, offsetDoubleSpreads: false,
|
||||||
|
preloadPages: 3, autoMarkRead: true, autoNextChapter: true,
|
||||||
|
libraryCropCovers: true, libraryPageSize: 48, showNsfw: false, discordRpc: false,
|
||||||
|
chapterSortDir: "desc", chapterSortMode: "source", chapterPageSize: 25,
|
||||||
|
uiZoom: 1.0, compactSidebar: false, gpuAcceleration: true,
|
||||||
|
serverUrl: "http://localhost:4567", serverBinary: "", autoStartServer: true,
|
||||||
|
preferredExtensionLang: "en", keybinds: DEFAULT_KEYBINDS,
|
||||||
|
idleTimeoutMin: 5, splashCards: true, storageLimitGb: null,
|
||||||
|
markReadOnNext: true, readerDebounceMs: 120, autoBookmark: true,
|
||||||
|
theme: "dark", libraryBranches: true, renderLimit: 48,
|
||||||
|
heroSlots: [null, null, null, null], mangaLinks: {}, mangaPrefs: {},
|
||||||
|
serverAuthUser: "", serverAuthPass: "", serverAuthMode: "NONE",
|
||||||
|
socksProxyEnabled: false, socksProxyHost: "", socksProxyPort: "1080",
|
||||||
|
socksProxyVersion: 5, socksProxyUsername: "", socksProxyPassword: "",
|
||||||
|
flareSolverrEnabled: false, flareSolverrUrl: "http://localhost:8191",
|
||||||
|
flareSolverrTimeout: 60, flareSolverrSessionName: "moku",
|
||||||
|
flareSolverrSessionTtl: 15, flareSolverrFallback: false,
|
||||||
|
appLockEnabled: false, appLockPin: "",
|
||||||
|
customThemes: [], hiddenCategoryIds: [], defaultLibraryCategoryId: null,
|
||||||
|
savedIsDefaultCategory: false,
|
||||||
|
nsfwFilteredTags: ["adult", "mature", "hentai", "ecchi", "erotic", "pornograph", "18+", "smut", "lemon", "explicit", "sexual violence"],
|
||||||
|
nsfwAllowedSourceIds: [], nsfwBlockedSourceIds: [],
|
||||||
|
libraryTabSort: {}, libraryTabStatus: {}, libraryTabFilters: {},
|
||||||
|
extraScanDirs: [], serverDownloadsPath: "", serverLocalSourcePath: "",
|
||||||
|
qolAnimations: true,
|
||||||
|
pinnedSourceIds: [],
|
||||||
|
readerPresets: [],
|
||||||
|
mangaReaderSettings: {},
|
||||||
|
trackerSyncBack: false,
|
||||||
|
trackerSyncBackThreshold: 20,
|
||||||
|
trackerRespectScanlatorFilter: true,
|
||||||
|
pinchZoom: false,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user