mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Fix: Browse Bug Fixes & Enhancements
This commit is contained in:
@@ -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%; }
|
||||
|
||||
@@ -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%; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
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<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
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
</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);
|
||||
|
||||
Reference in New Issue
Block a user