mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
chore: ported over extensions & settings
This commit is contained in:
@@ -1,3 +1,704 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
import { X, Book, Image, Sliders, Info, Keyboard, Gear, HardDrives, FolderSimple, Plus, Pencil, Trash, Wrench, PaintBrush } from "phosphor-svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { gql } from "../../lib/client";
|
||||
import { GET_DOWNLOADS_PATH } from "../../lib/queries";
|
||||
import { settings, settingsOpen, history, updateSettings, resetKeybinds, addFolder, removeFolder, renameFolder, toggleFolderTab } from "../../store";
|
||||
import { KEYBIND_LABELS, DEFAULT_KEYBINDS, eventToKeybind } from "../../lib/keybinds";
|
||||
import type { Settings, FitMode, Theme } from "../../store";
|
||||
import type { Keybinds } from "../../lib/keybinds";
|
||||
|
||||
type Tab = "general" | "appearance" | "reader" | "library" | "performance" | "keybinds" | "storage" | "folders" | "about" | "devtools";
|
||||
|
||||
const TABS: { id: Tab; label: string; icon: any }[] = [
|
||||
{ id: "general", label: "General", icon: Gear },
|
||||
{ id: "appearance", label: "Appearance", icon: PaintBrush },
|
||||
{ id: "reader", label: "Reader", icon: Book },
|
||||
{ id: "library", label: "Library", icon: Image },
|
||||
{ id: "performance",label: "Performance", icon: Sliders },
|
||||
{ id: "keybinds", label: "Keybinds", icon: Keyboard },
|
||||
{ id: "storage", label: "Storage", icon: HardDrives },
|
||||
{ id: "folders", label: "Folders", icon: FolderSimple },
|
||||
{ id: "about", label: "About", icon: Info },
|
||||
{ id: "devtools", label: "Dev Tools", icon: Wrench },
|
||||
];
|
||||
|
||||
const THEMES: { id: Theme; label: string; description: string; swatches: string[] }[] = [
|
||||
{ id: "dark", label: "Dark", description: "Default near-black", swatches: ["#101010","#151515","#a8c4a8","#f0efec"] },
|
||||
{ id: "high-contrast", label: "High Contrast", description: "Darker base, sharper text", swatches: ["#080808","#111111","#bcd8bc","#ffffff"] },
|
||||
{ id: "light", label: "Light", description: "Warm off-white", swatches: ["#f4f2ee","#faf8f4","#2a5a2a","#1a1916"] },
|
||||
{ id: "light-contrast", label: "Light Contrast", description: "Light with maximum contrast", swatches: ["#ece8e2","#f5f2ec","#183818","#080806"] },
|
||||
{ id: "midnight", label: "Midnight", description: "Deep blue-black tint", swatches: ["#0c1020","#101428","#a8b4e8","#eeeef8"] },
|
||||
{ id: "warm", label: "Warm", description: "Amber and sepia tones", swatches: ["#16130c","#1c1810","#e0b860","#f5f0e0"] },
|
||||
];
|
||||
|
||||
let tab: Tab = "general";
|
||||
let contentBodyEl: HTMLDivElement;
|
||||
|
||||
$: { tab; tick().then(() => contentBodyEl?.scrollTo({ top: 0 })); }
|
||||
|
||||
function close() { settingsOpen.set(false); }
|
||||
|
||||
function onKey(e: KeyboardEvent) { if (e.key === "Escape" && !listeningKey) close(); }
|
||||
onMount(() => window.addEventListener("keydown", onKey));
|
||||
onDestroy(() => window.removeEventListener("keydown", onKey));
|
||||
|
||||
// ── Keybinds ─────────────────────────────────────────────────────────────────
|
||||
let listeningKey: keyof Keybinds | null = null;
|
||||
|
||||
function startListen(key: keyof Keybinds) {
|
||||
listeningKey = listeningKey === key ? null : key;
|
||||
}
|
||||
|
||||
function onKeyCapture(e: KeyboardEvent) {
|
||||
if (!listeningKey) return;
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const bind = eventToKeybind(e);
|
||||
if (!bind) return;
|
||||
updateSettings({ keybinds: { ...$settings.keybinds, [listeningKey]: bind } });
|
||||
listeningKey = null;
|
||||
}
|
||||
|
||||
$: if (listeningKey) {
|
||||
window.addEventListener("keydown", onKeyCapture, true);
|
||||
} else {
|
||||
window.removeEventListener("keydown", onKeyCapture, true);
|
||||
}
|
||||
|
||||
// ── Storage ───────────────────────────────────────────────────────────────────
|
||||
interface StorageInfo { manga_bytes: number; total_bytes: number; free_bytes: number; path: string; }
|
||||
let storageInfo: StorageInfo | null = null;
|
||||
let storageLoading = false;
|
||||
let storageError: string | null = null;
|
||||
let clearing = false;
|
||||
let cleared = false;
|
||||
|
||||
async function fetchStorage() {
|
||||
storageLoading = true; storageError = null;
|
||||
try {
|
||||
const pathData = await gql<{ settings: { downloadsPath: string } }>(GET_DOWNLOADS_PATH);
|
||||
storageInfo = await invoke<StorageInfo>("get_storage_info", { downloadsPath: pathData.settings.downloadsPath });
|
||||
} catch (e: any) { storageError = e instanceof Error ? e.message : String(e); }
|
||||
finally { storageLoading = false; }
|
||||
}
|
||||
|
||||
$: if (tab === "storage" && !storageInfo && !storageLoading) fetchStorage();
|
||||
|
||||
function handleClearCache() {
|
||||
clearing = true;
|
||||
caches.keys().then((names) => Promise.all(names.map((n) => caches.delete(n)))).catch(() => {})
|
||||
.finally(() => { clearing = false; cleared = true; setTimeout(() => cleared = false, 2500); fetchStorage(); });
|
||||
}
|
||||
|
||||
function fmtBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B";
|
||||
const units = ["B","KB","MB","GB","TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(i >= 2 ? 1 : 0)} ${units[i]}`;
|
||||
}
|
||||
|
||||
// ── Folders ────────────────────────────────────────────────────────────────────
|
||||
let newFolderName = "";
|
||||
let editingId: string | null = null;
|
||||
let editingName = "";
|
||||
|
||||
function createFolder() {
|
||||
const name = newFolderName.trim();
|
||||
if (!name) return;
|
||||
addFolder(name); newFolderName = "";
|
||||
}
|
||||
|
||||
function startEdit(id: string, name: string) { editingId = id; editingName = name; }
|
||||
|
||||
function commitEdit() {
|
||||
if (editingId && editingName.trim()) renameFolder(editingId, editingName.trim());
|
||||
editingId = null; editingName = "";
|
||||
}
|
||||
|
||||
// ── Select dropdown ────────────────────────────────────────────────────────────
|
||||
let selectOpen: string | null = null;
|
||||
|
||||
function toggleSelect(id: string) { selectOpen = selectOpen === id ? null : id; }
|
||||
|
||||
function onSelectOutside(e: MouseEvent) {
|
||||
if (selectOpen && !(e.target as HTMLElement).closest(".select-wrap")) selectOpen = null;
|
||||
}
|
||||
|
||||
onMount(() => document.addEventListener("mousedown", onSelectOutside));
|
||||
onDestroy(() => document.removeEventListener("mousedown", onSelectOutside));
|
||||
|
||||
// ── DevTools ──────────────────────────────────────────────────────────────────
|
||||
let splashTriggered = false;
|
||||
function triggerSplash() {
|
||||
splashTriggered = true;
|
||||
setTimeout(() => splashTriggered = false, 200);
|
||||
(window as any).__mokuShowSplash?.();
|
||||
}
|
||||
</script>
|
||||
<div>Settings stub</div>
|
||||
|
||||
<div class="backdrop" on:click={(e) => { if (e.target === e.currentTarget) close(); }}>
|
||||
<div class="modal" role="dialog" aria-label="Settings">
|
||||
<div class="sidebar">
|
||||
<p class="modal-title">Settings</p>
|
||||
<nav class="nav">
|
||||
{#each TABS as t}
|
||||
<button class="nav-item" class:active={tab === t.id} on:click={() => tab = t.id}>
|
||||
<svelte:component this={t.icon} size={14} weight="light" />
|
||||
<span>{t.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content-header">
|
||||
<p class="content-title">{TABS.find((t) => t.id === tab)?.label}</p>
|
||||
<button class="close-btn" on:click={close}><X size={15} weight="light" /></button>
|
||||
</div>
|
||||
|
||||
<div class="content-body" bind:this={contentBodyEl}>
|
||||
|
||||
<!-- GENERAL -->
|
||||
{#if tab === "general"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Interface Scale</p>
|
||||
<div class="scale-row">
|
||||
<input type="range" min={70} max={150} step={5} value={$settings.uiScale}
|
||||
on:input={(e) => updateSettings({ uiScale: Number(e.currentTarget.value) })} class="scale-slider" />
|
||||
<span class="scale-val">{$settings.uiScale}%</span>
|
||||
<button class="step-btn" on:click={() => updateSettings({ uiScale: 100 })} disabled={$settings.uiScale === 100} title="Reset">↺</button>
|
||||
</div>
|
||||
<p class="scale-hint">
|
||||
{#each [70,80,90,100,110,125,150] as v}
|
||||
<button class="scale-preset" class:active={$settings.uiScale === v} on:click={() => updateSettings({ uiScale: v })}>{v}%</button>
|
||||
{/each}
|
||||
</p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Server</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Server URL</span><span class="toggle-desc">Base URL of your Suwayomi instance</span></div>
|
||||
<input class="text-input" value={$settings.serverUrl ?? "http://localhost:4567"} on:input={(e) => updateSettings({ serverUrl: e.currentTarget.value })} placeholder="http://localhost:4567" spellcheck="false" />
|
||||
</div>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Server binary</span><span class="toggle-desc">Path or command to launch tachidesk-server</span></div>
|
||||
<input class="text-input" value={$settings.serverBinary} on:input={(e) => updateSettings({ serverBinary: e.currentTarget.value })} placeholder="tachidesk-server" spellcheck="false" />
|
||||
</div>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Auto-start server</span><span class="toggle-desc">Launch tachidesk-server when Moku opens</span></div>
|
||||
<button role="switch" aria-checked={$settings.autoStartServer} class="toggle" class:on={$settings.autoStartServer} on:click={() => updateSettings({ autoStartServer: !$settings.autoStartServer })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Inactivity</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Idle screen timeout</span><span class="toggle-desc">Show the Moku idle splash after this much inactivity.</span></div>
|
||||
<div class="select-wrap" id="idle-timeout">
|
||||
<button class="select-btn" on:click={() => toggleSelect("idle-timeout")}>
|
||||
<span>{{ "0":"Never","1":"1 minute","2":"2 minutes","5":"5 minutes","10":"10 minutes","15":"15 minutes","30":"30 minutes" }[String($settings.idleTimeoutMin ?? 5)] ?? `${$settings.idleTimeoutMin} min`}</span>
|
||||
<svg class="select-caret" class:open={selectOpen === "idle-timeout"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === "idle-timeout"}
|
||||
<div class="select-menu">
|
||||
{#each [["0","Never"],["1","1 minute"],["2","2 minutes"],["5","5 minutes"],["10","10 minutes"],["15","15 minutes"],["30","30 minutes"]] as [v, l]}
|
||||
<button class="select-option" class:active={String($settings.idleTimeoutMin ?? 5) === v} on:click={() => { updateSettings({ idleTimeoutMin: Number(v) }); selectOpen = null; }}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- APPEARANCE -->
|
||||
{:else if tab === "appearance"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Theme</p>
|
||||
<div class="theme-grid">
|
||||
{#each THEMES as theme}
|
||||
{@const active = ($settings.theme ?? "dark") === theme.id}
|
||||
<button class="theme-card" class:active on:click={() => updateSettings({ theme: theme.id })} title={theme.description}>
|
||||
<div class="theme-preview">
|
||||
<div class="theme-preview-bg" style="background:{theme.swatches[0]}">
|
||||
<div class="theme-preview-sidebar" style="background:{theme.swatches[1]}"></div>
|
||||
<div class="theme-preview-content">
|
||||
<div class="theme-preview-accent" style="background:{theme.swatches[2]}"></div>
|
||||
<div class="theme-preview-text" style="background:{theme.swatches[3]}55"></div>
|
||||
<div class="theme-preview-text" style="background:{theme.swatches[3]}33;width:60%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="theme-card-info">
|
||||
<span class="theme-card-label">{theme.label}</span>
|
||||
<span class="theme-card-desc">{theme.description}</span>
|
||||
</div>
|
||||
{#if active}<span class="theme-card-check">✓</span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- READER -->
|
||||
{:else if tab === "reader"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Page Layout</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Default layout</span><span class="toggle-desc">How chapters open by default</span></div>
|
||||
<div class="select-wrap" id="page-style">
|
||||
<button class="select-btn" on:click={() => toggleSelect("page-style")}>
|
||||
<span>{{ "single":"Single page","longstrip":"Long strip" }[$settings.pageStyle === "double" ? "single" : $settings.pageStyle]}</span>
|
||||
<svg class="select-caret" class:open={selectOpen === "page-style"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === "page-style"}
|
||||
<div class="select-menu">
|
||||
{#each [["single","Single page"],["longstrip","Long strip"]] as [v, l]}
|
||||
<button class="select-option" class:active={($settings.pageStyle === "double" ? "single" : $settings.pageStyle) === v} on:click={() => { updateSettings({ pageStyle: v as Settings["pageStyle"] }); selectOpen = null; }}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Reading direction</span><span class="toggle-desc">Left-to-right for most manga, right-to-left for Japanese</span></div>
|
||||
<div class="select-wrap" id="reading-dir">
|
||||
<button class="select-btn" on:click={() => toggleSelect("reading-dir")}>
|
||||
<span>{{ "ltr":"Left to right","rtl":"Right to left" }[$settings.readingDirection]}</span>
|
||||
<svg class="select-caret" class:open={selectOpen === "reading-dir"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === "reading-dir"}
|
||||
<div class="select-menu">
|
||||
{#each [["ltr","Left to right"],["rtl","Right to left"]] as [v, l]}
|
||||
<button class="select-option" class:active={$settings.readingDirection === v} on:click={() => { updateSettings({ readingDirection: v as Settings["readingDirection"] }); selectOpen = null; }}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Page gap</span><span class="toggle-desc">Add spacing between pages in longstrip mode</span></div>
|
||||
<button role="switch" aria-checked={$settings.pageGap} class="toggle" class:on={$settings.pageGap} on:click={() => updateSettings({ pageGap: !$settings.pageGap })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Fit & Zoom</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Default fit mode</span><span class="toggle-desc">How pages are sized to fit the screen</span></div>
|
||||
<div class="select-wrap" id="fit-mode">
|
||||
<button class="select-btn" on:click={() => toggleSelect("fit-mode")}>
|
||||
<span>{{ "width":"Fit width","height":"Fit height","screen":"Fit screen","original":"Original (1:1)" }[$settings.fitMode ?? "width"]}</span>
|
||||
<svg class="select-caret" class:open={selectOpen === "fit-mode"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === "fit-mode"}
|
||||
<div class="select-menu">
|
||||
{#each [["width","Fit width"],["height","Fit height"],["screen","Fit screen"],["original","Original (1:1)"]] as [v, l]}
|
||||
<button class="select-option" class:active={($settings.fitMode ?? "width") === v} on:click={() => { updateSettings({ fitMode: v as FitMode }); selectOpen = null; }}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Max page width</span><span class="toggle-desc">Pixel cap for fit-width mode.</span></div>
|
||||
<div class="step-controls">
|
||||
<button class="step-btn" on:click={() => updateSettings({ maxPageWidth: Math.max(200, ($settings.maxPageWidth ?? 900) - 100) })}>−</button>
|
||||
<span class="step-val">{$settings.maxPageWidth ?? 900}px</span>
|
||||
<button class="step-btn" on:click={() => updateSettings({ maxPageWidth: Math.min(2400, ($settings.maxPageWidth ?? 900) + 100) })}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Optimize contrast</span><span class="toggle-desc">Use webkit-optimize-contrast rendering</span></div>
|
||||
<button role="switch" aria-checked={$settings.optimizeContrast} class="toggle" class:on={$settings.optimizeContrast} on:click={() => updateSettings({ optimizeContrast: !$settings.optimizeContrast })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Behaviour</p>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Auto-mark chapters read</span><span class="toggle-desc">Mark a chapter as read when you reach the last page</span></div>
|
||||
<button role="switch" aria-checked={$settings.autoMarkRead} class="toggle" class:on={$settings.autoMarkRead} on:click={() => updateSettings({ autoMarkRead: !$settings.autoMarkRead })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Auto-advance chapters</span><span class="toggle-desc">Automatically open the next chapter at the end of a long strip</span></div>
|
||||
<button role="switch" aria-checked={$settings.autoNextChapter ?? false} class="toggle" class:on={$settings.autoNextChapter} on:click={() => updateSettings({ autoNextChapter: !($settings.autoNextChapter ?? false) })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
{#if !($settings.autoNextChapter ?? false)}
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Mark read when skipping to next chapter</span><span class="toggle-desc">Mark chapter as read when you tap next before finishing</span></div>
|
||||
<button role="switch" aria-checked={$settings.markReadOnNext ?? true} class="toggle" class:on={$settings.markReadOnNext ?? true} on:click={() => updateSettings({ markReadOnNext: !($settings.markReadOnNext ?? true) })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Pages to preload</span><span class="toggle-desc">Images loaded ahead of the current page</span></div>
|
||||
<div class="step-controls">
|
||||
<button class="step-btn" on:click={() => updateSettings({ preloadPages: Math.max(0, $settings.preloadPages - 1) })} disabled={$settings.preloadPages <= 0}>−</button>
|
||||
<span class="step-val">{$settings.preloadPages}</span>
|
||||
<button class="step-btn" on:click={() => updateSettings({ preloadPages: Math.min(10, $settings.preloadPages + 1) })} disabled={$settings.preloadPages >= 10}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LIBRARY -->
|
||||
{:else if tab === "library"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Display</p>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Crop cover images</span><span class="toggle-desc">Fill grid cells — may crop cover edges</span></div>
|
||||
<button role="switch" aria-checked={$settings.libraryCropCovers} class="toggle" class:on={$settings.libraryCropCovers} on:click={() => updateSettings({ libraryCropCovers: !$settings.libraryCropCovers })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Show NSFW sources</span><span class="toggle-desc">Display adult content sources in the sources list</span></div>
|
||||
<button role="switch" aria-checked={$settings.showNsfw} class="toggle" class:on={$settings.showNsfw} on:click={() => updateSettings({ showNsfw: !$settings.showNsfw })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Chapters</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Default sort direction</span></div>
|
||||
<div class="select-wrap" id="sort-dir">
|
||||
<button class="select-btn" on:click={() => toggleSelect("sort-dir")}>
|
||||
<span>{{ "desc":"Newest first","asc":"Oldest first" }[$settings.chapterSortDir]}</span>
|
||||
<svg class="select-caret" class:open={selectOpen === "sort-dir"} width="10" height="6" viewBox="0 0 10 6"><path d="M0 0l5 6 5-6" fill="currentColor"/></svg>
|
||||
</button>
|
||||
{#if selectOpen === "sort-dir"}
|
||||
<div class="select-menu">
|
||||
{#each [["desc","Newest first"],["asc","Oldest first"]] as [v, l]}
|
||||
<button class="select-option" class:active={$settings.chapterSortDir === v} on:click={() => { updateSettings({ chapterSortDir: v as Settings["chapterSortDir"] }); selectOpen = null; }}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">History</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Reading history</span><span class="toggle-desc">{$history.length} entries stored</span></div>
|
||||
<button class="danger-btn" on:click={() => history.set([])} disabled={$history.length === 0}>Clear history</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PERFORMANCE -->
|
||||
{:else if tab === "performance"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Rendering</p>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">GPU acceleration</span><span class="toggle-desc">Promote reader and library to compositor layers</span></div>
|
||||
<button role="switch" aria-checked={$settings.gpuAcceleration} class="toggle" class:on={$settings.gpuAcceleration} on:click={() => updateSettings({ gpuAcceleration: !$settings.gpuAcceleration })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Idle / Splash Screen</p>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Animated card background</span><span class="toggle-desc">Show floating manga cards on splash and idle screens.</span></div>
|
||||
<button role="switch" aria-checked={$settings.splashCards ?? true} class="toggle" class:on={$settings.splashCards ?? true} on:click={() => updateSettings({ splashCards: !($settings.splashCards ?? true) })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Interface</p>
|
||||
<label class="toggle-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Compact sidebar</span><span class="toggle-desc">Reduce sidebar icon spacing</span></div>
|
||||
<button role="switch" aria-checked={$settings.compactSidebar} class="toggle" class:on={$settings.compactSidebar} on:click={() => updateSettings({ compactSidebar: !$settings.compactSidebar })}><span class="toggle-thumb"></span></button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KEYBINDS -->
|
||||
{:else if tab === "keybinds"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<div class="kb-header">
|
||||
<p class="section-title">Keyboard shortcuts</p>
|
||||
<button class="reset-all-btn" on:click={resetKeybinds}>Reset all</button>
|
||||
</div>
|
||||
<p class="kb-hint">Click a key to rebind, then press the new combination.</p>
|
||||
<div class="kb-list">
|
||||
{#each Object.keys(KEYBIND_LABELS) as key}
|
||||
{@const k = key as keyof Keybinds}
|
||||
{@const isListening = listeningKey === k}
|
||||
{@const isDefault = $settings.keybinds[k] === DEFAULT_KEYBINDS[k]}
|
||||
<div class="kb-row">
|
||||
<span class="kb-label">{KEYBIND_LABELS[k]}</span>
|
||||
<div class="kb-right">
|
||||
<button class="kb-bind" class:listening={isListening} on:click={() => startListen(k)}>
|
||||
{isListening ? "Press key…" : $settings.keybinds[k]}
|
||||
</button>
|
||||
<button class="kb-reset" on:click={() => updateSettings({ keybinds: { ...$settings.keybinds, [k]: DEFAULT_KEYBINDS[k] } })} disabled={isDefault} title="Reset">↺</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- STORAGE -->
|
||||
{:else if tab === "storage"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Disk Usage</p>
|
||||
{#if storageLoading}<p class="storage-loading">Reading filesystem…</p>
|
||||
{:else if storageError}<p class="storage-loading" style="color:var(--color-error)">{storageError}</p>
|
||||
{:else if storageInfo}
|
||||
{@const mangaBytes = storageInfo.manga_bytes}
|
||||
{@const totalBytes = storageInfo.total_bytes}
|
||||
{@const freeBytes = storageInfo.free_bytes}
|
||||
{@const limitGb = $settings.storageLimitGb ?? null}
|
||||
{@const limitBytes = limitGb !== null ? limitGb * 1024 ** 3 : null}
|
||||
{@const available = mangaBytes + freeBytes}
|
||||
{@const cap = limitBytes !== null ? Math.min(limitBytes, available) : available}
|
||||
{@const pctUsed = cap > 0 ? Math.min(100, (mangaBytes / cap) * 100) : 0}
|
||||
<div class="storage-bar-wrap">
|
||||
<div class="storage-bar">
|
||||
<div class="storage-bar-fill" class:critical={pctUsed > 90} class:warn={pctUsed > 75 && pctUsed <= 90} style="width:{pctUsed}%"></div>
|
||||
</div>
|
||||
<div class="storage-bar-labels">
|
||||
<span class="storage-bar-used">{fmtBytes(mangaBytes)} used</span>
|
||||
<span class="storage-bar-free">{fmtBytes(Math.max(0, cap - mangaBytes))} free</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="storage-legend">
|
||||
<div class="storage-legend-row"><span class="storage-dot storage-dot-manga"></span><span class="storage-legend-label">Downloaded manga</span><span class="storage-legend-val">{fmtBytes(mangaBytes)}</span></div>
|
||||
<div class="storage-legend-row"><span class="storage-dot storage-dot-free"></span><span class="storage-legend-label">Drive free</span><span class="storage-legend-val">{fmtBytes(freeBytes)}</span></div>
|
||||
<div class="storage-legend-row"><span class="storage-dot storage-dot-app"></span><span class="storage-legend-label">Drive total</span><span class="storage-legend-val">{fmtBytes(totalBytes)}</span></div>
|
||||
</div>
|
||||
<p class="storage-path-note">{storageInfo.path}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Cache</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Image cache</span><span class="toggle-desc">Cached page images stored by the webview</span></div>
|
||||
<button class="danger-btn" on:click={handleClearCache} disabled={clearing}>
|
||||
{cleared ? "Cleared" : clearing ? "Clearing…" : "Clear cache"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FOLDERS -->
|
||||
{:else if tab === "folders"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Manage Folders</p>
|
||||
<p class="toggle-desc" style="padding:0 var(--sp-3) var(--sp-3);display:block">Assign manga to folders from the series detail page.</p>
|
||||
<div class="folder-create-row">
|
||||
<input class="text-input" placeholder="New folder name…" bind:value={newFolderName}
|
||||
on:keydown={(e) => e.key === "Enter" && createFolder()} style="flex:1;width:auto" />
|
||||
<button class="folder-create-btn" on:click={createFolder} disabled={!newFolderName.trim()}>
|
||||
<Plus size={13} weight="bold" /> Create
|
||||
</button>
|
||||
</div>
|
||||
{#if $settings.folders.length === 0}
|
||||
<p class="storage-loading">No folders yet. Create one above.</p>
|
||||
{:else}
|
||||
<div class="folder-list">
|
||||
{#each $settings.folders as folder}
|
||||
<div class="folder-row">
|
||||
{#if editingId === folder.id}
|
||||
<input class="text-input" bind:value={editingName}
|
||||
on:keydown={(e) => { if (e.key === "Enter") commitEdit(); if (e.key === "Escape") editingId = null; }}
|
||||
on:blur={commitEdit} style="flex:1;width:auto" use:focusInput />
|
||||
<button class="kb-reset" on:click={commitEdit} title="Save">✓</button>
|
||||
{:else}
|
||||
<FolderSimple size={14} weight="light" style="color:var(--text-faint);flex-shrink:0" />
|
||||
<span class="folder-row-name">{folder.name}</span>
|
||||
<span class="folder-row-count">{folder.mangaIds.length} manga</span>
|
||||
<button class="folder-tab-toggle" class:on={folder.showTab} on:click={() => toggleFolderTab(folder.id)}>
|
||||
{folder.showTab ? "Tab on" : "Tab off"}
|
||||
</button>
|
||||
<button class="kb-reset" on:click={() => startEdit(folder.id, folder.name)} title="Rename"><Pencil size={12} weight="light" /></button>
|
||||
<button class="kb-reset folder-delete" on:click={() => removeFolder(folder.id)} title="Delete"><Trash size={12} weight="light" /></button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ABOUT -->
|
||||
{:else if tab === "about"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Moku</p>
|
||||
<div class="about-block">
|
||||
<p class="about-line">A manga reader frontend for Suwayomi / Tachidesk.</p>
|
||||
<p class="about-line" style="color:var(--text-faint);margin-top:var(--sp-2)">Built with Tauri + Svelte. Connects to tachidesk-server.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- DEVTOOLS -->
|
||||
{:else if tab === "devtools"}
|
||||
<div class="panel">
|
||||
<div class="section">
|
||||
<p class="section-title">Splash Screen</p>
|
||||
<div class="step-row">
|
||||
<div class="toggle-info"><span class="toggle-label">Preview idle screen</span><span class="toggle-desc">Show the idle splash — dismiss with any click or key</span></div>
|
||||
<button class="danger-btn" on:click={triggerSplash}
|
||||
style={splashTriggered ? "background:var(--accent-fg);color:var(--bg-base);border-color:var(--accent-fg);transition:all 0.15s ease" : ""}>
|
||||
Show idle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<p class="section-title">Build Info</p>
|
||||
<div class="about-block">
|
||||
<p class="about-line" style="font-family:monospace;font-size:11px;color:var(--text-faint)">Mode: {import.meta.env.MODE}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script context="module">
|
||||
function focusInput(node: HTMLElement) { node.focus(); }
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: var(--z-settings); display: flex; align-items: center; justify-content: center; animation: fadeIn 0.1s ease both; backdrop-filter: blur(4px); }
|
||||
.modal { width: min(720px, calc(100vw - 48px)); height: min(600px, calc(100vh - 80px)); display: flex; background: var(--bg-surface); border: 1px solid var(--border-base); border-radius: var(--radius-xl); overflow: hidden; animation: scaleIn 0.15s ease both; box-shadow: 0 24px 64px rgba(0,0,0,0.6); }
|
||||
.sidebar { width: 168px; flex-shrink: 0; background: var(--bg-base); border-right: 1px solid var(--border-dim); padding: var(--sp-5) var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-1); overflow-y: auto; }
|
||||
.modal-title { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: 0 var(--sp-2) var(--sp-3); }
|
||||
.nav { display: flex; flex-direction: column; gap: 1px; }
|
||||
.nav-item { display: flex; align-items: center; gap: var(--sp-2); padding: 6px var(--sp-2); border-radius: var(--radius-md); font-size: var(--text-sm); color: var(--text-muted); background: none; border: none; cursor: pointer; text-align: left; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.nav-item:hover { background: var(--bg-raised); color: var(--text-secondary); }
|
||||
.nav-item.active { background: var(--accent-muted); color: var(--accent-fg); }
|
||||
.content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
|
||||
.content-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-5) var(--sp-6) var(--sp-4); border-bottom: 1px solid var(--border-dim); flex-shrink: 0; }
|
||||
.content-title { font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-secondary); }
|
||||
.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); }
|
||||
.close-btn:hover { color: var(--text-muted); background: var(--bg-raised); }
|
||||
.content-body { flex: 1; overflow-y: auto; }
|
||||
|
||||
.panel { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-4) var(--sp-6); }
|
||||
.section { display: flex; flex-direction: column; gap: 1px; border-bottom: 1px solid var(--border-dim); padding-bottom: var(--sp-4); margin-bottom: var(--sp-2); }
|
||||
.section:last-child { border-bottom: none; }
|
||||
.section-title { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; padding: var(--sp-3) var(--sp-3) var(--sp-2); }
|
||||
|
||||
.toggle-row { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3); border-radius: var(--radius-md); cursor: default; transition: background var(--t-fast); }
|
||||
.toggle-row:hover { background: var(--bg-raised); }
|
||||
.toggle-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; margin-right: var(--sp-4); }
|
||||
.toggle-label { font-size: var(--text-sm); color: var(--text-secondary); }
|
||||
.toggle-desc { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); line-height: var(--leading-snug); }
|
||||
.toggle { position: relative; width: 32px; height: 18px; border-radius: var(--radius-full); border: none; background: var(--bg-overlay); cursor: pointer; flex-shrink: 0; transition: background var(--t-base); border: 1px solid var(--border-strong); }
|
||||
.toggle.on { background: var(--accent); border-color: var(--accent); }
|
||||
.toggle-thumb { position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--text-faint); transition: transform var(--t-base), background var(--t-base); }
|
||||
.toggle.on .toggle-thumb { transform: translateX(14px); background: var(--bg-void); }
|
||||
|
||||
.step-row { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3); border-radius: var(--radius-md); transition: background var(--t-fast); gap: var(--sp-3); }
|
||||
.step-row:hover { background: var(--bg-raised); }
|
||||
.step-controls { display: flex; align-items: center; gap: var(--sp-1); flex-shrink: 0; }
|
||||
.step-btn { font-family: var(--font-ui); font-size: var(--text-sm); width: 26px; height: 26px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base); display: flex; align-items: center; justify-content: center; }
|
||||
.step-btn:hover:not(:disabled) { color: var(--text-secondary); border-color: var(--border-strong); }
|
||||
.step-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
.step-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); letter-spacing: var(--tracking-wide); min-width: 40px; text-align: center; }
|
||||
|
||||
.select-wrap { position: relative; flex-shrink: 0; }
|
||||
.select-btn { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-sm); color: var(--text-secondary); background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px; cursor: pointer; min-width: 130px; transition: border-color var(--t-base); }
|
||||
.select-btn:hover { border-color: var(--border-strong); }
|
||||
.select-caret { color: var(--text-faint); transition: transform var(--t-base); flex-shrink: 0; margin-left: auto; }
|
||||
.select-caret.open { transform: rotate(180deg); }
|
||||
.select-menu { position: absolute; top: calc(100% + 4px); right: 0; min-width: 100%; background: var(--bg-raised); border: 1px solid var(--border-base); border-radius: var(--radius-md); padding: var(--sp-1); z-index: 200; box-shadow: 0 8px 24px rgba(0,0,0,0.4); animation: scaleIn 0.1s ease both; transform-origin: top right; }
|
||||
.select-option { display: block; width: 100%; padding: 6px var(--sp-3); border-radius: var(--radius-sm); font-size: var(--text-sm); color: var(--text-secondary); background: none; border: none; cursor: pointer; text-align: left; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.select-option:hover { background: var(--bg-overlay); color: var(--text-primary); }
|
||||
.select-option.active { color: var(--accent-fg); background: var(--accent-muted); }
|
||||
|
||||
.text-input { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 5px 10px; color: var(--text-primary); font-size: var(--text-sm); outline: none; width: 200px; transition: border-color var(--t-base); flex-shrink: 0; }
|
||||
.text-input:focus { border-color: var(--border-strong); }
|
||||
|
||||
.danger-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 5px 12px; border-radius: var(--radius-md); border: 1px solid var(--color-error); background: none; color: var(--color-error); cursor: pointer; flex-shrink: 0; transition: background var(--t-base); }
|
||||
.danger-btn:hover:not(:disabled) { background: var(--color-error-bg); }
|
||||
.danger-btn:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
.scale-row { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
|
||||
.scale-slider { flex: 1; }
|
||||
.scale-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); min-width: 40px; text-align: center; }
|
||||
.scale-hint { padding: 0 var(--sp-3) var(--sp-2); display: flex; gap: var(--sp-1); flex-wrap: wrap; }
|
||||
.scale-preset { font-family: var(--font-ui); font-size: var(--text-2xs); padding: 2px 7px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: none; color: var(--text-faint); cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
.scale-preset:hover { color: var(--text-muted); border-color: var(--border-strong); }
|
||||
.scale-preset.active { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
|
||||
|
||||
/* Theme */
|
||||
.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: var(--sp-3); padding: var(--sp-2) var(--sp-3); }
|
||||
.theme-card { background: var(--bg-raised); border: 1px solid var(--border-dim); border-radius: var(--radius-lg); overflow: hidden; cursor: pointer; text-align: left; transition: border-color var(--t-base), box-shadow var(--t-base); position: relative; }
|
||||
.theme-card:hover { border-color: var(--border-strong); }
|
||||
.theme-card.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
|
||||
.theme-preview { height: 70px; overflow: hidden; }
|
||||
.theme-preview-bg { width: 100%; height: 100%; display: flex; }
|
||||
.theme-preview-sidebar { width: 20%; height: 100%; flex-shrink: 0; }
|
||||
.theme-preview-content { flex: 1; padding: 8px 6px; display: flex; flex-direction: column; gap: 5px; }
|
||||
.theme-preview-accent { height: 6px; width: 50%; border-radius: 3px; }
|
||||
.theme-preview-text { height: 4px; width: 100%; border-radius: 2px; }
|
||||
.theme-card-info { padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; }
|
||||
.theme-card-label { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); }
|
||||
.theme-card-desc { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
.theme-card-check { position: absolute; top: 6px; right: 6px; font-size: 10px; color: var(--accent-fg); background: var(--accent-muted); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 1px 4px; }
|
||||
|
||||
/* Keybinds */
|
||||
.kb-header { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--sp-3) var(--sp-2); }
|
||||
.reset-all-btn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 4px 10px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: none; color: var(--text-muted); cursor: pointer; transition: color var(--t-base), border-color var(--t-base); }
|
||||
.reset-all-btn:hover { color: var(--text-secondary); border-color: var(--border-strong); }
|
||||
.kb-hint { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: 0 var(--sp-3) var(--sp-3); }
|
||||
.kb-list { display: flex; flex-direction: column; gap: 1px; }
|
||||
.kb-row { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-md); transition: background var(--t-fast); }
|
||||
.kb-row:hover { background: var(--bg-raised); }
|
||||
.kb-label { font-size: var(--text-sm); color: var(--text-secondary); flex: 1; }
|
||||
.kb-right { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.kb-bind { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 4px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border-dim); background: var(--bg-raised); color: var(--text-secondary); cursor: pointer; min-width: 90px; text-align: center; transition: border-color var(--t-base), color var(--t-base); }
|
||||
.kb-bind:hover { border-color: var(--border-strong); }
|
||||
.kb-bind.listening { border-color: var(--accent); color: var(--accent-fg); background: var(--accent-muted); animation: pulse 1s ease infinite; }
|
||||
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.6 } }
|
||||
.kb-reset { font-size: var(--text-sm); color: var(--text-faint); padding: 3px 6px; border-radius: var(--radius-sm); border: 1px solid transparent; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
.kb-reset:hover:not(:disabled) { color: var(--text-muted); border-color: var(--border-dim); background: var(--bg-overlay); }
|
||||
.kb-reset:disabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
/* Storage */
|
||||
.storage-loading { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-3); }
|
||||
.storage-bar-wrap { padding: var(--sp-2) var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }
|
||||
.storage-bar { height: 6px; background: var(--bg-overlay); border-radius: var(--radius-full); overflow: hidden; }
|
||||
.storage-bar-fill { height: 100%; background: var(--accent); border-radius: var(--radius-full); transition: width 0.4s ease; }
|
||||
.storage-bar-fill.warn { background: #d97706; }
|
||||
.storage-bar-fill.critical { background: var(--color-error); }
|
||||
.storage-bar-labels { display: flex; justify-content: space-between; }
|
||||
.storage-bar-used, .storage-bar-free { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
|
||||
.storage-legend { display: flex; flex-direction: column; gap: var(--sp-1); padding: 0 var(--sp-3); }
|
||||
.storage-legend-row { display: flex; align-items: center; gap: var(--sp-2); }
|
||||
.storage-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.storage-dot-manga { background: var(--accent); }
|
||||
.storage-dot-free { background: var(--bg-overlay); border: 1px solid var(--border-strong); }
|
||||
.storage-dot-app { background: var(--text-faint); }
|
||||
.storage-legend-label { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-muted); flex: 1; }
|
||||
.storage-legend-val { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-secondary); }
|
||||
.storage-path-note { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); padding: var(--sp-2) var(--sp-3) 0; word-break: break-all; }
|
||||
|
||||
/* Folders */
|
||||
.folder-create-row { display: flex; gap: var(--sp-2); padding: 0 var(--sp-3) var(--sp-3); }
|
||||
.folder-create-btn { display: flex; align-items: center; gap: var(--sp-1); font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); padding: 5px 12px; border-radius: var(--radius-md); background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); cursor: pointer; flex-shrink: 0; transition: filter var(--t-base); }
|
||||
.folder-create-btn:hover:not(:disabled) { filter: brightness(1.1); }
|
||||
.folder-create-btn:disabled { opacity: 0.4; cursor: default; }
|
||||
.folder-list { display: flex; flex-direction: column; gap: 1px; padding: 0 var(--sp-3); }
|
||||
.folder-row { display: flex; align-items: center; gap: var(--sp-2); padding: 8px var(--sp-2); border-radius: var(--radius-md); transition: background var(--t-fast); }
|
||||
.folder-row:hover { background: var(--bg-raised); }
|
||||
.folder-row-name { flex: 1; font-size: var(--text-sm); color: var(--text-secondary); }
|
||||
.folder-row-count { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
|
||||
.folder-tab-toggle { 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; flex-shrink: 0; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||
.folder-tab-toggle.on { background: var(--accent-muted); border-color: var(--accent-dim); color: var(--accent-fg); }
|
||||
.folder-tab-toggle:hover { color: var(--text-muted); border-color: var(--border-strong); }
|
||||
.folder-delete:hover:not(:disabled) { color: var(--color-error) !important; }
|
||||
|
||||
/* About */
|
||||
.about-block { padding: 0 var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-1); }
|
||||
.about-line { font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-base); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user