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/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"
}
}
+39 -23
View File
@@ -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: {}
+26 -15
View File
@@ -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 @@
<div class="root">
<div class="header">
<button class="back" onclick={onBack}>
<ArrowLeft size={13} weight="light" /><span>Back</span>
<ArrowLeftIcon size={13} weight="light" /><span>Back</span>
</button>
<span class="title">{label}</span>
{#if !loadingInitial || filtered.length > 0}
@@ -213,7 +224,7 @@
{#if hasMore}
<div class="show-more-cell">
<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>
</div>
{/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%; }
+9 -9
View File
@@ -37,7 +37,7 @@
let kw_inputEl: HTMLInputElement | null = $state(null);
let kw_abortCtrl: AbortController | null = null;
let kw_debounceTimer: ReturnType<typeof setTimeout> | 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%; }
+14 -6
View File
@@ -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<string>(allSources.map((s) => s.lang))).sort());
const hasMultipleLangs = $derived(availableLangs.length > 1);
let sourcesAbort: AbortController | null = null;
$effect(() => {
sourcesAbort?.abort();
const ctrl = new AbortController();
sourcesAbort = ctrl;
loadingSources = true;
getAdapter().getSources()
.then((nodes) => {
.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(() => { loadingSources = false; });
.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<number>();
let popular_seenTitles = new Set<string>();
@@ -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}
<SourceTab
+4 -4
View File
@@ -1,7 +1,7 @@
<script lang="ts">
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); }
+26 -18
View File
@@ -42,6 +42,7 @@
let tag_localOffset = $state(0);
let tag_localHasNext = $state(false);
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();
});
</script>
@@ -379,6 +380,10 @@
{#each Array(12) as _, i (i)}
<div class="skCard"><div class="skeleton skCover"></div><div class="skeleton skTitle"></div></div>
{/each}
{:else if tag_localHasNext}
<div class="loadMoreRow">
<button class="loadMoreBtn" onclick={tagLoadMoreLocal}>Load more</button>
</div>
{/if}
</div>
{: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 } }
@@ -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);