Fix: Browse Bug Fixes & Enhancements

This commit is contained in:
Youwes09
2026-06-12 04:12:33 -05:00
parent 437b52fd8b
commit 31a19687ce
8 changed files with 166 additions and 107 deletions
+2 -1
View File
@@ -21,6 +21,8 @@
"@sveltejs/kit": "^2.62.0", "@sveltejs/kit": "^2.62.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2", "@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2.11.2", "@tauri-apps/cli": "^2.11.2",
"@types/node": "^25.9.3",
"phosphor-svelte": "^3.1.0",
"svelte": "^5.56.1", "svelte": "^5.56.1",
"svelte-check": "^4.5.0", "svelte-check": "^4.5.0",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -42,7 +44,6 @@
"@tauri-apps/plugin-store": "^2.4.3", "@tauri-apps/plugin-store": "^2.4.3",
"capacitor-native-biometric": "^4.2.2", "capacitor-native-biometric": "^4.2.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"phosphor-svelte": "^3.1.0",
"tauri-plugin-discord-rpc-api": "github:Youwes09/tauri-plugin-discord-rpc" "tauri-plugin-discord-rpc-api": "github:Youwes09/tauri-plugin-discord-rpc"
} }
} }
+39 -23
View File
@@ -53,28 +53,31 @@ importers:
clsx: clsx:
specifier: ^2.1.1 specifier: ^2.1.1
version: 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: tauri-plugin-discord-rpc-api:
specifier: github:Youwes09/tauri-plugin-discord-rpc specifier: github:Youwes09/tauri-plugin-discord-rpc
version: https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c version: https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c
devDependencies: devDependencies:
'@sveltejs/adapter-node': '@sveltejs/adapter-node':
specifier: ^5.5.4 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': '@sveltejs/adapter-static':
specifier: ^3.0.10 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': '@sveltejs/kit':
specifier: ^2.62.0 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': '@sveltejs/vite-plugin-svelte':
specifier: ^7.1.2 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': '@tauri-apps/cli':
specifier: ^2.11.2 specifier: ^2.11.2
version: 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: svelte:
specifier: ^5.56.1 specifier: ^5.56.1
version: 5.56.1 version: 5.56.1
@@ -86,7 +89,7 @@ importers:
version: 6.0.3 version: 6.0.3
vite: vite:
specifier: ^8.0.16 specifier: ^8.0.16
version: 8.0.16 version: 8.0.16(@types/node@25.9.3)
packages: packages:
@@ -578,6 +581,9 @@ packages:
'@types/estree@1.0.9': '@types/estree@1.0.9':
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
'@types/node@25.9.3':
resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -874,6 +880,9 @@ packages:
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
vite@8.0.16: vite@8.0.16:
resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==} resolution: {integrity: sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -1171,23 +1180,23 @@ snapshots:
dependencies: dependencies:
acorn: 8.16.0 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: dependencies:
'@rollup/plugin-commonjs': 29.0.3(rollup@4.61.0) '@rollup/plugin-commonjs': 29.0.3(rollup@4.61.0)
'@rollup/plugin-json': 6.1.0(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) '@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 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: 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: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
'@sveltejs/acorn-typescript': 1.0.10(acorn@8.16.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 '@types/cookie': 0.6.0
acorn: 8.16.0 acorn: 8.16.0
cookie: 0.6.0 cookie: 0.6.0
@@ -1199,18 +1208,18 @@ snapshots:
set-cookie-parser: 3.1.0 set-cookie-parser: 3.1.0
sirv: 3.0.2 sirv: 3.0.2
svelte: 5.56.1 svelte: 5.56.1
vite: 8.0.16 vite: 8.0.16(@types/node@25.9.3)
optionalDependencies: optionalDependencies:
typescript: 6.0.3 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: dependencies:
deepmerge: 4.3.1 deepmerge: 4.3.1
magic-string: 0.30.21 magic-string: 0.30.21
obug: 2.1.1 obug: 2.1.1
svelte: 5.56.1 svelte: 5.56.1
vite: 8.0.16 vite: 8.0.16(@types/node@25.9.3)
vitefu: 1.1.3(vite@8.0.16) vitefu: 1.1.3(vite@8.0.16(@types/node@25.9.3))
'@tauri-apps/api@2.11.0': {} '@tauri-apps/api@2.11.0': {}
@@ -1298,6 +1307,10 @@ snapshots:
'@types/estree@1.0.9': {} '@types/estree@1.0.9': {}
'@types/node@25.9.3':
dependencies:
undici-types: 7.24.6
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
@@ -1436,13 +1449,13 @@ snapshots:
path-parse@1.0.7: {} 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: dependencies:
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
svelte: 5.56.1 svelte: 5.56.1
optionalDependencies: optionalDependencies:
vite: 8.0.16 vite: 8.0.16(@types/node@25.9.3)
picocolors@1.1.1: {} picocolors@1.1.1: {}
@@ -1579,7 +1592,9 @@ snapshots:
typescript@6.0.3: {} typescript@6.0.3: {}
vite@8.0.16: undici-types@7.24.6: {}
vite@8.0.16(@types/node@25.9.3):
dependencies: dependencies:
lightningcss: 1.32.0 lightningcss: 1.32.0
picomatch: 4.0.4 picomatch: 4.0.4
@@ -1587,10 +1602,11 @@ snapshots:
rolldown: 1.0.3 rolldown: 1.0.3
tinyglobby: 0.2.17 tinyglobby: 0.2.17
optionalDependencies: optionalDependencies:
'@types/node': 25.9.3
fsevents: 2.3.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: optionalDependencies:
vite: 8.0.16 vite: 8.0.16(@types/node@25.9.3)
zimmerframe@1.1.4: {} zimmerframe@1.1.4: {}
+26 -15
View File
@@ -6,14 +6,25 @@
import { dedupeMangaById, shouldHideNsfw } from "$lib/core/util"; import { dedupeMangaById, shouldHideNsfw } from "$lib/core/util";
import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte"; import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte";
import ContextMenu from "$lib/components/shared/ui/ContextMenu.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 { Manga, Source, Category } from "$lib/types";
import type { MenuEntry } from "$lib/components/shared/ui/ContextMenu.svelte";
import { import {
PAGE_SIZE, INITIAL_PAGES, MAX_SOURCES, PAGE_SIZE, INITIAL_PAGES, MAX_SOURCES,
parseTags, tagsLabel, matchesAllTags, runConcurrent, parseTags, tagsLabel, matchesAllTags, runConcurrent,
} from "$lib/components/browse/lib/searchFilter"; } 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 { interface Props {
genre: string; genre: string;
onBack: () => void; onBack: () => void;
@@ -63,17 +74,17 @@
const t = parseTags(filter); const t = parseTags(filter);
const pt = t[0] ?? ""; const pt = t[0] ?? "";
getAdapter().getMangaList({}).then((result) => { getAdapter().getMangaList({}).then((result: { items: Manga[] }) => {
if (!ctrl.signal.aborted) libraryManga = result.items; if (!ctrl.signal.aborted) libraryManga = result.items;
}).catch(() => {}); }).catch(() => {});
getAdapter().getSources().then(async (allSources) => { getAdapter().getSources().then(async (allSources: Source[]) => {
if (ctrl.signal.aborted) return; if (ctrl.signal.aborted) return;
const srcs = allSources.filter((s: Source) => s.id !== "0").slice(0, MAX_SOURCES); const srcs = allSources.filter((s: Source) => s.id !== "0").slice(0, MAX_SOURCES);
sources = srcs; sources = srcs;
for (const src of srcs) nextPageMap.set(src.id, -1); 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; if (ctrl.signal.aborted) return;
const pageItems: Manga[] = []; const pageItems: Manga[] = [];
for (let page = 1; page <= INITIAL_PAGES; page++) { for (let page = 1; page <= INITIAL_PAGES; page++) {
@@ -108,7 +119,7 @@
const ctrl = new AbortController(); const ctrl = new AbortController();
abortCtrl = ctrl; abortCtrl = ctrl;
try { try {
await runConcurrent(srcs, async (src) => { await runConcurrent(srcs, async (src: Source) => {
const page = nextPageMap.get(src.id)!; const page = nextPageMap.get(src.id)!;
if (ctrl.signal.aborted) return; if (ctrl.signal.aborted) return;
let result: { items: Manga[]; hasNextPage: boolean } | null = null; let result: { items: Manga[]; hasNextPage: boolean } | null = null;
@@ -131,7 +142,7 @@
if (!catsLoaded) { if (!catsLoaded) {
catsLoaded = true; catsLoaded = true;
getAdapter().getCategories() 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); .catch(console.error);
} }
} }
@@ -140,7 +151,7 @@
return [ return [
{ {
label: m.inLibrary ? "In Library" : "Add to library", label: m.inLibrary ? "In Library" : "Add to library",
icon: BookmarkSimple, icon: BookmarkSimpleIcon,
disabled: m.inLibrary, disabled: m.inLibrary,
onClick: () => getAdapter().addToLibrary(String(m.id)) onClick: () => getAdapter().addToLibrary(String(m.id))
.then(() => { sourceManga = sourceManga.map((x) => x.id === m.id ? { ...x, inLibrary: true } : x); }) .then(() => { sourceManga = sourceManga.map((x) => x.id === m.id ? { ...x, inLibrary: true } : x); })
@@ -149,17 +160,17 @@
...(categories.length > 0 ? [ ...(categories.length > 0 ? [
{ separator: true } as MenuEntry, { separator: true } as MenuEntry,
...categories.map((cat): MenuEntry => ({ ...categories.map((cat): MenuEntry => ({
label: (cat.mangas?.nodes ?? []).some((x: { id: number }) => x.id === m.id) ? `✓ ${cat.name}` : cat.name, label: (cat.mangas ?? []).some((x: Manga) => x.id === m.id) ? `✓ ${cat.name}` : cat.name,
icon: Folder, icon: FolderIcon,
onClick: () => getAdapter().updateMangaCategories(String(m.id), [cat.id], []).catch(console.error), onClick: () => getAdapter().updateMangaCategories(String(m.id), [cat.id], []).catch(console.error),
})), })),
] : []), ] : []),
{ separator: true }, { separator: true },
{ {
label: "New folder & add", label: "New folder & add",
icon: FolderSimplePlus, icon: FolderSimplePlusIcon,
onClick: async () => { onClick: async () => {
const name = prompt("Folder name:"); const name = prompt("FolderIcon name:");
if (!name?.trim()) return; if (!name?.trim()) return;
const cat = await getAdapter().createCategory(name.trim()).catch(console.error); const cat = await getAdapter().createCategory(name.trim()).catch(console.error);
if (cat) { if (cat) {
@@ -177,7 +188,7 @@
<div class="root"> <div class="root">
<div class="header"> <div class="header">
<button class="back" onclick={onBack}> <button class="back" onclick={onBack}>
<ArrowLeft size={13} weight="light" /><span>Back</span> <ArrowLeftIcon size={13} weight="light" /><span>Back</span>
</button> </button>
<span class="title">{label}</span> <span class="title">{label}</span>
{#if !loadingInitial || filtered.length > 0} {#if !loadingInitial || filtered.length > 0}
@@ -213,7 +224,7 @@
{#if hasMore} {#if hasMore}
<div class="show-more-cell"> <div class="show-more-cell">
<button class="show-more-btn" onclick={loadMore} disabled={loadingMore}> <button class="show-more-btn" onclick={loadMore} disabled={loadingMore}>
{#if loadingMore}<CircleNotch size={13} weight="light" class="anim-spin" /> Loading…{:else}Show more{/if} {#if loadingMore}<CircleNotchIcon size={13} weight="light" class="anim-spin" /> Loading…{:else}Show more{/if}
</button> </button>
</div> </div>
{/if} {/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); } .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; } :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); } .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; } .card-skeleton { padding: 0; }
.cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); } .cover-skeleton { aspect-ratio: 2/3; border-radius: var(--radius-md); }
.title-skeleton { height: 11px; margin-top: var(--sp-2); width: 75%; } .title-skeleton { height: 11px; margin-top: var(--sp-2); width: 75%; }
+9 -9
View File
@@ -37,7 +37,7 @@
let kw_inputEl: HTMLInputElement | null = $state(null); let kw_inputEl: HTMLInputElement | null = $state(null);
let kw_abortCtrl: AbortController | null = null; let kw_abortCtrl: AbortController | null = null;
let kw_debounceTimer: ReturnType<typeof setTimeout> | null = null; let kw_debounceTimer: ReturnType<typeof setTimeout> | null = null;
let kw_localQuery = $state(query); let kw_localQuery = $state("");
let kw_pending = $state(false); let kw_pending = $state(false);
interface SourceResult { interface SourceResult {
@@ -77,19 +77,19 @@
}, 2000); }, 2000);
} }
function kwGetVisibleSources(): Source[] { const kw_visibleSources = $derived.by(() => {
let srcs = allSources; let srcs = allSources;
if (kw_selectedLangs.size > 0) if (kw_selectedLangs.size > 0)
srcs = srcs.filter((s) => kw_selectedLangs.has(s.lang)); srcs = srcs.filter((s) => kw_selectedLangs.has(s.lang));
if (settingsState.settings.contentLevel !== "unrestricted") if (settingsState.settings.contentLevel !== "unrestricted")
srcs = srcs.filter((s) => !shouldHideSource(s, settingsState.settings)); srcs = srcs.filter((s) => !shouldHideSource(s, settingsState.settings));
return srcs; return srcs;
} });
async function kwDoSearch(q: string) { async function kwDoSearch(q: string) {
const trimmed = q.trim(); const trimmed = q.trim();
if (!trimmed) return; if (!trimmed) return;
const visible = kwGetVisibleSources(); const visible = kw_visibleSources;
if (!visible.length) return; if (!visible.length) return;
kw_abortCtrl?.abort(); kw_abortCtrl?.abort();
@@ -102,13 +102,13 @@
await Promise.allSettled(visible.map(async (src) => { await Promise.allSettled(visible.map(async (src) => {
const idx = idxOf.get(src.id)!; const idx = idxOf.get(src.id)!;
try { 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; if (ctrl.signal.aborted) return;
const mangas = result.items.filter((m) => !shouldHideNsfw(m as any, settingsState.settings)); 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) { } catch (e: any) {
if (ctrl.signal.aborted || e?.name === "AbortError") return; 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; 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_anyLoading = $derived(kw_results.some((r) => r.loading));
const kw_allDone = $derived(kw_results.length > 0 && kw_results.every((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)); 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); } .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; } .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; } .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; } .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; } .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%; } .skCard { display: flex; flex-direction: column; gap: var(--sp-2); flex-shrink: 0; width: 100%; }
+14 -6
View File
@@ -5,7 +5,6 @@
import { getAdapter } from "$lib/request-manager"; import { getAdapter } from "$lib/request-manager";
import { settingsState } from "$lib/state/settings.svelte"; import { settingsState } from "$lib/state/settings.svelte";
import { setPreviewManga } from "$lib/state/series.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 { toCachedManga, shouldHideNsfw, runConcurrent, type CachedManga } from "$lib/components/browse/lib/searchFilter";
import type { Manga, Source } from "$lib/types"; import type { Manga, Source } from "$lib/types";
@@ -66,22 +65,31 @@
const availableLangs = $derived(Array.from(new Set<string>(allSources.map((s) => s.lang))).sort()); const availableLangs = $derived(Array.from(new Set<string>(allSources.map((s) => s.lang))).sort());
const hasMultipleLangs = $derived(availableLangs.length > 1); const hasMultipleLangs = $derived(availableLangs.length > 1);
let sourcesAbort: AbortController | null = null;
$effect(() => {
sourcesAbort?.abort();
const ctrl = new AbortController();
sourcesAbort = ctrl;
loadingSources = true; loadingSources = true;
getAdapter().getSources() getAdapter().getSources()
.then((nodes) => { .then((nodes: Source[]) => {
if (ctrl.signal.aborted) return;
localSource = nodes.find((s: Source) => s.id === "0") ?? null; localSource = nodes.find((s: Source) => s.id === "0") ?? null;
allSources = nodes.filter((s: Source) => s.id !== "0"); allSources = nodes.filter((s: Source) => s.id !== "0");
startSourceCacheBuild(); startSourceCacheBuild();
popularStart(allSources); popularStart(allSources);
}) })
.catch(console.error) .catch(console.error)
.finally(() => { loadingSources = false; }); .finally(() => { if (!ctrl.signal.aborted) loadingSources = false; });
return () => { ctrl.abort(); };
});
let popular_raw: Manga[] = $state([]); let popular_raw: Manga[] = $state([]);
let popular_loading = $state(false); let popular_loading = $state(false);
let popular_abortCtrl: AbortController | null = null; let popular_abortCtrl: AbortController | null = null;
let popular_sourcePool: Source[] = $state([]); let popular_sourcePool: Source[] = [];
let popular_sourceCursor = $state(0); let popular_sourceCursor = 0;
let popular_seenIds = new Set<number>(); let popular_seenIds = new Set<number>();
let popular_seenTitles = new Set<string>(); let popular_seenTitles = new Set<string>();
@@ -210,6 +218,7 @@
} }
onDestroy(() => { onDestroy(() => {
sourcesAbort?.abort();
popular_abortCtrl?.abort(); popular_abortCtrl?.abort();
sourceCacheAbort?.abort(); sourceCacheAbort?.abort();
}); });
@@ -267,7 +276,6 @@
{sourceCacheLoading} {sourceCacheLoading}
{sourceCacheEnriching} {sourceCacheEnriching}
onPreview={(m) => setPreviewManga(m)} onPreview={(m) => setPreviewManga(m)}
onGenreDrill={(genre) => goto(`/browse?genre=${encodeURIComponent(genre)}`)}
/> />
{:else} {:else}
<SourceTab <SourceTab
+4 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from "svelte"; import { onDestroy } from "svelte";
import { getAdapter } from "$lib/request-manager"; 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 { shouldHideNsfw, shouldHideSource } from "$lib/core/util";
import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte"; import Thumbnail from "$lib/components/shared/manga/Thumbnail.svelte";
import ContextMenu from "$lib/components/shared/ui/ContextMenu.svelte"; import ContextMenu from "$lib/components/shared/ui/ContextMenu.svelte";
@@ -20,7 +20,7 @@
const preferredLang = $derived(settingsState.settings.preferredExtensionLang ?? "en"); 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_activeSource: Source | null = $state(null);
let src_browseResults: Manga[] = $state([]); let src_browseResults: Manga[] = $state([]);
let src_loadingBrowse = $state(false); let src_loadingBrowse = $state(false);
@@ -122,7 +122,7 @@
function togglePinnedSource(id: string) { function togglePinnedSource(id: string) {
const current = settingsState.settings.pinnedSourceIds ?? []; const current = settingsState.settings.pinnedSourceIds ?? [];
const next = current.includes(id) ? current.filter((x: string) => x !== id) : [...current, id]; const next = current.includes(id) ? current.filter((x: string) => x !== id) : [...current, id];
settingsState.updateSettings({ pinnedSourceIds: next }); updateSettings({ pinnedSourceIds: next });
} }
onDestroy(() => { src_abortCtrl?.abort(); }); 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; } .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); } .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); } .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; } .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; } .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); } .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); }
+26 -18
View File
@@ -42,6 +42,7 @@
let tag_localOffset = $state(0); let tag_localOffset = $state(0);
let tag_localHasNext = $state(false); 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); const renderLimit = $derived(settingsState.settings.renderLimit ?? 48);
@@ -52,14 +53,6 @@
untrack(() => tagFetchLocal(_tags, _mode, _statuses)); 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[]) { async function tagFetchLocal(activeTags: string[], tagMode: TagMode, activeStatuses: string[]) {
if (activeTags.length === 0 && activeStatuses.length === 0) { if (activeTags.length === 0 && activeStatuses.length === 0) {
@@ -67,6 +60,7 @@
return; return;
} }
tag_abortLocal?.abort(); tag_abortLocal?.abort();
tag_abortLoadMore?.abort();
const ctrl = new AbortController(); const ctrl = new AbortController();
tag_abortLocal = ctrl; tag_abortLocal = ctrl;
tag_localResults = []; tag_totalCount = 0; tag_localOffset = 0; tag_localHasNext = false; tag_localResults = []; tag_totalCount = 0; tag_localOffset = 0; tag_localHasNext = false;
@@ -82,6 +76,7 @@
tag_totalCount = d.totalCount; tag_totalCount = d.totalCount;
tag_localHasNext = d.hasNextPage; tag_localHasNext = d.hasNextPage;
tag_localOffset = limit; tag_localOffset = limit;
if (d.hasNextPage && tag_localResults.length < 20) tagLoadMoreLocal();
} catch (e: any) { } catch (e: any) {
if (e?.name !== "AbortError") console.error(e); if (e?.name !== "AbortError") console.error(e);
} finally { } finally {
@@ -91,10 +86,10 @@
async function tagLoadMoreLocal() { async function tagLoadMoreLocal() {
if (tag_loadingMoreLocal || !tag_localHasNext) return; if (tag_loadingMoreLocal || !tag_localHasNext) return;
tag_loadingMoreLocal = true; tag_abortLoadMore?.abort();
tag_abortLocal?.abort();
const ctrl = new AbortController(); const ctrl = new AbortController();
tag_abortLocal = ctrl; tag_abortLoadMore = ctrl;
tag_loadingMoreLocal = true;
const limit = renderLimit; const limit = renderLimit;
try { try {
const d = await getAdapter().getMangasByGenre( const d = await getAdapter().getMangasByGenre(
@@ -196,17 +191,22 @@
let tag_autoSearchFired = $state(false); let tag_autoSearchFired = $state(false);
$effect(() => { $effect(() => {
const _tags = tag_activeTags; void tag_activeTags;
const _statuses = tag_activeStatuses; void tag_activeStatuses;
untrack(() => { tag_autoSearchFired = false; });
});
$effect(() => {
const _loadingLocal = tag_loadingLocal; const _loadingLocal = tag_loadingLocal;
const _hasFilters = tag_hasActiveFilters; const _hasFilters = tag_hasActiveFilters;
const _resultLen = tag_localResults.length; const _resultLen = tag_localResults.length;
const _cacheReady = sourceCacheReady; const _cacheReady = sourceCacheReady;
untrack(() => { tag_autoSearchFired = false; }); if (!_loadingLocal && _hasFilters && _cacheReady) {
if (!_loadingLocal && _hasFilters && !tag_autoSearchFired && !tag_searchSources && _cacheReady) { untrack(() => {
if (_resultLen < 20) { if (!tag_autoSearchFired && !tag_searchSources && _resultLen < 20) {
untrack(() => { tag_autoSearchFired = true; tag_searchSources = true; }); tag_autoSearchFired = true;
tag_searchSources = true;
} }
});
} }
}); });
@@ -239,6 +239,7 @@
onDestroy(() => { onDestroy(() => {
tag_abortLocal?.abort(); tag_abortLocal?.abort();
tag_abortLoadMore?.abort();
tag_fanOutAbort?.abort(); tag_fanOutAbort?.abort();
}); });
</script> </script>
@@ -379,6 +380,10 @@
{#each Array(12) as _, i (i)} {#each Array(12) as _, i (i)}
<div class="skCard"><div class="skeleton skCover"></div><div class="skeleton skTitle"></div></div> <div class="skCard"><div class="skeleton skCover"></div><div class="skeleton skTitle"></div></div>
{/each} {/each}
{:else if tag_localHasNext}
<div class="loadMoreRow">
<button class="loadMoreBtn" onclick={tagLoadMoreLocal}>Load more</button>
</div>
{/if} {/if}
</div> </div>
{:else} {: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)); } .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; } .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; } .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); } .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); } .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; } .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); } .skCard { display: flex; flex-direction: column; gap: var(--sp-2); }
@keyframes shimmer { from { background-position: -200% 0 } to { background-position: 200% 0 } } @keyframes shimmer { from { background-position: -200% 0 } to { background-position: 200% 0 } }
@@ -59,7 +59,7 @@
const unreadCount = $derived(totalCount - readCount); const unreadCount = $derived(totalCount - readCount);
const downloadedCount = $derived(chapters.filter((c) => c.downloaded).length); const downloadedCount = $derived(chapters.filter((c) => c.downloaded).length);
const bookmarkCount = $derived(chapters.filter((c) => c.bookmarked).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( const scanlators = $derived(
[...new Set(chapters.map((c) => c.scanlator).filter((s): s is string => !!s?.trim()))], [...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); const inProgress = asc.find((c) => !c.read && (c.lastPageRead ?? 0) > 0);
if (inProgress) return { ch: inProgress, type: "continue" as const, resumePage: inProgress.lastPageRead! }; if (inProgress) return { ch: inProgress, type: "continue" as const, resumePage: inProgress.lastPageRead! };
const firstUnread = asc.find((c) => !c.read); 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 }; return { ch: asc[0], type: "reread" as const, resumePage: null };
}); });
@@ -127,10 +127,14 @@
linkPickerOpen = true; linkPickerOpen = true;
if (allMangaForLink.length) return; if (allMangaForLink.length) return;
loadingLinkList = true; loadingLinkList = true;
getAdapter().getMangaList({}) try {
.then((d) => { allMangaForLink = d.items; }) const result = await getAdapter().getMangaList({});
.catch(console.error) allMangaForLink = result.items;
.finally(() => { loadingLinkList = false; }); } catch (e) {
console.error(e);
} finally {
loadingLinkList = false;
}
} }
function closeLinkPicker() { linkPickerOpen = false; } function closeLinkPicker() { linkPickerOpen = false; }
@@ -139,10 +143,14 @@
coverPickerOpen = true; coverPickerOpen = true;
if (allMangaForLink.length) return; if (allMangaForLink.length) return;
loadingLinkList = true; loadingLinkList = true;
getAdapter().getMangaList({}) try {
.then((d) => { allMangaForLink = d.items; }) const result = await getAdapter().getMangaList({});
.catch(console.error) allMangaForLink = result.items;
.finally(() => { loadingLinkList = false; }); } catch (e) {
console.error(e);
} finally {
loadingLinkList = false;
}
} }
$effect(() => { $effect(() => {
@@ -158,14 +166,21 @@
if (shouldAutoLink) { if (shouldAutoLink) {
if (allMangaForLink.length) { if (allMangaForLink.length) {
autoLinkLibrary(focal, allMangaForLink) 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 { } else {
loadingLinkList = true; loadingLinkList = true;
getAdapter().getMangaList({}) (async () => {
.then((d) => { allMangaForLink = d.items; return autoLinkLibrary(focal, d.items); }) try {
.then(n => { if (n > 0) addToast({ kind: "success", title: "Series linked", body: `${n} new link${n === 1 ? "" : "s"} found` }); }) const result = await getAdapter().getMangaList({});
.catch(console.error) allMangaForLink = result.items;
.finally(() => { loadingLinkList = false; }); 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) { function loadCategories(id: number) {
catsLoading = true; catsLoading = true;
getAdapter().getCategories() getAdapter().getCategories()
.then((cats) => { .then((cats: Category[]) => {
allCategories = cats.filter((c) => c.id !== 0); allCategories = cats.filter((c: Category) => c.id !== 0);
mangaCategories = allCategories.filter((c) => c.mangas?.nodes?.some((m) => m.id === id)); mangaCategories = allCategories.filter((c: Category) => c.mangas?.some((m: Manga) => m.id === id));
}) })
.catch(console.error) .catch(console.error)
.finally(() => { catsLoading = false; }); .finally(() => { catsLoading = false; });
@@ -836,8 +851,8 @@
.read-btn:hover { filter: brightness(1.1); } .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-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 { 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; overflow: visible; } .desc.desc-open { display: block; -webkit-line-clamp: unset; line-clamp: unset; overflow: visible; }
.desc-toggle { .desc-toggle {
display: flex; align-items: center; gap: var(--sp-1); align-self: flex-start; 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); font-family: var(--font-ui); font-size: var(--text-xs); color: var(--text-faint);