From 31a19687ce9b9610b465e030c9d3aced91492ba7 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Fri, 12 Jun 2026 04:12:33 -0500 Subject: [PATCH] Fix: Browse Bug Fixes & Enhancements --- package.json | 3 +- pnpm-lock.yaml | 62 ++++++++++++------- .../components/browse/GenreDrillPage.svelte | 41 +++++++----- src/lib/components/browse/KeywordTab.svelte | 18 +++--- src/lib/components/browse/Search.svelte | 36 ++++++----- src/lib/components/browse/SourceTab.svelte | 8 +-- src/lib/components/browse/TagTab.svelte | 48 ++++++++------ .../shared/manga/MangaPreview.svelte | 57 ++++++++++------- 8 files changed, 166 insertions(+), 107 deletions(-) diff --git a/package.json b/package.json index 004fd5b..bec343a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@sveltejs/kit": "^2.62.0", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tauri-apps/cli": "^2.11.2", + "@types/node": "^25.9.3", + "phosphor-svelte": "^3.1.0", "svelte": "^5.56.1", "svelte-check": "^4.5.0", "typescript": "^6.0.3", @@ -42,7 +44,6 @@ "@tauri-apps/plugin-store": "^2.4.3", "capacitor-native-biometric": "^4.2.2", "clsx": "^2.1.1", - "phosphor-svelte": "^3.1.0", "tauri-plugin-discord-rpc-api": "github:Youwes09/tauri-plugin-discord-rpc" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a23e29..c309f43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,28 +53,31 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - phosphor-svelte: - specifier: ^3.1.0 - version: 3.1.0(svelte@5.56.1)(vite@8.0.16) tauri-plugin-discord-rpc-api: specifier: github:Youwes09/tauri-plugin-discord-rpc version: https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c devDependencies: '@sveltejs/adapter-node': specifier: ^5.5.4 - version: 5.5.4(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16)) + version: 5.5.4(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3))) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16)) + version: 3.0.10(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3))) '@sveltejs/kit': specifier: ^2.62.0 - version: 2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16) + version: 2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)) '@sveltejs/vite-plugin-svelte': specifier: ^7.1.2 - version: 7.1.2(svelte@5.56.1)(vite@8.0.16) + version: 7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)) '@tauri-apps/cli': specifier: ^2.11.2 version: 2.11.2 + '@types/node': + specifier: ^25.9.3 + version: 25.9.3 + phosphor-svelte: + specifier: ^3.1.0 + version: 3.1.0(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)) svelte: specifier: ^5.56.1 version: 5.56.1 @@ -86,7 +89,7 @@ importers: version: 6.0.3 vite: specifier: ^8.0.16 - version: 8.0.16 + version: 8.0.16(@types/node@25.9.3) packages: @@ -578,6 +581,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@25.9.3': + resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -874,6 +880,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + vite@8.0.16: resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1171,23 +1180,23 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16))': + '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)))': dependencies: '@rollup/plugin-commonjs': 29.0.3(rollup@4.61.0) '@rollup/plugin-json': 6.1.0(rollup@4.61.0) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.61.0) - '@sveltejs/kit': 2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16) + '@sveltejs/kit': 2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)) rollup: 4.61.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)))': dependencies: - '@sveltejs/kit': 2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16) + '@sveltejs/kit': 2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3)) - '@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16)': + '@sveltejs/kit@2.62.0(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)))(svelte@5.56.1)(typescript@6.0.3)(vite@8.0.16(@types/node@25.9.3))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 7.1.2(svelte@5.56.1)(vite@8.0.16) + '@sveltejs/vite-plugin-svelte': 7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -1199,18 +1208,18 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.56.1 - vite: 8.0.16 + vite: 8.0.16(@types/node@25.9.3) optionalDependencies: typescript: 6.0.3 - '@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16)': + '@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 svelte: 5.56.1 - vite: 8.0.16 - vitefu: 1.1.3(vite@8.0.16) + vite: 8.0.16(@types/node@25.9.3) + vitefu: 1.1.3(vite@8.0.16(@types/node@25.9.3)) '@tauri-apps/api@2.11.0': {} @@ -1298,6 +1307,10 @@ snapshots: '@types/estree@1.0.9': {} + '@types/node@25.9.3': + dependencies: + undici-types: 7.24.6 + '@types/resolve@1.20.2': {} '@types/trusted-types@2.0.7': {} @@ -1436,13 +1449,13 @@ snapshots: path-parse@1.0.7: {} - phosphor-svelte@3.1.0(svelte@5.56.1)(vite@8.0.16): + phosphor-svelte@3.1.0(svelte@5.56.1)(vite@8.0.16(@types/node@25.9.3)): dependencies: estree-walker: 3.0.3 magic-string: 0.30.21 svelte: 5.56.1 optionalDependencies: - vite: 8.0.16 + vite: 8.0.16(@types/node@25.9.3) picocolors@1.1.1: {} @@ -1579,7 +1592,9 @@ snapshots: typescript@6.0.3: {} - vite@8.0.16: + undici-types@7.24.6: {} + + vite@8.0.16(@types/node@25.9.3): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -1587,10 +1602,11 @@ snapshots: rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: + '@types/node': 25.9.3 fsevents: 2.3.3 - vitefu@1.1.3(vite@8.0.16): + vitefu@1.1.3(vite@8.0.16(@types/node@25.9.3)): optionalDependencies: - vite: 8.0.16 + vite: 8.0.16(@types/node@25.9.3) zimmerframe@1.1.4: {} diff --git a/src/lib/components/browse/GenreDrillPage.svelte b/src/lib/components/browse/GenreDrillPage.svelte index 2afe8b4..7e6c9b0 100644 --- a/src/lib/components/browse/GenreDrillPage.svelte +++ b/src/lib/components/browse/GenreDrillPage.svelte @@ -6,14 +6,25 @@ import { dedupeMangaById, shouldHideNsfw } from "$lib/core/util"; import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte"; import ContextMenu from "$lib/components/shared/ui/ContextMenu.svelte"; - import { ArrowLeft, BookmarkSimple, FolderSimplePlus, Folder, CircleNotch } from "phosphor-svelte"; + import { ArrowLeftIcon, BookmarkSimpleIcon, FolderSimplePlusIcon, FolderIcon, CircleNotchIcon } from "phosphor-svelte"; import type { Manga, Source, Category } from "$lib/types"; - import type { MenuEntry } from "$lib/components/shared/ui/ContextMenu.svelte"; import { PAGE_SIZE, INITIAL_PAGES, MAX_SOURCES, parseTags, tagsLabel, matchesAllTags, runConcurrent, } from "$lib/components/browse/lib/searchFilter"; + interface MenuItem { + label: string; + icon?: any; + onClick: () => void; + danger?: boolean; + disabled?: boolean; + separator?: never; + children?: MenuEntry[]; + } + interface MenuSeparator { separator: true } + type MenuEntry = MenuItem | MenuSeparator; + interface Props { genre: string; onBack: () => void; @@ -63,17 +74,17 @@ const t = parseTags(filter); const pt = t[0] ?? ""; - getAdapter().getMangaList({}).then((result) => { + getAdapter().getMangaList({}).then((result: { items: Manga[] }) => { if (!ctrl.signal.aborted) libraryManga = result.items; }).catch(() => {}); - getAdapter().getSources().then(async (allSources) => { + getAdapter().getSources().then(async (allSources: Source[]) => { if (ctrl.signal.aborted) return; const srcs = allSources.filter((s: Source) => s.id !== "0").slice(0, MAX_SOURCES); sources = srcs; for (const src of srcs) nextPageMap.set(src.id, -1); - await runConcurrent(srcs, async (src) => { + await runConcurrent(srcs, async (src: Source) => { if (ctrl.signal.aborted) return; const pageItems: Manga[] = []; for (let page = 1; page <= INITIAL_PAGES; page++) { @@ -108,7 +119,7 @@ const ctrl = new AbortController(); abortCtrl = ctrl; try { - await runConcurrent(srcs, async (src) => { + await runConcurrent(srcs, async (src: Source) => { const page = nextPageMap.get(src.id)!; if (ctrl.signal.aborted) return; let result: { items: Manga[]; hasNextPage: boolean } | null = null; @@ -131,7 +142,7 @@ if (!catsLoaded) { catsLoaded = true; getAdapter().getCategories() - .then((cats) => { categories = cats.filter((c) => c.id !== 0); }) + .then((cats: Category[]) => { categories = cats.filter((c: Category) => c.id !== 0); }) .catch(console.error); } } @@ -140,7 +151,7 @@ return [ { label: m.inLibrary ? "In Library" : "Add to library", - icon: BookmarkSimple, + icon: BookmarkSimpleIcon, disabled: m.inLibrary, onClick: () => getAdapter().addToLibrary(String(m.id)) .then(() => { sourceManga = sourceManga.map((x) => x.id === m.id ? { ...x, inLibrary: true } : x); }) @@ -149,17 +160,17 @@ ...(categories.length > 0 ? [ { separator: true } as MenuEntry, ...categories.map((cat): MenuEntry => ({ - label: (cat.mangas?.nodes ?? []).some((x: { id: number }) => x.id === m.id) ? `✓ ${cat.name}` : cat.name, - icon: Folder, + label: (cat.mangas ?? []).some((x: Manga) => x.id === m.id) ? `✓ ${cat.name}` : cat.name, + icon: FolderIcon, onClick: () => getAdapter().updateMangaCategories(String(m.id), [cat.id], []).catch(console.error), })), ] : []), { separator: true }, { label: "New folder & add", - icon: FolderSimplePlus, + icon: FolderSimplePlusIcon, onClick: async () => { - const name = prompt("Folder name:"); + const name = prompt("FolderIcon name:"); if (!name?.trim()) return; const cat = await getAdapter().createCategory(name.trim()).catch(console.error); if (cat) { @@ -177,7 +188,7 @@
{label} {#if !loadingInitial || filtered.length > 0} @@ -213,7 +224,7 @@ {#if hasMore}
{/if} @@ -239,7 +250,7 @@ .cover-wrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); transform: translateZ(0); } :global(.cover) { width: 100%; height: 100%; object-fit: cover; transition: filter var(--t-base); will-change: filter; } .in-library-badge { position: absolute; bottom: var(--sp-1); left: var(--sp-1); font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); text-transform: uppercase; background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); padding: 2px 5px; border-radius: var(--radius-sm); } - .card-title { margin-top: var(--sp-2); font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: color var(--t-base); } + .card-title { margin-top: var(--sp-2); font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: color var(--t-base); } .card-skeleton { padding: 0; } .cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); } .title-skeleton { height: 11px; margin-top: var(--sp-2); width: 75%; } diff --git a/src/lib/components/browse/KeywordTab.svelte b/src/lib/components/browse/KeywordTab.svelte index b5678a9..0936572 100644 --- a/src/lib/components/browse/KeywordTab.svelte +++ b/src/lib/components/browse/KeywordTab.svelte @@ -37,7 +37,7 @@ let kw_inputEl: HTMLInputElement | null = $state(null); let kw_abortCtrl: AbortController | null = null; let kw_debounceTimer: ReturnType | null = null; - let kw_localQuery = $state(query); + let kw_localQuery = $state(""); let kw_pending = $state(false); interface SourceResult { @@ -77,19 +77,19 @@ }, 2000); } - function kwGetVisibleSources(): Source[] { + const kw_visibleSources = $derived.by(() => { let srcs = allSources; if (kw_selectedLangs.size > 0) srcs = srcs.filter((s) => kw_selectedLangs.has(s.lang)); if (settingsState.settings.contentLevel !== "unrestricted") srcs = srcs.filter((s) => !shouldHideSource(s, settingsState.settings)); return srcs; - } + }); async function kwDoSearch(q: string) { const trimmed = q.trim(); if (!trimmed) return; - const visible = kwGetVisibleSources(); + const visible = kw_visibleSources; if (!visible.length) return; kw_abortCtrl?.abort(); @@ -102,13 +102,13 @@ await Promise.allSettled(visible.map(async (src) => { const idx = idxOf.get(src.id)!; try { - const result = await getAdapter().searchSource(src.id, trimmed, 1, ctrl.signal); + const result: { items: Manga[]; hasNextPage: boolean } = await getAdapter().searchSource(src.id, trimmed, 1, ctrl.signal); if (ctrl.signal.aborted) return; const mangas = result.items.filter((m) => !shouldHideNsfw(m as any, settingsState.settings)); - kw_results = kw_results.map((r, i) => i === idx ? { ...r, mangas, loading: false } : r); + kw_results[idx] = { ...kw_results[idx], mangas, loading: false }; } catch (e: any) { if (ctrl.signal.aborted || e?.name === "AbortError") return; - kw_results = kw_results.map((r, i) => i === idx ? { ...r, loading: false, error: e.message ?? "Error" } : r); + kw_results[idx] = { ...kw_results[idx], loading: false, error: e.message ?? "Error" }; } })); } @@ -120,7 +120,7 @@ kw_selectedLangs = next; } - const kw_visibleCount = $derived(kwGetVisibleSources().length); + const kw_visibleCount = $derived(kw_visibleSources.length); const kw_anyLoading = $derived(kw_results.some((r) => r.loading)); const kw_allDone = $derived(kw_results.length > 0 && kw_results.every((r) => !r.loading)); const kw_hasResults = $derived(kw_results.some((r) => r.mangas.length > 0)); @@ -315,7 +315,7 @@ .srchCoverWrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); transform: translateZ(0); transition: filter var(--t-base); } .srchGradient { position: absolute; inset: 0; z-index: 1; background: linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.15) 50%, transparent 72%); pointer-events: none; } .srchFooter { position: absolute; bottom: 0; left: 0; right: 0; z-index: 2; padding: var(--sp-2); pointer-events: none; } - .srchTitle { font-size: var(--text-xs); font-weight: var(--weight-medium); color: rgba(255,255,255,0.92); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 1px 4px rgba(0,0,0,0.7); } + .srchTitle { font-size: var(--text-xs); font-weight: var(--weight-medium); color: rgba(255,255,255,0.92); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 1px 4px rgba(0,0,0,0.7); } .srchSource { font-family: var(--font-ui); font-size: 9px; color: rgba(255,255,255,0.45); letter-spacing: var(--tracking-wide); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .inLibBadge { position: absolute; top: var(--sp-2); left: var(--sp-2); z-index: 2; font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 1px 5px; } .skCard { display: flex; flex-direction: column; gap: var(--sp-2); flex-shrink: 0; width: 100%; } diff --git a/src/lib/components/browse/Search.svelte b/src/lib/components/browse/Search.svelte index 4788ebc..fe7e3f4 100644 --- a/src/lib/components/browse/Search.svelte +++ b/src/lib/components/browse/Search.svelte @@ -5,7 +5,6 @@ import { getAdapter } from "$lib/request-manager"; import { settingsState } from "$lib/state/settings.svelte"; import { setPreviewManga } from "$lib/state/series.svelte"; - import { dedupeMangaById } from "$lib/core/util"; import { toCachedManga, shouldHideNsfw, runConcurrent, type CachedManga } from "$lib/components/browse/lib/searchFilter"; import type { Manga, Source } from "$lib/types"; @@ -66,22 +65,31 @@ const availableLangs = $derived(Array.from(new Set(allSources.map((s) => s.lang))).sort()); const hasMultipleLangs = $derived(availableLangs.length > 1); - loadingSources = true; - getAdapter().getSources() - .then((nodes) => { - localSource = nodes.find((s: Source) => s.id === "0") ?? null; - allSources = nodes.filter((s: Source) => s.id !== "0"); - startSourceCacheBuild(); - popularStart(allSources); - }) - .catch(console.error) - .finally(() => { loadingSources = false; }); + let sourcesAbort: AbortController | null = null; + + $effect(() => { + sourcesAbort?.abort(); + const ctrl = new AbortController(); + sourcesAbort = ctrl; + loadingSources = true; + getAdapter().getSources() + .then((nodes: Source[]) => { + if (ctrl.signal.aborted) return; + localSource = nodes.find((s: Source) => s.id === "0") ?? null; + allSources = nodes.filter((s: Source) => s.id !== "0"); + startSourceCacheBuild(); + popularStart(allSources); + }) + .catch(console.error) + .finally(() => { if (!ctrl.signal.aborted) loadingSources = false; }); + return () => { ctrl.abort(); }; + }); let popular_raw: Manga[] = $state([]); let popular_loading = $state(false); let popular_abortCtrl: AbortController | null = null; - let popular_sourcePool: Source[] = $state([]); - let popular_sourceCursor = $state(0); + let popular_sourcePool: Source[] = []; + let popular_sourceCursor = 0; let popular_seenIds = new Set(); let popular_seenTitles = new Set(); @@ -210,6 +218,7 @@ } onDestroy(() => { + sourcesAbort?.abort(); popular_abortCtrl?.abort(); sourceCacheAbort?.abort(); }); @@ -267,7 +276,6 @@ {sourceCacheLoading} {sourceCacheEnriching} onPreview={(m) => setPreviewManga(m)} - onGenreDrill={(genre) => goto(`/browse?genre=${encodeURIComponent(genre)}`)} /> {:else} import { onDestroy } from "svelte"; import { getAdapter } from "$lib/request-manager"; - import { settingsState } from "$lib/state/settings.svelte"; + import { settingsState, updateSettings } from "$lib/state/settings.svelte"; import { shouldHideNsfw, shouldHideSource } from "$lib/core/util"; import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte"; import ContextMenu from "$lib/components/shared/ui/ContextMenu.svelte"; @@ -20,7 +20,7 @@ const preferredLang = $derived(settingsState.settings.preferredExtensionLang ?? "en"); - let src_selectedLang = $state(preferredLang || "all"); + let src_selectedLang = $state(settingsState.settings.preferredExtensionLang || "all"); let src_activeSource: Source | null = $state(null); let src_browseResults: Manga[] = $state([]); let src_loadingBrowse = $state(false); @@ -122,7 +122,7 @@ function togglePinnedSource(id: string) { const current = settingsState.settings.pinnedSourceIds ?? []; const next = current.includes(id) ? current.filter((x: string) => x !== id) : [...current, id]; - settingsState.updateSettings({ pinnedSourceIds: next }); + updateSettings({ pinnedSourceIds: next }); } onDestroy(() => { src_abortCtrl?.abort(); }); @@ -356,7 +356,7 @@ .tagGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: var(--sp-4); padding: var(--sp-4); overflow-y: auto; flex: 1; align-content: start; } .card { background: none; border: none; padding: 0; cursor: pointer; text-align: left; display: flex; flex-direction: column; gap: var(--sp-2); } .coverWrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); } - .cardTitle { font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } + .cardTitle { font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .inLibBadge { position: absolute; top: var(--sp-2); left: var(--sp-2); font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 1px 5px; } .showMoreCell { grid-column: 1 / -1; display: flex; justify-content: center; padding: var(--sp-2) 0; } .showMoreBtn { display: inline-flex; align-items: center; gap: var(--sp-1); padding: 5px 12px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: none; font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); color: var(--text-muted); cursor: pointer; transition: background var(--t-base), color var(--t-base), border-color var(--t-base); } diff --git a/src/lib/components/browse/TagTab.svelte b/src/lib/components/browse/TagTab.svelte index 6ef0b1b..3fc32c7 100644 --- a/src/lib/components/browse/TagTab.svelte +++ b/src/lib/components/browse/TagTab.svelte @@ -41,7 +41,8 @@ let tag_loadingMoreLocal = $state(false); let tag_localOffset = $state(0); let tag_localHasNext = $state(false); - let tag_abortLocal: AbortController | null = null; + let tag_abortLocal: AbortController | null = null; + let tag_abortLoadMore: AbortController | null = null; const renderLimit = $derived(settingsState.settings.renderLimit ?? 48); @@ -52,14 +53,6 @@ untrack(() => tagFetchLocal(_tags, _mode, _statuses)); }); - $effect(() => { - const _hasNext = tag_localHasNext; - const _loadingMore = tag_loadingMoreLocal; - const _loadingLocal = tag_loadingLocal; - untrack(() => { - if (_hasNext && !_loadingMore && !_loadingLocal) tagLoadMoreLocal(); - }); - }); async function tagFetchLocal(activeTags: string[], tagMode: TagMode, activeStatuses: string[]) { if (activeTags.length === 0 && activeStatuses.length === 0) { @@ -67,6 +60,7 @@ return; } tag_abortLocal?.abort(); + tag_abortLoadMore?.abort(); const ctrl = new AbortController(); tag_abortLocal = ctrl; tag_localResults = []; tag_totalCount = 0; tag_localOffset = 0; tag_localHasNext = false; @@ -82,6 +76,7 @@ tag_totalCount = d.totalCount; tag_localHasNext = d.hasNextPage; tag_localOffset = limit; + if (d.hasNextPage && tag_localResults.length < 20) tagLoadMoreLocal(); } catch (e: any) { if (e?.name !== "AbortError") console.error(e); } finally { @@ -91,10 +86,10 @@ async function tagLoadMoreLocal() { if (tag_loadingMoreLocal || !tag_localHasNext) return; - tag_loadingMoreLocal = true; - tag_abortLocal?.abort(); + tag_abortLoadMore?.abort(); const ctrl = new AbortController(); - tag_abortLocal = ctrl; + tag_abortLoadMore = ctrl; + tag_loadingMoreLocal = true; const limit = renderLimit; try { const d = await getAdapter().getMangasByGenre( @@ -196,17 +191,22 @@ let tag_autoSearchFired = $state(false); $effect(() => { - const _tags = tag_activeTags; - const _statuses = tag_activeStatuses; + void tag_activeTags; + void tag_activeStatuses; + untrack(() => { tag_autoSearchFired = false; }); + }); + $effect(() => { const _loadingLocal = tag_loadingLocal; const _hasFilters = tag_hasActiveFilters; const _resultLen = tag_localResults.length; const _cacheReady = sourceCacheReady; - untrack(() => { tag_autoSearchFired = false; }); - if (!_loadingLocal && _hasFilters && !tag_autoSearchFired && !tag_searchSources && _cacheReady) { - if (_resultLen < 20) { - untrack(() => { tag_autoSearchFired = true; tag_searchSources = true; }); - } + if (!_loadingLocal && _hasFilters && _cacheReady) { + untrack(() => { + if (!tag_autoSearchFired && !tag_searchSources && _resultLen < 20) { + tag_autoSearchFired = true; + tag_searchSources = true; + } + }); } }); @@ -239,6 +239,7 @@ onDestroy(() => { tag_abortLocal?.abort(); + tag_abortLoadMore?.abort(); tag_fanOutAbort?.abort(); }); @@ -379,6 +380,10 @@ {#each Array(12) as _, i (i)}
{/each} + {:else if tag_localHasNext} +
+ +
{/if}
{:else} @@ -435,9 +440,12 @@ .tagClearAll:hover { color: var(--color-error); border-color: color-mix(in srgb, var(--color-error) 40%, transparent); background: var(--color-error-bg, color-mix(in srgb, var(--color-error) 8%, transparent)); } .tagCheckMark { font-size: var(--text-xs); color: var(--accent-fg); margin-left: auto; } .tagGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: var(--sp-4); padding: var(--sp-4); overflow-y: auto; flex: 1; align-content: start; will-change: scroll-position; } + .loadMoreRow { grid-column: 1 / -1; display: flex; justify-content: center; padding: var(--sp-2) 0 var(--sp-4); } + .loadMoreBtn { font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide); color: var(--text-faint); background: none; border: 1px solid var(--border-dim); border-radius: var(--radius-md); padding: 6px 20px; cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); } + .loadMoreBtn:hover { color: var(--text-muted); border-color: var(--border-strong); background: var(--bg-raised); } .card { background: none; border: none; padding: 0; cursor: pointer; text-align: left; display: flex; flex-direction: column; gap: var(--sp-2); } .coverWrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); } - .cardTitle { font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: color var(--t-base); } + .cardTitle { font-size: var(--text-sm); color: var(--text-secondary); line-height: var(--leading-snug); display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; transition: color var(--t-base); } .inLibBadge { position: absolute; top: var(--sp-2); left: var(--sp-2); font-family: var(--font-ui); font-size: 9px; letter-spacing: var(--tracking-wide); background: var(--accent-muted); color: var(--accent-fg); border: 1px solid var(--accent-dim); border-radius: var(--radius-sm); padding: 1px 5px; } .skCard { display: flex; flex-direction: column; gap: var(--sp-2); } @keyframes shimmer { from { background-position: -200% 0 } to { background-position: 200% 0 } } diff --git a/src/lib/components/shared/manga/MangaPreview.svelte b/src/lib/components/shared/manga/MangaPreview.svelte index 4f2fc7b..90b73c7 100644 --- a/src/lib/components/shared/manga/MangaPreview.svelte +++ b/src/lib/components/shared/manga/MangaPreview.svelte @@ -59,7 +59,7 @@ const unreadCount = $derived(totalCount - readCount); const downloadedCount = $derived(chapters.filter((c) => c.downloaded).length); const bookmarkCount = $derived(chapters.filter((c) => c.bookmarked).length); - const inLibrary = $derived(manga?.inLibrary ?? seriesState.previewManga?.inLibrary ?? false); + const inLibrary = $derived((manga as Manga | null)?.inLibrary ?? seriesState.previewManga?.inLibrary ?? false); const scanlators = $derived( [...new Set(chapters.map((c) => c.scanlator).filter((s): s is string => !!s?.trim()))], ); @@ -98,7 +98,7 @@ const inProgress = asc.find((c) => !c.read && (c.lastPageRead ?? 0) > 0); if (inProgress) return { ch: inProgress, type: "continue" as const, resumePage: inProgress.lastPageRead! }; const firstUnread = asc.find((c) => !c.read); - if (firstUnread) return { ch: firstUnread, type: (anyRead ? "continue" : "start") as const, resumePage: null }; + if (firstUnread) return { ch: firstUnread, type: (anyRead ? "continue" as const : "start" as const), resumePage: null }; return { ch: asc[0], type: "reread" as const, resumePage: null }; }); @@ -127,10 +127,14 @@ linkPickerOpen = true; if (allMangaForLink.length) return; loadingLinkList = true; - getAdapter().getMangaList({}) - .then((d) => { allMangaForLink = d.items; }) - .catch(console.error) - .finally(() => { loadingLinkList = false; }); + try { + const result = await getAdapter().getMangaList({}); + allMangaForLink = result.items; + } catch (e) { + console.error(e); + } finally { + loadingLinkList = false; + } } function closeLinkPicker() { linkPickerOpen = false; } @@ -139,10 +143,14 @@ coverPickerOpen = true; if (allMangaForLink.length) return; loadingLinkList = true; - getAdapter().getMangaList({}) - .then((d) => { allMangaForLink = d.items; }) - .catch(console.error) - .finally(() => { loadingLinkList = false; }); + try { + const result = await getAdapter().getMangaList({}); + allMangaForLink = result.items; + } catch (e) { + console.error(e); + } finally { + loadingLinkList = false; + } } $effect(() => { @@ -158,14 +166,21 @@ if (shouldAutoLink) { if (allMangaForLink.length) { autoLinkLibrary(focal, allMangaForLink) - .then(n => { if (n > 0) addToast({ kind: "success", title: "Series linked", body: `${n} new link${n === 1 ? "" : "s"} found` }); }); + .then((n: number) => { if (n > 0) addToast({ kind: "success", title: "Series linked", body: `${n} new link${n === 1 ? "" : "s"} found` }); }); } else { loadingLinkList = true; - getAdapter().getMangaList({}) - .then((d) => { allMangaForLink = d.items; return autoLinkLibrary(focal, d.items); }) - .then(n => { if (n > 0) addToast({ kind: "success", title: "Series linked", body: `${n} new link${n === 1 ? "" : "s"} found` }); }) - .catch(console.error) - .finally(() => { loadingLinkList = false; }); + (async () => { + try { + const result = await getAdapter().getMangaList({}); + allMangaForLink = result.items; + const n = await autoLinkLibrary(focal, result.items); + if (n > 0) addToast({ kind: "success", title: "Series linked", body: `${n} new link${n === 1 ? "" : "s"} found` }); + } catch (e) { + console.error(e); + } finally { + loadingLinkList = false; + } + })(); } } }); @@ -258,9 +273,9 @@ function loadCategories(id: number) { catsLoading = true; getAdapter().getCategories() - .then((cats) => { - allCategories = cats.filter((c) => c.id !== 0); - mangaCategories = allCategories.filter((c) => c.mangas?.nodes?.some((m) => m.id === id)); + .then((cats: Category[]) => { + allCategories = cats.filter((c: Category) => c.id !== 0); + mangaCategories = allCategories.filter((c: Category) => c.mangas?.some((m: Manga) => m.id === id)); }) .catch(console.error) .finally(() => { catsLoading = false; }); @@ -836,8 +851,8 @@ .read-btn:hover { filter: brightness(1.1); } .desc-block { display: flex; flex-direction: column; gap: var(--sp-2); border-top: 1px solid var(--border-dim); padding-top: var(--sp-3); } - .desc { font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-base); display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; } - .desc.desc-open { display: block; -webkit-line-clamp: unset; overflow: visible; } + .desc { font-size: var(--text-sm); color: var(--text-muted); line-height: var(--leading-base); display: -webkit-box; -webkit-line-clamp: 5; line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; } + .desc.desc-open { display: block; -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; } .desc-toggle { display: flex; align-items: center; gap: var(--sp-1); align-self: flex-start; font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint);