mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: Removed Show-More for Preferential Loading using PR Methods
This commit is contained in:
+119
-141
@@ -24,34 +24,38 @@
|
|||||||
let srch_sourcePool: Source[] = $state([]);
|
let srch_sourcePool: Source[] = $state([]);
|
||||||
let srch_sourceCursor = $state(0);
|
let srch_sourceCursor = $state(0);
|
||||||
let srch_hasMorePopular = $state(false);
|
let srch_hasMorePopular = $state(false);
|
||||||
let srch_visibleLimit = $state(SEARCH_VISIBLE_LIMIT);
|
|
||||||
|
|
||||||
function srch_filterOut(mangas: Manga[]): Manga[] {
|
function dedupeSourcesByLang(sources: Source[], lang: string, applyHide = false): Source[] {
|
||||||
return dedupeMangaByTitle(
|
|
||||||
dedupeMangaById(mangas.filter(m => !shouldHideNsfw(m, store.settings))),
|
|
||||||
store.settings.mangaLinks,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function srch_rotatedSources(sources: Source[]): Source[] {
|
|
||||||
const lang = store.settings?.preferredExtensionLang || "en";
|
|
||||||
const eligible = sources.filter(s => s.id !== "0" && !shouldHideSource(s, store.settings));
|
|
||||||
const map = new Map<string, Source>();
|
const map = new Map<string, Source>();
|
||||||
for (const s of eligible) {
|
for (const s of sources) {
|
||||||
|
if (s.id === "0") continue;
|
||||||
|
if (applyHide && shouldHideSource(s, store.settings)) continue;
|
||||||
const existing = map.get(s.name);
|
const existing = map.get(s.name);
|
||||||
if (!existing) { map.set(s.name, s); continue; }
|
if (!existing) { map.set(s.name, s); continue; }
|
||||||
if (s.lang === lang && existing.lang !== lang) map.set(s.name, s);
|
const existingPref = existing.lang === lang;
|
||||||
|
const newPref = s.lang === lang;
|
||||||
|
if (newPref && !existingPref) map.set(s.name, s);
|
||||||
|
else if (!existingPref && !newPref && s.lang < existing.lang) map.set(s.name, s);
|
||||||
}
|
}
|
||||||
return Array.from(map.values());
|
return Array.from(map.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let srch_seenIds = new Set<number>();
|
||||||
|
let srch_seenTitles = new Set<string>();
|
||||||
|
|
||||||
function srch_push(incoming: Manga[]) {
|
function srch_push(incoming: Manga[]) {
|
||||||
const filtered = srch_filterOut(incoming);
|
const toAdd: Manga[] = [];
|
||||||
if (!filtered.length) return;
|
for (const m of incoming) {
|
||||||
srch_results = dedupeMangaByTitle(
|
if (shouldHideNsfw(m, store.settings)) continue;
|
||||||
dedupeMangaById([...srch_results, ...filtered]),
|
if (srch_seenIds.has(m.id)) continue;
|
||||||
store.settings.mangaLinks,
|
const norm = normalizeTitle(m.title, store.settings.mangaLinks);
|
||||||
).slice(0, SEARCH_LIMIT);
|
if (srch_seenTitles.has(norm)) continue;
|
||||||
|
srch_seenIds.add(m.id);
|
||||||
|
srch_seenTitles.add(norm);
|
||||||
|
toAdd.push(m);
|
||||||
|
}
|
||||||
|
if (!toAdd.length) return;
|
||||||
|
srch_results = [...srch_results, ...toAdd].slice(0, SEARCH_LIMIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function srch_fanOut(signal: AbortSignal) {
|
async function srch_fanOut(signal: AbortSignal) {
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(batch.map(async (src) => {
|
await runConcurrent(batch, async (src) => {
|
||||||
for (let page = 1; page <= SEARCH_PAGES; page++) {
|
for (let page = 1; page <= SEARCH_PAGES; page++) {
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
const key = dKey(src.id, page);
|
const key = dKey(src.id, page);
|
||||||
@@ -81,7 +85,7 @@
|
|||||||
}
|
}
|
||||||
srch_push(mangas);
|
srch_push(mangas);
|
||||||
}
|
}
|
||||||
}));
|
}, signal);
|
||||||
|
|
||||||
srch_sourceCursor += batch.length;
|
srch_sourceCursor += batch.length;
|
||||||
srch_hasMorePopular = srch_sourceCursor < srch_sourcePool.length;
|
srch_hasMorePopular = srch_sourceCursor < srch_sourcePool.length;
|
||||||
@@ -92,28 +96,21 @@
|
|||||||
srch_abortCtrl?.abort();
|
srch_abortCtrl?.abort();
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
srch_abortCtrl = ctrl;
|
srch_abortCtrl = ctrl;
|
||||||
srch_sourcePool = srch_rotatedSources(sources);
|
srch_seenIds.clear();
|
||||||
|
srch_seenTitles.clear();
|
||||||
|
srch_sourcePool = dedupeSourcesByLang(sources, store.settings?.preferredExtensionLang || "en", true);
|
||||||
srch_sourceCursor = 0;
|
srch_sourceCursor = 0;
|
||||||
srch_hasMorePopular = false;
|
srch_hasMorePopular = false;
|
||||||
srch_moreLoading = false;
|
srch_moreLoading = false;
|
||||||
srch_visibleLimit = SEARCH_VISIBLE_LIMIT;
|
|
||||||
srch_loading = true;
|
srch_loading = true;
|
||||||
srch_fanOut(ctrl.signal)
|
(async () => {
|
||||||
.catch(() => {})
|
try {
|
||||||
.finally(() => { if (!ctrl.signal.aborted) srch_loading = false; });
|
while (!ctrl.signal.aborted && srch_sourceCursor < srch_sourcePool.length) {
|
||||||
}
|
await srch_fanOut(ctrl.signal);
|
||||||
|
}
|
||||||
function srch_loadMorePopular() {
|
} catch {}
|
||||||
if (srch_moreLoading) return;
|
if (!ctrl.signal.aborted) srch_loading = false;
|
||||||
srch_visibleLimit += SEARCH_VISIBLE_LIMIT;
|
})();
|
||||||
if (!srch_hasMorePopular) return;
|
|
||||||
srch_abortCtrl?.abort();
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
srch_abortCtrl = ctrl;
|
|
||||||
srch_moreLoading = true;
|
|
||||||
srch_fanOut(ctrl.signal)
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => { if (!ctrl.signal.aborted) srch_moreLoading = false; });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchTab = "keyword" | "tag" | "source";
|
type SearchTab = "keyword" | "tag" | "source";
|
||||||
@@ -133,6 +130,7 @@
|
|||||||
inLibrary: boolean;
|
inLibrary: boolean;
|
||||||
status: string;
|
status: string;
|
||||||
genre: string[];
|
genre: string[];
|
||||||
|
lowerGenres: string[];
|
||||||
sourceId: string;
|
sourceId: string;
|
||||||
genreEnriched: boolean;
|
genreEnriched: boolean;
|
||||||
}
|
}
|
||||||
@@ -168,26 +166,6 @@
|
|||||||
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker));
|
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, items.length) }, worker));
|
||||||
}
|
}
|
||||||
|
|
||||||
function dedupSourcesByLang(sources: Source[], preferredLang: string): Source[] {
|
|
||||||
const map = new Map<string, Source>();
|
|
||||||
for (const s of sources) {
|
|
||||||
if (s.id === "0") continue;
|
|
||||||
const key = s.name;
|
|
||||||
const existing = map.get(key);
|
|
||||||
if (!existing) {
|
|
||||||
map.set(key, s);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const existingIsPreferred = existing.lang === preferredLang;
|
|
||||||
const newIsPreferred = s.lang === preferredLang;
|
|
||||||
if (newIsPreferred && !existingIsPreferred) {
|
|
||||||
map.set(key, s);
|
|
||||||
} else if (!existingIsPreferred && !newIsPreferred && s.lang < existing.lang) {
|
|
||||||
map.set(key, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(map.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceCache = new Map<number, CachedManga>();
|
const sourceCache = new Map<number, CachedManga>();
|
||||||
let sourceCacheReady = $state(false);
|
let sourceCacheReady = $state(false);
|
||||||
@@ -205,22 +183,31 @@
|
|||||||
await runConcurrent(tasks, async ({ src, page }) => {
|
await runConcurrent(tasks, async ({ src, page }) => {
|
||||||
if (signal.aborted) return;
|
if (signal.aborted) return;
|
||||||
try {
|
try {
|
||||||
const d = await gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>(
|
const cacheKey = `${src.id}|POPULAR|All:p${page}`;
|
||||||
FETCH_SOURCE_MANGA,
|
let mangas: Manga[];
|
||||||
{ source: src.id, type: "POPULAR", page },
|
if (store.searchCache?.has(cacheKey)) {
|
||||||
signal,
|
mangas = store.searchCache.get(cacheKey)!;
|
||||||
);
|
} else {
|
||||||
if (signal.aborted) return;
|
const d = await gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>(
|
||||||
for (const m of d.fetchSourceManga.mangas) {
|
FETCH_SOURCE_MANGA,
|
||||||
|
{ source: src.id, type: "POPULAR", page },
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
if (signal.aborted) return;
|
||||||
|
mangas = d.fetchSourceManga.mangas;
|
||||||
|
store.searchCache?.set(cacheKey, mangas);
|
||||||
|
}
|
||||||
|
for (const m of mangas) {
|
||||||
if (!sourceCache.has(m.id)) {
|
if (!sourceCache.has(m.id)) {
|
||||||
sourceCache.set(m.id, {
|
sourceCache.set(m.id, {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
title: m.title,
|
title: m.title,
|
||||||
thumbnailUrl: m.thumbnailUrl,
|
thumbnailUrl: m.thumbnailUrl,
|
||||||
inLibrary: m.inLibrary,
|
inLibrary: m.inLibrary,
|
||||||
status: (m as any).status ?? "UNKNOWN",
|
status: (m as any).status ?? "UNKNOWN",
|
||||||
genre: (m as any).genre ?? [],
|
genre: (m as any).genre ?? [],
|
||||||
sourceId: src.id,
|
lowerGenres: ((m as any).genre ?? []).map((g: string) => g.toLowerCase()),
|
||||||
|
sourceId: src.id,
|
||||||
genreEnriched: ((m as any).genre?.length ?? 0) > 0,
|
genreEnriched: ((m as any).genre?.length ?? 0) > 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -308,7 +295,7 @@
|
|||||||
|
|
||||||
let genreMatch = true;
|
let genreMatch = true;
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
const lowerGenres = m.genre.map((g) => g.toLowerCase());
|
const lowerGenres = m.lowerGenres;
|
||||||
if (mode === "AND") {
|
if (mode === "AND") {
|
||||||
genreMatch = tags.every((t) => lowerGenres.some((g) => g.includes(t.toLowerCase())));
|
genreMatch = tags.every((t) => lowerGenres.some((g) => g.includes(t.toLowerCase())));
|
||||||
} else {
|
} else {
|
||||||
@@ -356,8 +343,7 @@
|
|||||||
sourceCacheLoading = true;
|
sourceCacheLoading = true;
|
||||||
sourceCache.clear();
|
sourceCache.clear();
|
||||||
|
|
||||||
const dedupedSources = dedupSourcesByLang(allSources, preferredLang)
|
const dedupedSources = dedupeSourcesByLang(allSources, preferredLang, true);
|
||||||
.filter((s) => !shouldHideSource(s, store.settings));
|
|
||||||
|
|
||||||
buildSourceCache(dedupedSources, ctrl.signal)
|
buildSourceCache(dedupedSources, ctrl.signal)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -379,7 +365,6 @@
|
|||||||
let kw_results: SourceResult[] = $state([]);
|
let kw_results: SourceResult[] = $state([]);
|
||||||
let kw_showAdvanced = $state(false);
|
let kw_showAdvanced = $state(false);
|
||||||
let kw_selectedLangs: Set<string> = $state(new Set());
|
let kw_selectedLangs: Set<string> = $state(new Set());
|
||||||
let kw_visibleLimit = $state(SEARCH_VISIBLE_LIMIT);
|
|
||||||
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;
|
||||||
@@ -428,37 +413,35 @@
|
|||||||
async function kwDoSearch(q: string) {
|
async function kwDoSearch(q: string) {
|
||||||
const trimmed = q.trim();
|
const trimmed = q.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) return;
|
||||||
kw_visibleLimit = SEARCH_VISIBLE_LIMIT;
|
|
||||||
const visible = kwGetVisibleSources();
|
const visible = kwGetVisibleSources();
|
||||||
if (!visible.length) return;
|
if (!visible.length) return;
|
||||||
kw_abortCtrl?.abort();
|
kw_abortCtrl?.abort();
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
kw_abortCtrl = ctrl;
|
kw_abortCtrl = ctrl;
|
||||||
kw_results = visible.map((src) => ({ source: src, mangas: [], loading: true, error: null }));
|
const initial: SourceResult[] = visible.map((src) => ({ source: src, mangas: [], loading: true, error: null }));
|
||||||
|
kw_results = initial;
|
||||||
|
const indexBySrcId = new Map(visible.map((src, i) => [src.id, i]));
|
||||||
await runConcurrent(visible, async (src) => {
|
await runConcurrent(visible, async (src) => {
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
|
const idx = indexBySrcId.get(src.id)!;
|
||||||
try {
|
try {
|
||||||
const d = await gql<{ fetchSourceManga: { mangas: Manga[] } }>(
|
const d = await gql<{ fetchSourceManga: { mangas: Manga[] } }>(
|
||||||
FETCH_SOURCE_MANGA, { source: src.id, type: "SEARCH", page: 1, query: trimmed }, ctrl.signal,
|
FETCH_SOURCE_MANGA, { source: src.id, type: "SEARCH", page: 1, query: trimmed }, ctrl.signal,
|
||||||
);
|
);
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
const mangas = d.fetchSourceManga.mangas.filter((m) => !shouldHideNsfw(m, store.settings));
|
const mangas = d.fetchSourceManga.mangas.filter((m) => !shouldHideNsfw(m, store.settings));
|
||||||
kw_results = kw_results.map((r) =>
|
const next = [...kw_results];
|
||||||
r.source.id === src.id ? { ...r, mangas, loading: false } : r,
|
next[idx] = { ...next[idx], mangas, loading: false };
|
||||||
);
|
kw_results = next;
|
||||||
} 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) =>
|
const next = [...kw_results];
|
||||||
r.source.id === src.id ? { ...r, loading: false, error: e.message ?? "Error" } : r,
|
next[idx] = { ...next[idx], loading: false, error: (e as any).message ?? "Error" };
|
||||||
);
|
kw_results = next;
|
||||||
}
|
}
|
||||||
}, ctrl.signal);
|
}, ctrl.signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
function kwLoadMore() {
|
|
||||||
kw_visibleLimit += SEARCH_VISIBLE_LIMIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
function kwToggleLang(lang: string) {
|
function kwToggleLang(lang: string) {
|
||||||
const next = new Set(kw_selectedLangs);
|
const next = new Set(kw_selectedLangs);
|
||||||
if (next.has(lang)) { if (next.size === 1) return; next.delete(lang); }
|
if (next.has(lang)) { if (next.size === 1) return; next.delete(lang); }
|
||||||
@@ -552,7 +535,11 @@
|
|||||||
tag_sourceFanOut = [];
|
tag_sourceFanOut = [];
|
||||||
tag_fanOutLoading = true;
|
tag_fanOutLoading = true;
|
||||||
|
|
||||||
const srcs = srch_rotatedSources(allSources);
|
const fanOutSeenIds = new Set<number>();
|
||||||
|
const fanOutSeenTitles = new Set<string>();
|
||||||
|
const genreLower = genre.toLowerCase();
|
||||||
|
|
||||||
|
const srcs = dedupeSourcesByLang(allSources, store.settings?.preferredExtensionLang || "en", true);
|
||||||
const PAGES = 2;
|
const PAGES = 2;
|
||||||
|
|
||||||
await runConcurrent(srcs, async (src) => {
|
await runConcurrent(srcs, async (src) => {
|
||||||
@@ -579,15 +566,22 @@
|
|||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
|
|
||||||
const matching = mangas.filter(m =>
|
const matching = mangas.filter(m =>
|
||||||
((m as any).genre ?? []).some((g: string) => g.toLowerCase() === genre.toLowerCase())
|
((m as any).genre ?? []).some((g: string) => g.toLowerCase() === genreLower)
|
||||||
);
|
);
|
||||||
const toAdd = (matching.length ? matching : mangas).filter(m => !shouldHideNsfw(m, store.settings));
|
const candidates = (matching.length ? matching : mangas).filter(m => !shouldHideNsfw(m, store.settings));
|
||||||
|
|
||||||
|
const toAdd: Manga[] = [];
|
||||||
|
for (const m of candidates) {
|
||||||
|
if (fanOutSeenIds.has(m.id)) continue;
|
||||||
|
const norm = normalizeTitle(m.title, store.settings.mangaLinks);
|
||||||
|
if (fanOutSeenTitles.has(norm)) continue;
|
||||||
|
fanOutSeenIds.add(m.id);
|
||||||
|
fanOutSeenTitles.add(norm);
|
||||||
|
toAdd.push(m);
|
||||||
|
}
|
||||||
|
|
||||||
if (toAdd.length) {
|
if (toAdd.length) {
|
||||||
tag_sourceFanOut = dedupeMangaByTitle(
|
tag_sourceFanOut = [...tag_sourceFanOut, ...toAdd].slice(0, SEARCH_LIMIT);
|
||||||
dedupeMangaById([...tag_sourceFanOut, ...toAdd]),
|
|
||||||
store.settings.mangaLinks,
|
|
||||||
).slice(0, SEARCH_LIMIT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasNextPage) return;
|
if (!hasNextPage) return;
|
||||||
@@ -599,17 +593,16 @@
|
|||||||
|
|
||||||
let tag_autoSearchFired = $state(false);
|
let tag_autoSearchFired = $state(false);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
// Track filter changes to reset the guard, then auto-enable source search when local results are sparse.
|
||||||
|
const _tags = tag_activeTags;
|
||||||
|
const _statuses = tag_activeStatuses;
|
||||||
|
untrack(() => { tag_autoSearchFired = false; });
|
||||||
if (!tag_loadingLocal && tag_hasActiveFilters && !tag_autoSearchFired && !tag_searchSources && sourceCacheReady) {
|
if (!tag_loadingLocal && tag_hasActiveFilters && !tag_autoSearchFired && !tag_searchSources && sourceCacheReady) {
|
||||||
if (tag_localResults.length < 20) {
|
if (tag_localResults.length < 20) {
|
||||||
untrack(() => { tag_autoSearchFired = true; tag_searchSources = true; });
|
untrack(() => { tag_autoSearchFired = true; tag_searchSources = true; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$effect(() => {
|
|
||||||
const _ = tag_activeTags;
|
|
||||||
const __ = tag_activeStatuses;
|
|
||||||
untrack(() => { tag_autoSearchFired = false; });
|
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
||||||
@@ -663,6 +656,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (tag_localHasNext && !tag_loadingMoreLocal && !tag_loadingLocal) {
|
||||||
|
tagLoadMoreLocal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function tagToggleTag(tag: string) {
|
function tagToggleTag(tag: string) {
|
||||||
tag_activeTags = tag_activeTags.includes(tag)
|
tag_activeTags = tag_activeTags.includes(tag)
|
||||||
? tag_activeTags.filter((t) => t !== tag)
|
? tag_activeTags.filter((t) => t !== tag)
|
||||||
@@ -886,7 +885,7 @@
|
|||||||
<span class="searchLabel">Popular right now</span>
|
<span class="searchLabel">Popular right now</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchGrid">
|
<div class="searchGrid">
|
||||||
{#each srch_results.slice(0, srch_visibleLimit) as m (m.id)}
|
{#each srch_results as m (m.id)}
|
||||||
<button class="srchCard" onclick={() => setPreviewManga(m)}>
|
<button class="srchCard" onclick={() => setPreviewManga(m)}>
|
||||||
<div class="srchCoverWrap">
|
<div class="srchCoverWrap">
|
||||||
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" />
|
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" />
|
||||||
@@ -898,20 +897,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if srch_loading}
|
{#if srch_loading}
|
||||||
{#each Array(6) as _, i (i)}
|
{#each Array(12) as _, i (i)}
|
||||||
<div class="skCard"><div class="skeleton skCover"></div></div>
|
<div class="skCard"><div class="skeleton skCover"></div></div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if srch_hasMorePopular || srch_results.length > srch_visibleLimit}
|
|
||||||
<div class="loadMoreRow loadMoreSticky">
|
|
||||||
<button class="showMoreBtn" onclick={srch_loadMorePopular} disabled={srch_moreLoading}>
|
|
||||||
{srch_moreLoading ? "Loading…" : "Show more"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<svg width="36" height="36" viewBox="0 0 256 256" fill="currentColor" class="emptyIcon" aria-hidden="true">
|
<svg width="36" height="36" viewBox="0 0 256 256" fill="currentColor" class="emptyIcon" aria-hidden="true">
|
||||||
@@ -933,7 +925,7 @@
|
|||||||
<span class="searchLabel">{kw_flatResults.length} result{kw_flatResults.length !== 1 ? "s" : ""}</span>
|
<span class="searchLabel">{kw_flatResults.length} result{kw_flatResults.length !== 1 ? "s" : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchGrid">
|
<div class="searchGrid">
|
||||||
{#each kw_flatResults.slice(0, kw_visibleLimit) as m (m.id)}
|
{#each kw_flatResults as m (m.id)}
|
||||||
<button class="srchCard" onclick={() => setPreviewManga(m)}>
|
<button class="srchCard" onclick={() => setPreviewManga(m)}>
|
||||||
<div class="srchCoverWrap">
|
<div class="srchCoverWrap">
|
||||||
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" />
|
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" />
|
||||||
@@ -945,20 +937,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if kw_anyLoading}
|
{#if kw_anyLoading}
|
||||||
{#each Array(6) as _, i (i)}
|
{#each Array(6) as _, i (i)}
|
||||||
<div class="skCard"><div class="skeleton skCover"></div></div>
|
<div class="skCard"><div class="skeleton skCover"></div></div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if kw_flatResults.length > kw_visibleLimit || kw_anyLoading}
|
|
||||||
<div class="loadMoreRow loadMoreSticky">
|
|
||||||
<button class="showMoreBtn" onclick={kwLoadMore}>
|
|
||||||
Show more
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{:else if kw_anyLoading}
|
{:else if kw_anyLoading}
|
||||||
<div class="searchGrid">
|
<div class="searchGrid">
|
||||||
{#each Array(12) as _, i (i)}
|
{#each Array(12) as _, i (i)}
|
||||||
@@ -1105,18 +1090,10 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if tag_localHasNext}
|
{#if tag_loadingMoreLocal}
|
||||||
<div class="showMoreCell">
|
{#each Array(12) as _, i (i)}
|
||||||
<button class="showMoreBtn" onclick={tagLoadMoreLocal} disabled={tag_loadingMoreLocal}>
|
<div class="skCard"><div class="skeleton skCover"></div><div class="skeleton skTitle"></div></div>
|
||||||
{#if tag_loadingMoreLocal}
|
{/each}
|
||||||
<svg width="13" height="13" viewBox="0 0 256 256" fill="currentColor" class="anim-spin" aria-hidden="true">
|
|
||||||
<path d="M232,128a104,104,0,0,1-208,0c0-41,23.81-78.36,60.66-95.27a8,8,0,0,1,6.68,14.54C60.15,61.59,40,93.27,40,128a88,88,0,0,0,176,0c0-34.73-20.15-66.41-51.34-80.73a8,8,0,0,1,6.68-14.54C208.19,49.64,232,87,232,128Z"/>
|
|
||||||
</svg> Loading…
|
|
||||||
{:else}
|
|
||||||
Show more (library)
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -1319,14 +1296,15 @@
|
|||||||
.card { display: flex; flex-direction: column; gap: var(--sp-2); cursor: pointer; flex-shrink: 0; width: 110px; text-align: left; background: none; border: none; padding: 0; }
|
.card { display: flex; flex-direction: column; gap: var(--sp-2); cursor: pointer; flex-shrink: 0; width: 110px; text-align: left; background: none; border: none; padding: 0; }
|
||||||
.card:hover :global(.cover) { filter: brightness(1.06); }
|
.card:hover :global(.cover) { filter: brightness(1.06); }
|
||||||
.card:hover .cardTitle { color: var(--text-primary); }
|
.card:hover .cardTitle { color: var(--text-primary); }
|
||||||
.coverWrap { position: relative; width: 100%; aspect-ratio: 2 / 3; border-radius: var(--radius-md); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); transform: translateZ(0); }
|
.coverWrap { position: relative; width: 100%; aspect-ratio: 2 / 3; border-radius: var(--radius-md); overflow: hidden; background: var(--bg-raised); border: 1px solid var(--border-dim); transform: translateZ(0); contain: layout style; }
|
||||||
:global(.cover) { width: 100%; height: 100%; object-fit: cover; transition: filter var(--t-base); }
|
:global(.cover) { width: 100%; height: 100%; object-fit: cover; }
|
||||||
.inLibBadge { position: absolute; bottom: var(--sp-1); right: var(--sp-1); background: var(--accent-dim); color: var(--accent-fg); font-family: var(--font-ui); font-size: 9px; font-weight: var(--weight-medium); letter-spacing: var(--tracking-wide); padding: 1px 5px; border-radius: var(--radius-sm); border: 1px solid var(--accent-muted); }
|
.inLibBadge { position: absolute; top: var(--sp-1); right: var(--sp-1); background: var(--accent-dim); color: var(--accent-fg); font-family: var(--font-ui); font-size: 9px; font-weight: var(--weight-medium); letter-spacing: var(--tracking-wide); padding: 1px 5px; border-radius: var(--radius-sm); border: 1px solid var(--accent-muted); }
|
||||||
.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; -webkit-box-orient: vertical; overflow: hidden; transition: color var(--t-base); }
|
||||||
.skCard { display: flex; flex-direction: column; gap: var(--sp-2); flex-shrink: 0; width: 110px; }
|
.skCard { display: flex; flex-direction: column; gap: var(--sp-2); flex-shrink: 0; width: 110px; }
|
||||||
.tagGrid .card { width: 100%; }
|
.tagGrid .card { width: 100%; }
|
||||||
.tagGrid .skCard { width: 100%; }
|
.tagGrid .skCard { width: 100%; }
|
||||||
.skeleton { border-radius: var(--radius-sm); }
|
@keyframes shimmer { from { background-position: -200% 0 } to { background-position: 200% 0 } }
|
||||||
|
.skeleton { border-radius: var(--radius-sm); background: linear-gradient(90deg, var(--bg-raised) 25%, var(--bg-overlay, color-mix(in srgb, var(--bg-raised) 80%, var(--text-primary) 6%)) 50%, var(--bg-raised) 75%); background-size: 200% 100%; animation: shimmer 1.6s ease-in-out infinite; }
|
||||||
.skCover { aspect-ratio: 2 / 3; width: 100%; border-radius: var(--radius-md); }
|
.skCover { aspect-ratio: 2 / 3; width: 100%; border-radius: var(--radius-md); }
|
||||||
.skTitle { height: 10px; width: 80%; }
|
.skTitle { height: 10px; width: 80%; }
|
||||||
.splitRoot { flex: 1; display: flex; overflow: hidden; }
|
.splitRoot { flex: 1; display: flex; overflow: hidden; }
|
||||||
@@ -1371,7 +1349,7 @@
|
|||||||
.tagClearAll { display: flex; align-items: center; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-faint); padding: 4px 8px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: none; cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
.tagClearAll { display: flex; align-items: center; font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--text-faint); padding: 4px 8px; border-radius: var(--radius-md); border: 1px solid var(--border-dim); background: none; cursor: pointer; transition: color var(--t-base), border-color var(--t-base), background var(--t-base); }
|
||||||
.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; }
|
.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; }
|
||||||
.showMoreCell { grid-column: 1 / -1; display: flex; justify-content: center; gap: var(--sp-2); padding: var(--sp-2) 0; }
|
.showMoreCell { grid-column: 1 / -1; display: flex; justify-content: center; gap: var(--sp-2); 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); }
|
||||||
.showMoreBtn:hover:not(:disabled) { background: var(--bg-raised); color: var(--text-secondary); border-color: var(--border-strong); }
|
.showMoreBtn:hover:not(:disabled) { background: var(--bg-raised); color: var(--text-secondary); border-color: var(--border-strong); }
|
||||||
@@ -1388,10 +1366,10 @@
|
|||||||
.nsfwBadge { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--color-error); background: var(--color-error-bg, rgba(180, 60, 60, 0.08)); border: 1px solid rgba(180, 60, 60, 0.25); border-radius: var(--radius-sm); padding: 1px 5px; margin-left: auto; flex-shrink: 0; }
|
.nsfwBadge { font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide); color: var(--color-error); background: var(--color-error-bg, rgba(180, 60, 60, 0.08)); border: 1px solid rgba(180, 60, 60, 0.25); border-radius: var(--radius-sm); padding: 1px 5px; margin-left: auto; flex-shrink: 0; }
|
||||||
.searchHeader { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4) var(--sp-1); flex-shrink: 0; }
|
.searchHeader { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-3) var(--sp-4) var(--sp-1); flex-shrink: 0; }
|
||||||
.searchLabel { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
|
.searchLabel { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase; }
|
||||||
.searchGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(clamp(90px, 11vw, 130px), 1fr)); gap: var(--sp-2); padding: var(--sp-2) var(--sp-4) var(--sp-6); overflow-y: auto; flex: 1; align-content: start; }
|
.searchGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(clamp(90px, 11vw, 130px), 1fr)); gap: var(--sp-2); padding: var(--sp-2) var(--sp-4) var(--sp-6); overflow-y: auto; flex: 1; align-content: start; will-change: scroll-position; }
|
||||||
.srchCard { background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
|
.srchCard { background: none; border: none; padding: 0; cursor: pointer; text-align: left; }
|
||||||
.srchCard:hover :global(.cover) { filter: brightness(1.08) saturate(1.05); transform: scale(1.02); }
|
.srchCard:hover .srchCoverWrap { filter: brightness(1.08) saturate(1.05); }
|
||||||
.srchCoverWrap { position: relative; aspect-ratio: 2/3; overflow: hidden; border-radius: var(--radius-md); background: var(--bg-raised); border: 1px solid var(--border-dim); box-shadow: 0 2px 8px rgba(0,0,0,0.25); }
|
.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); contain: layout style; }
|
||||||
.srchGradient { position: absolute; inset: 0; 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; 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; padding: var(--sp-2); pointer-events: none; }
|
.srchFooter { position: absolute; bottom: 0; left: 0; right: 0; 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; -webkit-box-orient: vertical; overflow: hidden; text-shadow: 0 1px 4px rgba(0,0,0,0.7); }
|
||||||
@@ -1403,4 +1381,4 @@
|
|||||||
|
|
||||||
<script module>
|
<script module>
|
||||||
function focusOnMount(node: HTMLElement) { node.focus(); }
|
function focusOnMount(node: HTMLElement) { node.focus(); }
|
||||||
</script>
|
</script>
|
||||||
Reference in New Issue
Block a user