mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Feat: Implemented Series-Link
This commit is contained in:
@@ -4,13 +4,12 @@
|
||||
import { gql, thumbUrl } from "../../lib/client";
|
||||
import { GET_SOURCES, FETCH_SOURCE_MANGA, UPDATE_MANGA } from "../../lib/queries";
|
||||
import { cache, CACHE_KEYS } from "../../lib/cache";
|
||||
import { dedupeSources, dedupeMangaByTitle, dedupeMangaById, groupDuplicates, normalizeTitle } from "../../lib/util";
|
||||
import { dedupeSources, dedupeMangaByTitle, dedupeMangaById } from "../../lib/util";
|
||||
import { settings, previewManga, activeSource, addFolder, assignMangaToFolder } from "../../store";
|
||||
import type { Manga, Source } from "../../lib/types";
|
||||
import ContextMenu from "../shared/ContextMenu.svelte";
|
||||
import type { MenuEntry } from "../shared/ContextMenu.svelte";
|
||||
import SourceBrowse from "../sources/SourceBrowse.svelte";
|
||||
import MangaPreview from "../shared/MangaPreview.svelte";
|
||||
|
||||
// ── Config ────────────────────────────────────────────────────────────────────
|
||||
const GENRE_TABS = ["All", "Action", "Romance", "Fantasy", "Comedy", "Drama", "Horror", "Sci-Fi", "Adventure", "Thriller"];
|
||||
@@ -57,39 +56,13 @@
|
||||
// Context menu
|
||||
let ctx: { x: number; y: number; manga: Manga } | null = null;
|
||||
|
||||
// Raw pool of ALL items before dedup — used to find alternates for the preview switcher.
|
||||
// Keyed by normalised title → array of Manga with that title from different sources.
|
||||
let rawPool = new Map<string, Manga[]>();
|
||||
|
||||
function addToPool(items: Manga[]) {
|
||||
for (const m of items) {
|
||||
const k = normalizeTitle(m.title);
|
||||
if (!rawPool.has(k)) rawPool.set(k, []);
|
||||
const group = rawPool.get(k)!;
|
||||
if (!group.some(x => x.id === m.id)) group.push(m);
|
||||
}
|
||||
}
|
||||
|
||||
// Get alternates (other source variants) for a given manga, excluding itself
|
||||
function getAlternates(m: Manga): Manga[] {
|
||||
const k = normalizeTitle(m.title);
|
||||
return (rawPool.get(k) ?? []).filter(x => x.id !== m.id);
|
||||
}
|
||||
|
||||
// Open preview with alternates pre-computed
|
||||
let previewAlternates: Manga[] = [];
|
||||
function openPreview(m: Manga) {
|
||||
previewAlternates = getAlternates(m);
|
||||
previewManga.set(m);
|
||||
}
|
||||
|
||||
// ── Derived ───────────────────────────────────────────────────────────────────
|
||||
$: visibleGrid = genreResults.get(currentGenre) ?? [];
|
||||
$: isLoading = genreLoading || (currentGenre === "All" && loadingLib);
|
||||
|
||||
// ── Dedup helper — always apply id first then title ───────────────────────────
|
||||
function dedup(items: Manga[]): Manga[] {
|
||||
return dedupeMangaByTitle(dedupeMangaById(items));
|
||||
return dedupeMangaByTitle(dedupeMangaById(items), $settings.mangaLinks);
|
||||
}
|
||||
|
||||
// ── Concurrent fan-out — conservative concurrency keeps connections free ──────
|
||||
@@ -113,7 +86,6 @@
|
||||
batchTimer = setInterval(() => {
|
||||
if (batchAccum.size === 0) return;
|
||||
for (const [genre, incoming] of batchAccum) {
|
||||
addToPool(incoming);
|
||||
const current = genreResults.get(genre) ?? [];
|
||||
genreResults.set(genre, dedup([...current, ...incoming]).slice(0, GRID_LIMIT));
|
||||
}
|
||||
@@ -266,11 +238,10 @@
|
||||
const lang = $settings.preferredExtensionLang || "en";
|
||||
|
||||
// Local library — populates "All" tab
|
||||
cache.get(CACHE_KEYS.LIBRARY, () =>
|
||||
cache.get(CACHE_KEYS.DISCOVER, () =>
|
||||
gql<{ mangas: { nodes: Manga[] } }>(EXPLORE_ALL_MANGA).then(d => d.mangas.nodes)
|
||||
).then(m => {
|
||||
allManga = dedupeMangaById(m);
|
||||
addToPool(allManga);
|
||||
genreResults.set("All", dedup(allManga).slice(0, GRID_LIMIT));
|
||||
genreResults = new Map(genreResults);
|
||||
}).catch(e => { console.error(e); loadError = true; })
|
||||
@@ -342,7 +313,7 @@
|
||||
{#each visibleGrid as m (m.id)}
|
||||
<button
|
||||
class="manga-card"
|
||||
on:click={() => openPreview(m)}
|
||||
on:click={() => previewManga.set(m)}
|
||||
on:contextmenu={(e) => openCtx(e, m)}
|
||||
>
|
||||
<div class="cover-wrap">
|
||||
@@ -372,8 +343,6 @@
|
||||
<ContextMenu x={ctx.x} y={ctx.y} items={buildCtxItems(ctx.manga)} onClose={() => ctx = null} />
|
||||
{/if}
|
||||
|
||||
<MangaPreview alternates={previewAlternates} />
|
||||
|
||||
<style>
|
||||
.root { display: flex; flex-direction: column; height: 100%; overflow: hidden; animation: fadeIn 0.14s ease both; }
|
||||
|
||||
|
||||
@@ -33,15 +33,18 @@
|
||||
}
|
||||
|
||||
function fetchLibrary() {
|
||||
return cache.get(CACHE_KEYS.LIBRARY, () =>
|
||||
gql<{ mangas: { nodes: Manga[] } }>(GET_LIBRARY).then((d) => d.mangas.nodes)
|
||||
return cache.get(
|
||||
CACHE_KEYS.LIBRARY,
|
||||
() => gql<{ mangas: { nodes: Manga[] } }>(GET_LIBRARY).then((d) => d.mangas.nodes),
|
||||
DEFAULT_TTL_MS,
|
||||
CACHE_GROUPS.LIBRARY,
|
||||
);
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
fetchLibrary()
|
||||
.then((nodes) => {
|
||||
allManga = dedupeMangaByTitle(dedupeMangaById(nodes));
|
||||
allManga = dedupeMangaByTitle(dedupeMangaById(nodes), $settings.mangaLinks);
|
||||
error = null;
|
||||
})
|
||||
.catch((e) => error = e.message)
|
||||
@@ -52,7 +55,7 @@
|
||||
DEFAULT_TTL_MS,
|
||||
CACHE_GROUPS.LIBRARY,
|
||||
).then((nodes) => {
|
||||
allMangaUnfiltered = dedupeMangaByTitle(dedupeMangaById(nodes));
|
||||
allMangaUnfiltered = dedupeMangaByTitle(dedupeMangaById(nodes), $settings.mangaLinks);
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
@@ -136,8 +139,7 @@
|
||||
async function removeFromLibrary(manga: Manga) {
|
||||
await gql(UPDATE_MANGA, { id: manga.id, inLibrary: false }).catch(console.error);
|
||||
allManga = allManga.filter((m) => m.id !== manga.id);
|
||||
cache.clear(CACHE_KEYS.LIBRARY);
|
||||
cache.clearGroup(CACHE_GROUPS.LIBRARY);
|
||||
cache.clearGroup(CACHE_GROUPS.LIBRARY); // clears "library" + "all_manga_unfiltered" + notifies subscribers
|
||||
}
|
||||
|
||||
async function deleteAllDownloads(manga: Manga) {
|
||||
|
||||
Reference in New Issue
Block a user