Chore: Finalized Svelte-5 Rewrite (Testing Phase)

This commit is contained in:
Youwes09
2026-03-20 15:58:35 -05:00
parent 96bac1ad2b
commit 4903b066b1
26 changed files with 1460 additions and 1512 deletions
+21 -18
View File
@@ -1,34 +1,37 @@
<script lang="ts">
import { navPage, activeManga } from "../../store";
import Sidebar from "./Sidebar.svelte";
import Home from "../pages/Home.svelte";
import Library from "../pages/Library.svelte";
import SeriesDetail from "../pages/SeriesDetail.svelte";
import History from "../pages/History.svelte";
import Search from "../pages/Search.svelte";
import Discover from "../pages/Discover.svelte";
import Downloads from "../pages/Downloads.svelte";
import Extensions from "../pages/Extensions.svelte";
import { store } from "../../store/state.svelte";
import Sidebar from "./Sidebar.svelte";
import Home from "../pages/Home.svelte";
import Library from "../pages/Library.svelte";
import SeriesDetail from "../pages/SeriesDetail.svelte";
import History from "../pages/History.svelte";
import Search from "../pages/Search.svelte";
import Discover from "../pages/Discover.svelte";
import GenreDrillPage from "../pages/GenreDrillPage.svelte";
import Downloads from "../pages/Downloads.svelte";
import Extensions from "../pages/Extensions.svelte";
</script>
<div class="root">
<Sidebar />
<main class="main">
{#if activeManga}
{#if store.activeManga}
<SeriesDetail />
{:else if navPage === "home"}
{:else if store.navPage === "home"}
<Home />
{:else if navPage === "library"}
{:else if store.navPage === "library"}
<Library />
{:else if navPage === "search"}
{:else if store.navPage === "search"}
<Search />
{:else if navPage === "history"}
{:else if store.navPage === "history"}
<History />
{:else if navPage === "explore" || navPage === "sources"}
{:else if (store.navPage === "explore" || store.navPage === "sources") && store.genreFilter}
<GenreDrillPage />
{:else if store.navPage === "explore" || store.navPage === "sources"}
<Discover />
{:else if navPage === "downloads"}
{:else if store.navPage === "downloads"}
<Downloads />
{:else if navPage === "extensions"}
{:else if store.navPage === "extensions"}
<Extensions />
{:else}
<Home />
+51 -57
View File
@@ -1,13 +1,12 @@
<script lang="ts">
import { ClockCounterClockwise, Trash, MagnifyingGlass, Play, Books, Fire, BookOpen, Clock, TrendUp } from "phosphor-svelte";
import { thumbUrl } from "../../lib/client";
import { history, readingStats, clearHistory, activeManga, activeChapterList, openReader } from "../../store";
import type { HistoryEntry } from "../../store";
import { store, clearHistory, openReader, setActiveManga } from "../../store/state.svelte";
import type { HistoryEntry } from "../../store/state.svelte";
let search = "";
let confirmClear = false;
let search = $state("");
let confirmClear = $state(false);
// ── Helpers ───────────────────────────────────────────────────────────────────
function timeAgo(ts: number): string {
const diff = Date.now() - ts, m = Math.floor(diff / 60000);
if (m < 1) return "Just now";
@@ -34,13 +33,18 @@
return r === 0 ? `${h}h` : `${h}h ${r}m`;
}
// ── Session grouping — collapses rapid same-manga reads ───────────────────────
const SESSION_GAP_MS = 30 * 60 * 1000;
interface Session {
mangaId: number; mangaTitle: string; thumbnailUrl: string;
latestChapterId: number; latestChapterName: string; latestPageNumber: number;
firstChapterName: string; chapterCount: number; readAt: number;
mangaId: number;
mangaTitle: string;
thumbnailUrl: string;
latestChapterId: number;
latestChapterName: string;
latestPageNumber: number;
firstChapterName: string;
chapterCount: number;
readAt: number;
}
function buildSessions(entries: HistoryEntry[]): Session[] {
@@ -53,28 +57,37 @@
let j = i + 1;
while (j < entries.length) {
const next = entries[j];
if (next.mangaId === anchor.mangaId && anchor.readAt - next.readAt <= SESSION_GAP_MS) { group.push(next); j++; }
else break;
if (next.mangaId === anchor.mangaId && anchor.readAt - next.readAt <= SESSION_GAP_MS) {
group.push(next); j++;
} else break;
}
const latest = group[0], oldest = group[group.length - 1];
sessions.push({
mangaId: latest.mangaId, mangaTitle: latest.mangaTitle, thumbnailUrl: latest.thumbnailUrl,
latestChapterId: latest.chapterId, latestChapterName: latest.chapterName,
latestPageNumber: latest.pageNumber, firstChapterName: oldest.chapterName,
chapterCount: group.length, readAt: latest.readAt,
mangaId: latest.mangaId,
mangaTitle: latest.mangaTitle,
thumbnailUrl: latest.thumbnailUrl,
latestChapterId: latest.chapterId,
latestChapterName: latest.chapterName,
latestPageNumber: latest.pageNumber,
firstChapterName: oldest.chapterName,
chapterCount: group.length,
readAt: latest.readAt,
});
i = j;
}
return sessions;
}
$: filtered = search.trim()
? $history.filter(e => e.mangaTitle.toLowerCase().includes(search.toLowerCase()) || e.chapterName.toLowerCase().includes(search.toLowerCase()))
: $history;
const filtered = $derived(search.trim()
? store..filter((e) =>
e.mangaTitle.toLowerCase().includes(search.toLowerCase()) ||
e.chapterName.toLowerCase().includes(search.toLowerCase())
)
: store.);
$: sessions = buildSessions(filtered);
const sessions = $derived(buildSessions(filtered));
$: groups = (() => {
const groups = $derived.by(() => {
const map = new Map<string, Session[]>();
for (const s of sessions) {
const l = dayLabel(s.readAt);
@@ -82,12 +95,12 @@
map.get(l)!.push(s);
}
return Array.from(map.entries()).map(([label, items]) => ({ label, items }));
})();
});
function resume(session: Session) {
const ch = $activeChapterList.find(c => c.id === session.latestChapterId);
if (ch && $activeChapterList.length > 0) openReader(ch, $activeChapterList);
else activeManga.set({ id: session.mangaId, title: session.mangaTitle, thumbnailUrl: session.thumbnailUrl } as any);
const ch = store..find((c) => c.id === session.latestChapterId);
if (ch && store..length > 0) openReader(ch, );
else setActiveManga({ id: session.mangaId, title: session.mangaTitle, thumbnailUrl: session.thumbnailUrl } as any);
}
function handleClear() {
@@ -98,18 +111,17 @@
<div class="root">
<!-- ── Header ──────────────────────────────────────────────────────────────── -->
<div class="page-header">
<span class="heading">History</span>
<div class="header-right">
<div class="search-wrap">
<MagnifyingGlass size={12} class="search-icon" weight="light" />
<input class="search" placeholder="Search history…" bind:value={search} />
{#if search}<button class="search-clear" on:click={() => search = ""}>×</button>{/if}
<input class="search" placeholder="Search store.…" bind:value={search} />
{#if search}<button class="search-clear" onclick={() => search = ""}>×</button>{/if}
</div>
{#if $history.length > 0}
<button class="clear-btn" class:confirm={confirmClear} on:click={handleClear}
title={confirmClear ? "Click again to confirm" : "Clear history feed"}>
{#if store..length > 0}
<button class="clear-btn" class:confirm={confirmClear} onclick={handleClear}
title={confirmClear ? "Click again to confirm" : "Clear store. feed"}>
<Trash size={14} weight="light" />
{#if confirmClear}<span class="clear-label">Confirm?</span>{/if}
</button>
@@ -117,46 +129,44 @@
</div>
</div>
<!-- ── Persistent stats bar — never cleared ────────────────────────────────── -->
{#if $readingStats.totalChaptersRead > 0}
{#if store..totalChaptersRead > 0}
<div class="stats-bar">
<div class="stat-group">
<Fire size={13} weight="fill" class="stat-fire" />
<span class="stat-val accent">{$readingStats.currentStreakDays}</span>
<span class="stat-val accent">{store..currentStreakDays}</span>
<span class="stat-label">day streak</span>
</div>
<div class="stat-sep"></div>
<div class="stat-group">
<BookOpen size={13} weight="light" class="stat-icon-neutral" />
<span class="stat-val">{$readingStats.totalChaptersRead}</span>
<span class="stat-val">{store..totalChaptersRead}</span>
<span class="stat-label">chapters</span>
</div>
<div class="stat-sep"></div>
<div class="stat-group">
<Clock size={13} weight="light" class="stat-icon-neutral" />
<span class="stat-val">{formatReadTime($readingStats.totalMinutesRead)}</span>
<span class="stat-val">{formatReadTime(store..totalMinutesRead)}</span>
<span class="stat-label">read time</span>
</div>
<div class="stat-sep"></div>
<div class="stat-group">
<TrendUp size={13} weight="light" class="stat-icon-neutral" />
<span class="stat-val">{$readingStats.totalMangaRead}</span>
<span class="stat-val">{store..totalMangaRead}</span>
<span class="stat-label">series</span>
</div>
<div class="stat-sep"></div>
<div class="stat-group">
<span class="stat-val muted">{$readingStats.longestStreakDays}d</span>
<span class="stat-val muted">{store..longestStreakDays}d</span>
<span class="stat-label">best streak</span>
</div>
<span class="stats-note">Stats are preserved when you clear the feed</span>
</div>
{/if}
<!-- ── Empty states ────────────────────────────────────────────────────────── -->
{#if $history.length === 0}
{#if store..length === 0}
<div class="empty">
<ClockCounterClockwise size={32} weight="light" class="empty-icon" />
<p class="empty-text">No reading history</p>
<p class="empty-text">No reading store.</p>
<p class="empty-hint">Chapters you read will appear here</p>
</div>
{:else if sessions.length === 0}
@@ -164,8 +174,6 @@
<Books size={28} weight="light" class="empty-icon" />
<p class="empty-text">No results for "{search}"</p>
</div>
<!-- ── Timeline ────────────────────────────────────────────────────────────── -->
{:else}
<div class="timeline">
{#each groups as { label, items }}
@@ -176,17 +184,13 @@
</div>
<div class="session-list">
{#each items as session (session.latestChapterId)}
<button class="session-row" on:click={() => resume(session)}>
<!-- Cover -->
<button class="session-row" onclick={() => resume(session)}>
<div class="thumb-wrap">
<img src={thumbUrl(session.thumbnailUrl)} alt={session.mangaTitle} class="thumb" />
{#if session.chapterCount > 1}
<span class="session-count">{session.chapterCount}</span>
{/if}
</div>
<!-- Info -->
<div class="session-info">
<span class="session-title">{session.mangaTitle}</span>
<span class="session-chapter">
@@ -202,13 +206,10 @@
{/if}
</span>
</div>
<!-- Time + play -->
<span class="session-time">{timeAgo(session.readAt)}</span>
<div class="play-pill">
<Play size={10} weight="fill" /> Resume
</div>
</button>
{/each}
</div>
@@ -222,7 +223,6 @@
<style>
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; animation: fadeIn 0.14s ease both; }
/* ── Header ──────────────────────────────────────────────────────────────── */
.page-header {
display: flex; align-items: center; justify-content: space-between;
padding: var(--sp-4) var(--sp-6); border-bottom: 1px solid var(--border-dim); flex-shrink: 0;
@@ -250,7 +250,6 @@
.clear-btn.confirm { color: var(--color-error); background: var(--color-error-bg); border-color: var(--color-error); }
.clear-label { font-size: var(--text-2xs); }
/* ── Stats bar — persisted, never clears ────────────────────────────────── */
.stats-bar {
display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
padding: var(--sp-3) var(--sp-6); border-bottom: 1px solid var(--border-dim);
@@ -266,7 +265,6 @@
.stat-label { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); }
.stats-note { margin-left: auto; font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); opacity: 0.5; letter-spacing: var(--tracking-wide); font-style: italic; }
/* ── Timeline ────────────────────────────────────────────────────────────── */
.timeline { flex: 1; overflow-y: auto; padding: var(--sp-4) var(--sp-6); }
.day-group { margin-bottom: var(--sp-5); }
@@ -285,7 +283,6 @@
.session-row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
.session-row:hover .play-pill { opacity: 1; transform: translateX(0); }
/* Thumb */
.thumb-wrap { position: relative; flex-shrink: 0; }
.thumb { width: 38px; height: 54px; border-radius: var(--radius-sm); object-fit: cover; display: block; background: var(--bg-raised); border: 1px solid var(--border-dim); }
.session-count {
@@ -295,14 +292,12 @@
padding: 1px 4px; border-radius: 6px; line-height: 1.4; pointer-events: none;
}
/* Info */
.session-info { flex: 1; display: flex; flex-direction: column; gap: 3px; overflow: hidden; min-width: 0; }
.session-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.session-chapter { font-size: var(--text-xs); color: var(--text-muted); display: flex; align-items: center; gap: var(--sp-1); min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ch-arrow { color: var(--text-faint); font-size: 10px; flex-shrink: 0; }
.ch-page { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; }
/* Time & play */
.session-time { font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0; white-space: nowrap; }
.play-pill {
display: flex; align-items: center; gap: 4px; flex-shrink: 0;
@@ -313,7 +308,6 @@
transition: opacity var(--t-base), transform var(--t-base);
}
/* ── Empty ───────────────────────────────────────────────────────────────── */
.empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-2); }
:global(.empty-icon) { color: var(--text-faint); }
.empty-text { font-size: var(--text-base); color: var(--text-muted); }
+13 -13
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import { House, Books, MagnifyingGlass, ClockCounterClockwise, Compass, DownloadSimple, PuzzlePiece, GearSix } from "phosphor-svelte";
import { navPage, activeManga, activeSource, libraryFilter, genreFilter, settingsOpen } from "../../store";
import type { NavPage } from "../../store";
import { store, setNavPage, setActiveManga, setActiveSource, setLibraryFilter, setGenreFilter, setSettingsOpen } from "../../store/state.svelte";
import type { NavPage } from "../../store/state.svelte";
const TABS: { id: NavPage; label: string; icon: any }[] = [
{ id: "home", label: "Home", icon: House },
@@ -14,18 +14,18 @@
];
function navigate(id: NavPage) {
navPage = id;
activeManga = null;
genreFilter = "";
if (id !== "explore") activeSource = null;
store.navPage = id;
store.activeManga = null;
store.genreFilter = "";
if (id !== "explore") store.activeSource = null;
}
function goHome() {
navPage = "home";
activeSource = null;
activeManga = null;
libraryFilter = "library";
genreFilter = "";
store.navPage = "home";
store.activeSource = null;
store.activeManga = null;
store.libraryFilter = "library";
store.genreFilter = "";
}
</script>
@@ -35,14 +35,14 @@
</button>
<nav class="nav">
{#each TABS as tab}
<button class="tab" class:active={navPage === tab.id}
<button class="tab" class:active={store.navPage === tab.id}
title={tab.label} onclick={() => navigate(tab.id)}>
<tab.icon size={18} weight="light" />
</button>
{/each}
</nav>
<div class="bottom">
<button class="settings-btn" onclick={() => settingsOpen = true} title="Settings">
<button class="settings-btn" onclick={() => store.settingsOpen = true} title="Settings">
<GearSix size={18} weight="light" />
</button>
</div>
+1 -1
View File
@@ -24,7 +24,7 @@
let exiting = $state(false);
let exitLock = false;
let fpsEl: HTMLSpanElement;
let fpsEl = $state<HTMLSpanElement | undefined>(undefined);
function triggerExit(cb?: () => void) {
if (exitLock) return;
+3 -3
View File
@@ -6,17 +6,17 @@
<div class="bar" data-tauri-drag-region>
<span class="title" data-tauri-drag-region>Moku</span>
<div class="controls">
<button on:click={() => win.minimize()} title="Minimize" aria-label="Minimize">
<button onclick={() => win.minimize()} title="Minimize" aria-label="Minimize">
<svg width="10" height="1" viewBox="0 0 10 1">
<line x1="0" y1="0.5" x2="10" y2="0.5" stroke="currentColor" stroke-width="1.5" />
</svg>
</button>
<button on:click={() => win.toggleMaximize()} title="Maximize" aria-label="Maximize">
<button onclick={() => win.toggleMaximize()} title="Maximize" aria-label="Maximize">
<svg width="9" height="9" viewBox="0 0 9 9">
<rect x="0.75" y="0.75" width="7.5" height="7.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>
</button>
<button class="close" on:click={() => win.close()} title="Close" aria-label="Close">
<button class="close" onclick={() => win.close()} title="Close" aria-label="Close">
<svg width="10" height="10" viewBox="0 0 10 10">
<line x1="1" y1="1" x2="9" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<line x1="9" y1="1" x2="1" y2="9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
+9 -9
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import { onDestroy } from "svelte";
import { toasts, dismissToast } from "../../store";
import type { Toast } from "../../store";
import { store, dismissToast } from "../../store/state.svelte";
import type { Toast } from "../../store/state.svelte";
const timers = new Map<string, ReturnType<typeof setTimeout>>();
@@ -12,9 +11,10 @@
timers.set(t.id, setTimeout(() => dismissToast(t.id), dur));
}
$: $toasts.forEach(schedule);
onDestroy(() => timers.forEach(clearTimeout));
$effect(() => {
store.toasts.forEach(schedule);
return () => timers.forEach(clearTimeout);
});
const icons: Record<Toast["kind"], string> = {
success: "M9 12l2 2 4-4M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2z",
@@ -24,9 +24,9 @@
};
</script>
{#if $toasts.length}
{#if store.toasts.length}
<div class="toaster" aria-live="polite">
{#each $toasts as t (t.id)}
{#each store.toasts as t (t.id)}
<div class="toast toast-{t.kind}" role="alert">
<span class="icon">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
@@ -38,7 +38,7 @@
<p class="title">{t.title}</p>
{#if t.body}<p class="sub">{t.body}</p>{/if}
</div>
<button class="close" on:click={() => dismissToast(t.id)} title="Dismiss">
<button class="close" onclick={() => dismissToast(t.id)} title="Dismiss">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M18 6L6 18M6 6l12 12" />