mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Fix: SeriesDetail passing Incorrect Args to Reader
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
import { gql, thumbUrl } from "../../lib/client";
|
import { gql, thumbUrl } from "../../lib/client";
|
||||||
import { GET_SOURCES, FETCH_SOURCE_MANGA, UPDATE_MANGA, GET_CATEGORIES, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES } from "../../lib/queries";
|
import { GET_SOURCES, FETCH_SOURCE_MANGA, UPDATE_MANGA, GET_CATEGORIES, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES } from "../../lib/queries";
|
||||||
import { cache, CACHE_KEYS } from "../../lib/cache";
|
import { cache, CACHE_KEYS } from "../../lib/cache";
|
||||||
import { dedupeSources, dedupeMangaByTitle, dedupeMangaById } from "../../lib/util";
|
import { dedupeSources, dedupeMangaByTitle, dedupeMangaById, isNsfwManga } from "../../lib/util";
|
||||||
import { store, setPreviewManga, clearDiscoverCache } from "../../store/state.svelte";
|
import { store, setPreviewManga, clearDiscoverCache } from "../../store/state.svelte";
|
||||||
import type { Manga, Source, Category } from "../../lib/types";
|
import type { Manga, Source, Category } from "../../lib/types";
|
||||||
import ContextMenu from "../shared/ContextMenu.svelte";
|
import ContextMenu from "../shared/ContextMenu.svelte";
|
||||||
@@ -60,7 +60,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function filterOut(mangas: Manga[]): Manga[] {
|
function filterOut(mangas: Manga[]): Manga[] {
|
||||||
return dedup(mangas.filter(m => !m.inLibrary && !store.discoverLibraryIds.has(m.id)));
|
return dedup(mangas.filter(m => {
|
||||||
|
if (m.inLibrary || store.discoverLibraryIds.has(m.id)) return false;
|
||||||
|
if (!store.settings.showNsfw && isNsfwManga(m)) return false;
|
||||||
|
return true;
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotatedSources(): Source[] {
|
function rotatedSources(): Source[] {
|
||||||
@@ -183,7 +187,9 @@
|
|||||||
);
|
);
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
|
|
||||||
const local = dedup(d.mangas.nodes);
|
const local = dedup(
|
||||||
|
d.mangas.nodes.filter(m => store.settings.showNsfw || !isNsfwManga(m))
|
||||||
|
);
|
||||||
store.discoverCache.set(localKey, local);
|
store.discoverCache.set(localKey, local);
|
||||||
genreResults.set(genre, local.slice(0, GRID_LIMIT));
|
genreResults.set(genre, local.slice(0, GRID_LIMIT));
|
||||||
genreResults = new Map(genreResults);
|
genreResults = new Map(genreResults);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { gql, thumbUrl } from "../../lib/client";
|
import { gql, thumbUrl } from "../../lib/client";
|
||||||
import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER } from "../../lib/queries";
|
import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER } from "../../lib/queries";
|
||||||
import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "../../lib/cache";
|
import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "../../lib/cache";
|
||||||
import { dedupeMangaById, dedupeMangaByTitle } from "../../lib/util";
|
import { dedupeMangaById, dedupeMangaByTitle, isNsfwManga } from "../../lib/util";
|
||||||
import { store, setLibraryFilter, checkAndMarkCompleted as storeCheckAndMarkCompleted, updateSettings, setCategories } from "../../store/state.svelte";
|
import { store, setLibraryFilter, checkAndMarkCompleted as storeCheckAndMarkCompleted, updateSettings, setCategories } from "../../store/state.svelte";
|
||||||
import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter } from "../../store/state.svelte";
|
import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter } from "../../store/state.svelte";
|
||||||
import type { Manga, Category, Chapter } from "../../lib/types";
|
import type { Manga, Category, Chapter } from "../../lib/types";
|
||||||
@@ -319,10 +319,15 @@
|
|||||||
items = categoryMangaMap.get(Number(store.libraryFilter)) ?? [];
|
items = categoryMangaMap.get(Number(store.libraryFilter)) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Text search
|
// 2. NSFW filter — always applied before text search or sort
|
||||||
|
if (!store.settings.showNsfw) {
|
||||||
|
items = items.filter(m => !isNsfwManga(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Text search
|
||||||
if (q) items = items.filter(m => m.title.toLowerCase().includes(q));
|
if (q) items = items.filter(m => m.title.toLowerCase().includes(q));
|
||||||
|
|
||||||
// 3. Status filter
|
// 4. Status filter
|
||||||
if (status !== "ALL") {
|
if (status !== "ALL") {
|
||||||
items = items.filter(m => {
|
items = items.filter(m => {
|
||||||
const s = m.status?.toUpperCase().replace(/\s+/g, "_") ?? "UNKNOWN";
|
const s = m.status?.toUpperCase().replace(/\s+/g, "_") ?? "UNKNOWN";
|
||||||
@@ -330,7 +335,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Sort
|
// 5. Sort
|
||||||
const recentlyReadMap = new Map<number, number>();
|
const recentlyReadMap = new Map<number, number>();
|
||||||
if (mode === "recentlyRead") {
|
if (mode === "recentlyRead") {
|
||||||
for (const h of store.history) {
|
for (const h of store.history) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { gql, thumbUrl } from "../../lib/client";
|
import { gql, thumbUrl } from "../../lib/client";
|
||||||
import { GET_SOURCES, FETCH_SOURCE_MANGA } from "../../lib/queries";
|
import { GET_SOURCES, FETCH_SOURCE_MANGA } from "../../lib/queries";
|
||||||
import { cache, CACHE_KEYS, getPageSet } from "../../lib/cache";
|
import { cache, CACHE_KEYS, getPageSet } from "../../lib/cache";
|
||||||
import { dedupeSources, dedupeMangaById, dedupeMangaByTitle } from "../../lib/util";
|
import { dedupeSources, dedupeMangaById, dedupeMangaByTitle, isNsfwManga } from "../../lib/util";
|
||||||
import { store, setSearchPrefill, setPreviewManga } from "../../store/state.svelte";
|
import { store, setSearchPrefill, setPreviewManga } from "../../store/state.svelte";
|
||||||
import type { Manga, Source } from "../../lib/types";
|
import type { Manga, Source } from "../../lib/types";
|
||||||
|
|
||||||
@@ -146,8 +146,11 @@
|
|||||||
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 = store.settings.showNsfw
|
||||||
|
? d.fetchSourceManga.mangas
|
||||||
|
: d.fetchSourceManga.mangas.filter((m) => !isNsfwManga(m));
|
||||||
kw_results = kw_results.map((r) =>
|
kw_results = kw_results.map((r) =>
|
||||||
r.source.id === src.id ? { ...r, mangas: d.fetchSourceManga.mangas, loading: false } : r,
|
r.source.id === src.id ? { ...r, mangas, loading: false } : r,
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (ctrl.signal.aborted || e?.name === "AbortError") return;
|
if (ctrl.signal.aborted || e?.name === "AbortError") return;
|
||||||
@@ -243,7 +246,8 @@
|
|||||||
ctrl.signal,
|
ctrl.signal,
|
||||||
).then((d) => {
|
).then((d) => {
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
tag_localResults = d.mangas.nodes;
|
const nsfwFilter = (m: Manga) => store.settings.showNsfw || !isNsfwManga(m);
|
||||||
|
tag_localResults = d.mangas.nodes.filter(nsfwFilter);
|
||||||
tag_totalCount = d.mangas.totalCount;
|
tag_totalCount = d.mangas.totalCount;
|
||||||
tag_localHasNext = d.mangas.pageInfo.hasNextPage;
|
tag_localHasNext = d.mangas.pageInfo.hasNextPage;
|
||||||
tag_localOffset = (store.settings.renderLimit ?? 48);
|
tag_localOffset = (store.settings.renderLimit ?? 48);
|
||||||
@@ -279,9 +283,10 @@
|
|||||||
ps.add(1);
|
ps.add(1);
|
||||||
tag_srcNextPage.set(src.id, result.hasNextPage ? 2 : -1);
|
tag_srcNextPage.set(src.id, result.hasNextPage ? 2 : -1);
|
||||||
tag_srcNextPage = new Map(tag_srcNextPage);
|
tag_srcNextPage = new Map(tag_srcNextPage);
|
||||||
const matching = activeTags.length > 1
|
const matching = (activeTags.length > 1
|
||||||
? result.mangas.filter((m) => matchesAllTags(m, activeTags))
|
? result.mangas.filter((m) => matchesAllTags(m, activeTags))
|
||||||
: result.mangas;
|
: result.mangas
|
||||||
|
).filter((m) => store.settings.showNsfw || !isNsfwManga(m));
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0) {
|
||||||
tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks);
|
tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks);
|
||||||
tag_loadingSourceSearch = false;
|
tag_loadingSourceSearch = false;
|
||||||
@@ -304,7 +309,8 @@
|
|||||||
ctrl.signal,
|
ctrl.signal,
|
||||||
);
|
);
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
tag_localResults = [...tag_localResults, ...d.mangas.nodes];
|
const nsfwFilter = (m: Manga) => store.settings.showNsfw || !isNsfwManga(m);
|
||||||
|
tag_localResults = [...tag_localResults, ...d.mangas.nodes.filter(nsfwFilter)];
|
||||||
tag_localHasNext = d.mangas.pageInfo.hasNextPage;
|
tag_localHasNext = d.mangas.pageInfo.hasNextPage;
|
||||||
tag_localOffset += (store.settings.renderLimit ?? 48);
|
tag_localOffset += (store.settings.renderLimit ?? 48);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -340,9 +346,10 @@
|
|||||||
ps.add(page);
|
ps.add(page);
|
||||||
tag_srcNextPage.set(src.id, result.hasNextPage ? page + 1 : -1);
|
tag_srcNextPage.set(src.id, result.hasNextPage ? page + 1 : -1);
|
||||||
tag_srcNextPage = new Map(tag_srcNextPage);
|
tag_srcNextPage = new Map(tag_srcNextPage);
|
||||||
const matching = tag_activeTags.length > 1
|
const matching = (tag_activeTags.length > 1
|
||||||
? result.mangas.filter((m) => matchesAllTags(m, tag_activeTags))
|
? result.mangas.filter((m) => matchesAllTags(m, tag_activeTags))
|
||||||
: result.mangas;
|
: result.mangas
|
||||||
|
).filter((m) => store.settings.showNsfw || !isNsfwManga(m));
|
||||||
if (matching.length > 0) {
|
if (matching.length > 0) {
|
||||||
tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks);
|
tag_sourceResults = dedupeMangaByTitle(dedupeMangaById([...tag_sourceResults, ...matching]), store.settings.mangaLinks);
|
||||||
}
|
}
|
||||||
@@ -422,7 +429,10 @@
|
|||||||
FETCH_SOURCE_MANGA, { source: src.id, type, page, query: q ?? null }, ctrl.signal,
|
FETCH_SOURCE_MANGA, { source: src.id, type, page, query: q ?? null }, ctrl.signal,
|
||||||
);
|
);
|
||||||
if (ctrl.signal.aborted) return;
|
if (ctrl.signal.aborted) return;
|
||||||
src_browseResults = page === 1 ? d.fetchSourceManga.mangas : [...src_browseResults, ...d.fetchSourceManga.mangas];
|
const incoming = store.settings.showNsfw
|
||||||
|
? d.fetchSourceManga.mangas
|
||||||
|
: d.fetchSourceManga.mangas.filter((m) => !isNsfwManga(m));
|
||||||
|
src_browseResults = page === 1 ? incoming : [...src_browseResults, ...incoming];
|
||||||
src_hasNextPage = d.fetchSourceManga.hasNextPage;
|
src_hasNextPage = d.fetchSourceManga.hasNextPage;
|
||||||
src_currentPage = page;
|
src_currentPage = page;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
@@ -87,6 +87,15 @@
|
|||||||
}
|
}
|
||||||
return sortDir === "desc" ? base.reverse() : base;
|
return sortDir === "desc" ? base.reverse() : base;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chapter list in canonical reading order (ch1 -> ch2 -> ch3).
|
||||||
|
* Always passed to openReader so the Reader's idx-based prev/next
|
||||||
|
* navigation is direction-independent of the user's display sort.
|
||||||
|
*/
|
||||||
|
const chaptersAsc = $derived(
|
||||||
|
[...chapters].sort((a, b) => a.sourceOrder - b.sourceOrder)
|
||||||
|
);
|
||||||
const totalPages = $derived(Math.ceil(sortedChapters.length / CHAPTERS_PER_PAGE));
|
const totalPages = $derived(Math.ceil(sortedChapters.length / CHAPTERS_PER_PAGE));
|
||||||
const pageChapters = $derived(sortedChapters.slice((chapterPage - 1) * CHAPTERS_PER_PAGE, chapterPage * CHAPTERS_PER_PAGE));
|
const pageChapters = $derived(sortedChapters.slice((chapterPage - 1) * CHAPTERS_PER_PAGE, chapterPage * CHAPTERS_PER_PAGE));
|
||||||
const readCount = $derived(chapters.filter(c => c.isRead).length);
|
const readCount = $derived(chapters.filter(c => c.isRead).length);
|
||||||
@@ -444,7 +453,7 @@
|
|||||||
<!-- Zone 3: Primary CTA + library action -->
|
<!-- Zone 3: Primary CTA + library action -->
|
||||||
<div class="cta-section">
|
<div class="cta-section">
|
||||||
{#if continueChapter}
|
{#if continueChapter}
|
||||||
<button class="read-btn" onclick={() => openReader(continueChapter!.chapter, sortedChapters)}>
|
<button class="read-btn" onclick={() => openReader(continueChapter!.chapter, chaptersAsc)}>
|
||||||
<Play size={12} weight="fill" />
|
<Play size={12} weight="fill" />
|
||||||
{continueChapter.type === "continue"
|
{continueChapter.type === "continue"
|
||||||
? `Continue · Ch.${continueChapter.chapter.chapterNumber}${(continueChapter.chapter.lastPageRead ?? 0) > 0 ? ` p.${continueChapter.chapter.lastPageRead}` : ""}`
|
? `Continue · Ch.${continueChapter.chapter.chapterNumber}${(continueChapter.chapter.lastPageRead ?? 0) > 0 ? ` p.${continueChapter.chapter.lastPageRead}` : ""}`
|
||||||
@@ -665,7 +674,7 @@
|
|||||||
{#each sortedChapters as ch, i}
|
{#each sortedChapters as ch, i}
|
||||||
{@const inProgress = !ch.isRead && (ch.lastPageRead ?? 0) > 0}
|
{@const inProgress = !ch.isRead && (ch.lastPageRead ?? 0) > 0}
|
||||||
<button class="grid-cell" class:read={ch.isRead} class:in-progress={inProgress}
|
<button class="grid-cell" class:read={ch.isRead} class:in-progress={inProgress}
|
||||||
onclick={() => openReader(ch, sortedChapters)}
|
onclick={() => openReader(ch, chaptersAsc)}
|
||||||
oncontextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, chapter: ch, idx: i }; }}
|
oncontextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, chapter: ch, idx: i }; }}
|
||||||
title={ch.name}>
|
title={ch.name}>
|
||||||
<span class="grid-cell-num">{ch.chapterNumber % 1 === 0 ? ch.chapterNumber.toFixed(0) : ch.chapterNumber}</span>
|
<span class="grid-cell-num">{ch.chapterNumber % 1 === 0 ? ch.chapterNumber.toFixed(0) : ch.chapterNumber}</span>
|
||||||
@@ -677,8 +686,8 @@
|
|||||||
{#each pageChapters as ch}
|
{#each pageChapters as ch}
|
||||||
{@const idxInSorted = sortedChapters.indexOf(ch)}
|
{@const idxInSorted = sortedChapters.indexOf(ch)}
|
||||||
<div role="button" tabindex="0" class="ch-row" class:read={ch.isRead}
|
<div role="button" tabindex="0" class="ch-row" class:read={ch.isRead}
|
||||||
onclick={() => openReader(ch, sortedChapters)}
|
onclick={() => openReader(ch, chaptersAsc)}
|
||||||
onkeydown={(e) => e.key === "Enter" && openReader(ch, sortedChapters)}
|
onkeydown={(e) => e.key === "Enter" && openReader(ch, chaptersAsc)}
|
||||||
oncontextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, chapter: ch, idx: idxInSorted }; }}>
|
oncontextmenu={(e) => { e.preventDefault(); ctx = { x: e.clientX, y: e.clientY, chapter: ch, idx: idxInSorted }; }}>
|
||||||
<div class="ch-left">
|
<div class="ch-left">
|
||||||
<span class="ch-name">{ch.name}</span>
|
<span class="ch-name">{ch.name}</span>
|
||||||
|
|||||||
@@ -5,6 +5,34 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
return clsx(inputs);
|
return clsx(inputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── NSFW genre filtering ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Genre tags that indicate adult/mature content.
|
||||||
|
* Checked case-insensitively against each manga's genre array.
|
||||||
|
* Extend this set if additional tags need to be covered.
|
||||||
|
*/
|
||||||
|
const NSFW_GENRE_TAGS = new Set([
|
||||||
|
"adult",
|
||||||
|
"mature",
|
||||||
|
"hentai",
|
||||||
|
"ecchi",
|
||||||
|
"erotica",
|
||||||
|
"pornographic",
|
||||||
|
"18+",
|
||||||
|
"smut",
|
||||||
|
"lemon",
|
||||||
|
"explicit",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the manga carries at least one genre tag that is considered
|
||||||
|
* adult/mature. Used to enforce the `showNsfw` setting across all views.
|
||||||
|
*/
|
||||||
|
export function isNsfwManga(manga: { genre?: string[] | null }): boolean {
|
||||||
|
return (manga.genre ?? []).some((g) => NSFW_GENRE_TAGS.has(g.toLowerCase().trim()));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Source deduplication ──────────────────────────────────────────────────────
|
// ── Source deduplication ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function dedupeSources(sources: Source[], preferredLang: string): Source[] {
|
export function dedupeSources(sources: Source[], preferredLang: string): Source[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user