diff --git a/README.md b/README.md index d59e134..eb1b343 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Moku is a fast, minimal manga reader frontend for [Suwayomi-Server](https://gith
Home - Discover + TagSearch Reader Preview Tracker diff --git a/src/components/pages/Discover.svelte b/src/components/pages/Discover.svelte deleted file mode 100644 index fa4c1fd..0000000 --- a/src/components/pages/Discover.svelte +++ /dev/null @@ -1,390 +0,0 @@ - - -{#if store.activeSource} - -{:else} -
- -
- Discover -
- {#each GENRE_TABS as tab (tab)} - - {/each} -
- -
- -
- {#if isLoading && visibleGrid.length === 0} -
- {#each Array(24) as _, i (i)} -
- {/each} -
- - {:else if loadError && visibleGrid.length === 0} -
- Could not reach Suwayomi - -
- - {:else if visibleGrid.length === 0} -
Nothing found for "{currentGenre}"
- - {:else} -
- {#each visibleGrid as m (m.id)} - - {/each} -
- {/if} -
- -
-{/if} - -{#if ctx} - ctx = null} /> -{/if} - - diff --git a/src/components/pages/Search.svelte b/src/components/pages/Search.svelte index f54d5ac..a6711c8 100644 --- a/src/components/pages/Search.svelte +++ b/src/components/pages/Search.svelte @@ -2,34 +2,32 @@ import { onDestroy, untrack } from "svelte"; import { gql } from "../../lib/client"; import Thumbnail from "../shared/Thumbnail.svelte"; - import ContextMenu from "../shared/ContextMenu.svelte"; - import type { MenuEntry } from "../shared/ContextMenu.svelte"; - import { GET_SOURCES, FETCH_SOURCE_MANGA, FETCH_MANGA, UPDATE_MANGA, GET_CATEGORIES, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES } from "../../lib/queries"; - import { cache, CACHE_KEYS, getPageSet } from "../../lib/cache"; + import { GET_SOURCES, FETCH_SOURCE_MANGA, FETCH_MANGA } from "../../lib/queries"; + import { getPageSet } from "../../lib/cache"; import { dedupeMangaById, dedupeMangaByTitle, shouldHideNsfw, shouldHideSource, normalizeTitle } from "../../lib/util"; - import { store, setSearchPrefill, setPreviewManga, clearDiscoverCache } from "../../store/state.svelte"; - import type { Manga, Source, Category } from "../../lib/types"; + import { store, setSearchPrefill, setPreviewManga, clearSearchCache } from "../../store/state.svelte"; + import type { Manga, Source } from "../../lib/types"; - const DISCOVER_PAGES = 3; - const DISCOVER_LIMIT = 200; - const DISCOVER_CONCUR = 6; + const SEARCH_PAGES = 3; + const SEARCH_LIMIT = 200; + const SEARCH_CONCUR = 6; function dKey(srcId: string, page: number) { return `${srcId}|POPULAR|All:p${page}`; } - let disc_results: Manga[] = $state([]); - let disc_loading = $state(false); - let disc_abortCtrl: AbortController | null = null; + let srch_results: Manga[] = $state([]); + let srch_loading = $state(false); + let srch_abortCtrl: AbortController | null = null; - function disc_filterOut(mangas: Manga[]): Manga[] { + function srch_filterOut(mangas: Manga[]): Manga[] { return dedupeMangaByTitle( dedupeMangaById(mangas.filter(m => !shouldHideNsfw(m, store.settings))), store.settings.mangaLinks, ); } - function disc_rotatedSources(sources: Source[]): Source[] { + 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(); @@ -41,17 +39,17 @@ return Array.from(map.values()); } - function disc_push(incoming: Manga[]) { - const filtered = disc_filterOut(incoming); + function srch_push(incoming: Manga[]) { + const filtered = srch_filterOut(incoming); if (!filtered.length) return; - disc_results = dedupeMangaByTitle( - dedupeMangaById([...disc_results, ...filtered]), + srch_results = dedupeMangaByTitle( + dedupeMangaById([...srch_results, ...filtered]), store.settings.mangaLinks, - ).slice(0, DISCOVER_LIMIT); + ).slice(0, SEARCH_LIMIT); } - async function disc_fanOut(sources: Source[], signal: AbortSignal) { - const srcs = disc_rotatedSources(sources); + async function srch_fanOut(sources: Source[], signal: AbortSignal) { + const srcs = srch_rotatedSources(sources); if (!srcs.length) return; let i = 0; @@ -59,12 +57,12 @@ while (i < srcs.length) { if (signal.aborted) return; const src = srcs[i++]; - for (let page = 1; page <= DISCOVER_PAGES; page++) { + for (let page = 1; page <= SEARCH_PAGES; page++) { if (signal.aborted) return; const key = dKey(src.id, page); let mangas: Manga[]; - if (store.discoverCache?.has(key)) { - mangas = store.discoverCache.get(key)!; + if (store.searchCache?.has(key)) { + mangas = store.searchCache.get(key)!; } else { const result = await gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>( FETCH_SOURCE_MANGA, @@ -73,25 +71,25 @@ ).then(d => d.fetchSourceManga).catch(() => null); if (!result || signal.aborted) break; mangas = result.mangas; - store.discoverCache?.set(key, mangas); - if (!result.hasNextPage) { disc_push(mangas); break; } + store.searchCache?.set(key, mangas); + if (!result.hasNextPage) { srch_push(mangas); break; } } - disc_push(mangas); + srch_push(mangas); } } } - await Promise.all(Array.from({ length: Math.min(DISCOVER_CONCUR, srcs.length) }, worker)); + await Promise.all(Array.from({ length: Math.min(SEARCH_CONCUR, srcs.length) }, worker)); } - function disc_start(sources: Source[]) { - if (disc_results.length > 0) return; - disc_abortCtrl?.abort(); + function srch_start(sources: Source[]) { + if (srch_results.length > 0) return; + srch_abortCtrl?.abort(); const ctrl = new AbortController(); - disc_abortCtrl = ctrl; - disc_loading = true; - disc_fanOut(sources, ctrl.signal) + srch_abortCtrl = ctrl; + srch_loading = true; + srch_fanOut(sources, ctrl.signal) .catch(() => {}) - .finally(() => { if (!ctrl.signal.aborted) disc_loading = false; }); + .finally(() => { if (!ctrl.signal.aborted) srch_loading = false; }); } type SearchTab = "keyword" | "tag" | "source"; @@ -321,7 +319,7 @@ .then((d) => { allSources = d.sources.nodes.filter((src: Source) => src.id !== "0"); startSourceCacheBuild(); - disc_start(allSources); + srch_start(allSources); }) .catch(console.error) .finally(() => { loadingSources = false; }); @@ -353,7 +351,6 @@ const availableLangs = $derived(Array.from(new Set(allSources.map((s) => s.lang))).sort()); const hasMultipleLangs = $derived(availableLangs.length > 1); - // ── Keyword tab ─────────────────────────────────────────────────────────── let kw_query = $state(""); let kw_results: SourceResult[] = $state([]); let kw_showAdvanced = $state(false); @@ -454,7 +451,6 @@ ) as (Manga & { _sourceName?: string })[]; }); - // ── Tag tab ─────────────────────────────────────────────────────────────── let tag_activeTags: string[] = $state([]); let tag_activeStatuses: string[] = $state([]); let tag_tagMode: TagMode = $state("AND"); @@ -471,16 +467,10 @@ let tag_searchSources = $state(false); let tag_sourceFiltered: CachedManga[] = $state([]); - // Active source fan-out results (Discover-style live fetch per genre tag) let tag_sourceFanOut: Manga[] = $state([]); let tag_fanOutLoading = $state(false); let tag_fanOutAbort: AbortController | null = null; - // Context menu state - let ctx: { x: number; y: number; manga: Manga } | null = $state(null); - let categories: Category[] = $state([]); - let catsLoaded = false; - const tag_filteredGenres = $derived.by(() => { const q = tag_tagFilter.trim().toLowerCase(); return q ? COMMON_GENRES.filter((g) => g.toLowerCase().includes(q)) : COMMON_GENRES; @@ -510,7 +500,6 @@ }); }); - // Fan-out live source search when a single genre tag is active + sources enabled $effect(() => { const _tags = tag_activeTags; const _search = tag_searchSources; @@ -533,7 +522,7 @@ tag_sourceFanOut = []; tag_fanOutLoading = true; - const srcs = disc_rotatedSources(allSources); + const srcs = srch_rotatedSources(allSources); const PAGES = 2; await runConcurrent(srcs, async (src) => { @@ -543,8 +532,8 @@ let mangas: Manga[]; let hasNextPage = false; - if (store.discoverCache?.has(cacheKey)) { - mangas = store.discoverCache.get(cacheKey)!; + if (store.searchCache?.has(cacheKey)) { + mangas = store.searchCache.get(cacheKey)!; } else { const result = await gql<{ fetchSourceManga: { mangas: Manga[]; hasNextPage: boolean } }>( FETCH_SOURCE_MANGA, @@ -554,7 +543,7 @@ if (!result || ctrl.signal.aborted) return; mangas = result.mangas; hasNextPage = result.hasNextPage; - store.discoverCache?.set(cacheKey, mangas); + store.searchCache?.set(cacheKey, mangas); } if (ctrl.signal.aborted) return; @@ -568,7 +557,7 @@ tag_sourceFanOut = dedupeMangaByTitle( dedupeMangaById([...tag_sourceFanOut, ...toAdd]), store.settings.mangaLinks, - ).slice(0, DISCOVER_LIMIT); + ).slice(0, SEARCH_LIMIT); } if (!hasNextPage) return; @@ -660,52 +649,6 @@ tag_searchSources = !tag_searchSources; } - function openCtx(e: MouseEvent, m: Manga) { - e.preventDefault(); e.stopPropagation(); - ctx = { x: e.clientX, y: e.clientY, manga: m }; - if (!catsLoaded) { - catsLoaded = true; - gql<{ categories: { nodes: Category[] } }>(GET_CATEGORIES) - .then(d => { categories = d.categories.nodes.filter(c => c.id !== 0); }) - .catch(console.error); - } - } - - function buildCtxItems(m: Manga): MenuEntry[] { - return [ - { - label: m.inLibrary ? "In Library" : "Add to library", - disabled: m.inLibrary, - onClick: () => gql(UPDATE_MANGA, { id: m.id, inLibrary: true }) - .then(() => { - cache.clear(CACHE_KEYS.LIBRARY); - store.discoverLibraryIds = new Set([...store.discoverLibraryIds, m.id]); - }).catch(console.error), - }, - ...(categories.length > 0 ? [ - { separator: true } as MenuEntry, - ...categories.map(cat => ({ - label: (cat.mangas?.nodes ?? []).some((x: any) => x.id === m.id) ? `✓ ${cat.name}` : cat.name, - onClick: () => gql(UPDATE_MANGA_CATEGORIES, { mangaId: m.id, addTo: [cat.id], removeFrom: [] }).catch(console.error), - })), - ] : []), - { separator: true }, - { - label: "New folder & add", - onClick: async () => { - const n = prompt("Folder name:"); - if (!n?.trim()) return; - const res = await gql<{ createCategory: { category: Category } }>(CREATE_CATEGORY, { name: n.trim() }).catch(console.error); - if (res) { - const cat = res.createCategory.category; - categories = [...categories, cat]; - await gql(UPDATE_MANGA_CATEGORIES, { mangaId: m.id, addTo: [cat.id], removeFrom: [] }).catch(console.error); - } - }, - }, - ]; - } - const tag_localIds = $derived(new Set(tag_localResults.map((m) => m.id))); const tag_mergedResults = $derived.by(() => { @@ -729,7 +672,6 @@ const tag_totalVisible = $derived(tag_mergedResults.length); - // ── Source browse tab ───────────────────────────────────────────────────── let src_selectedLang = $state(preferredLang || "all"); let src_activeSource: Source | null = $state(null); let src_browseResults: Manga[] = $state([]); @@ -814,7 +756,7 @@ tag_fanOutAbort?.abort(); src_abortCtrl?.abort(); sourceCacheAbort?.abort(); - disc_abortCtrl?.abort(); + srch_abortCtrl?.abort(); }); @@ -903,31 +845,31 @@
{#if !kw_query.trim()} - {#if disc_loading && disc_results.length === 0} -
+ {#if srch_loading && srch_results.length === 0} +
{#each Array(24) as _, i (i)}
{/each}
- {:else if disc_results.length > 0} -
- Popular right now + {:else if srch_results.length > 0} +
+ Popular right now
-
- {#each disc_results as m (m.id)} - {/each} - {#if disc_loading} + {#if srch_loading} {#each Array(6) as _, i (i)}
{/each} @@ -950,19 +892,19 @@ {/if} {:else} {#if kw_flatResults.length > 0} -
- {kw_flatResults.length} result{kw_flatResults.length !== 1 ? "s" : ""} +
+ {kw_flatResults.length} result{kw_flatResults.length !== 1 ? "s" : ""}
-
+
{#each kw_flatResults.slice(0, store.settings.renderLimit ?? 48) as m (m.id)} - @@ -974,7 +916,7 @@ {/if}
{:else if kw_anyLoading} -
+
{#each Array(12) as _, i (i)}
{/each} @@ -1110,7 +1052,7 @@ {:else if tag_mergedResults.length > 0}
{#each tag_mergedResults as m (m.id)} -