mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 01:39:56 -05:00
Feat: Longstrip Viewer(s) & Lag Improvements
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { onMount, onDestroy, untrack } from "svelte";
|
||||
import {
|
||||
X, BookmarkSimple, ArrowSquareOut, Play, CircleNotch,
|
||||
Books, CaretDown, FolderSimplePlus, Folder, LinkSimpleHorizontalBreak, Image,
|
||||
@@ -16,17 +16,13 @@
|
||||
import { addToast } from "$lib/state/notifications.svelte";
|
||||
import {
|
||||
seriesState,
|
||||
setPreviewManga, setActiveManga, openReader, addBookmark,
|
||||
setPreviewManga, addBookmark, openReaderForChapter,
|
||||
} from "$lib/state/series.svelte";
|
||||
import { app } from "$lib/state/app.svelte";
|
||||
import type { Manga, Chapter, Category } from "$lib/types";
|
||||
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte'
|
||||
|
||||
import ModalBlur from '$lib/components/shared/ui/ModalBlur.svelte';
|
||||
|
||||
let manga: Manga | null = $state(null);
|
||||
let chapters: Chapter[] = $state([]);
|
||||
let loadingDetail = $state(false);
|
||||
let loadingChapters = $state(false);
|
||||
let togglingLib = $state(false);
|
||||
let descExpanded = $state(false);
|
||||
let folderOpen = $state(false);
|
||||
@@ -44,8 +40,6 @@
|
||||
let loadingLinkList = $state(false);
|
||||
let coverPickerOpen = $state(false);
|
||||
|
||||
let originNavPage = app.navPage;
|
||||
|
||||
const linkedIds = $derived(
|
||||
seriesState.previewManga
|
||||
? (settingsState.settings.mangaLinks?.[seriesState.previewManga.id] ?? [])
|
||||
@@ -57,6 +51,9 @@
|
||||
);
|
||||
|
||||
const displayManga = $derived(manga ?? seriesState.previewManga);
|
||||
const mangaId = $derived(seriesState.previewManga?.id ?? null);
|
||||
const chapters = $derived(mangaId != null ? seriesState.chaptersFor(mangaId) : []);
|
||||
const loadingChapters = $derived(mangaId != null ? seriesState.isLoadingChapters(mangaId) : false);
|
||||
const totalCount = $derived(chapters.length);
|
||||
const readCount = $derived(chapters.filter((c) => c.read).length);
|
||||
const unreadCount = $derived(totalCount - readCount);
|
||||
@@ -113,16 +110,12 @@
|
||||
return `Continue · Ch.${ch.chapterNumber}${resumePage ? ` p.${resumePage}` : ""}`;
|
||||
});
|
||||
|
||||
|
||||
let detailAbort: AbortController | null = null;
|
||||
let chapterAbort: AbortController | null = null;
|
||||
|
||||
let detailAbort: AbortController | null = null;
|
||||
|
||||
function close() {
|
||||
detailAbort?.abort();
|
||||
chapterAbort?.abort();
|
||||
setPreviewManga(null);
|
||||
manga = null; chapters = []; descExpanded = false;
|
||||
manga = null; descExpanded = false;
|
||||
folderOpen = false; creatingFolder = false; newFolderName = ""; fetchError = null;
|
||||
}
|
||||
|
||||
@@ -153,12 +146,15 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const shouldAutoLink = settingsState.settings.autoLinkOnOpen;
|
||||
const focal = seriesState.previewManga;
|
||||
if (focal) {
|
||||
originNavPage = app.navPage;
|
||||
load(focal.id);
|
||||
if (!focal) return;
|
||||
|
||||
untrack(() => {
|
||||
loadDetail(focal.id);
|
||||
seriesState.loadChapters(focal.id);
|
||||
loadCategories(focal.id);
|
||||
|
||||
const shouldAutoLink = settingsState.settings.autoLinkOnOpen;
|
||||
if (shouldAutoLink) {
|
||||
if (allMangaForLink.length) {
|
||||
autoLinkLibrary(focal, allMangaForLink)
|
||||
@@ -166,71 +162,48 @@
|
||||
} else {
|
||||
loadingLinkList = true;
|
||||
getAdapter().getMangaList({})
|
||||
.then((d) => {
|
||||
allMangaForLink = d.items;
|
||||
return autoLinkLibrary(focal, d.items);
|
||||
})
|
||||
.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 function load(id: number) {
|
||||
detailAbort?.abort(); chapterAbort?.abort();
|
||||
const dCtrl = new AbortController(), cCtrl = new AbortController();
|
||||
detailAbort = dCtrl; chapterAbort = cCtrl;
|
||||
async function loadDetail(id: number) {
|
||||
detailAbort?.abort();
|
||||
const ctrl = new AbortController();
|
||||
detailAbort = ctrl;
|
||||
manga = seriesState.previewManga as Manga;
|
||||
chapters = []; descExpanded = false; fetchError = null;
|
||||
loadingDetail = true; loadingChapters = true;
|
||||
descExpanded = false; fetchError = null;
|
||||
loadingDetail = true;
|
||||
|
||||
(async (): Promise<Manga> => {
|
||||
const key = CACHE_KEYS.MANGA(id);
|
||||
if (cache.has(key)) return cache.get(key, () => Promise.resolve(seriesState.previewManga as Manga)) as Promise<Manga>;
|
||||
try {
|
||||
return await getAdapter().fetchManga(String(id), dCtrl.signal);
|
||||
} catch (e: any) {
|
||||
if (e?.name === "AbortError") throw e;
|
||||
const local = await getAdapter().getManga(String(id), dCtrl.signal);
|
||||
if (local) return local;
|
||||
throw new Error("Could not load manga details");
|
||||
const key = CACHE_KEYS.MANGA(id);
|
||||
try {
|
||||
let fullManga: Manga;
|
||||
if (cache.has(key)) {
|
||||
fullManga = await (cache.get(key, () => Promise.resolve(seriesState.previewManga as Manga)) as Promise<Manga>);
|
||||
} else {
|
||||
try {
|
||||
fullManga = await getAdapter().fetchManga(String(id), ctrl.signal);
|
||||
} catch (e: any) {
|
||||
if (e?.name === "AbortError") return;
|
||||
const local = await getAdapter().getManga(String(id), ctrl.signal);
|
||||
if (local) fullManga = local;
|
||||
else throw new Error("Could not load manga details");
|
||||
}
|
||||
if (!cache.has(key)) cache.get(key, () => Promise.resolve(fullManga));
|
||||
}
|
||||
})()
|
||||
.then((fullManga) => {
|
||||
if (dCtrl.signal.aborted) return;
|
||||
if (!cache.has(CACHE_KEYS.MANGA(id)))
|
||||
cache.get(CACHE_KEYS.MANGA(id), () => Promise.resolve(fullManga));
|
||||
manga = fullManga; loadingDetail = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e?.name === "AbortError") return;
|
||||
manga = seriesState.previewManga as Manga;
|
||||
fetchError = "Could not load full details — showing cached data";
|
||||
loadingDetail = false;
|
||||
});
|
||||
|
||||
getAdapter().getChapters(String(id), cCtrl.signal)
|
||||
.then(async (nodes) => {
|
||||
if (cCtrl.signal.aborted) return;
|
||||
let sorted = [...nodes].sort((a, b) => a.sourceOrder - b.sourceOrder);
|
||||
if (sorted.length === 0) {
|
||||
try {
|
||||
const fetched = await getAdapter().fetchChapters(String(id), cCtrl.signal);
|
||||
if (!cCtrl.signal.aborted)
|
||||
sorted = [...fetched].sort((a, b) => a.sourceOrder - b.sourceOrder);
|
||||
} catch (e: any) {
|
||||
if (e?.name === "AbortError") return;
|
||||
}
|
||||
}
|
||||
if (!cCtrl.signal.aborted) {
|
||||
chapters = sorted;
|
||||
if (sorted.length > 0) checkAndMarkCompleted(id, sorted);
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { if (!cCtrl.signal.aborted) loadingChapters = false; });
|
||||
if (ctrl.signal.aborted) return;
|
||||
manga = fullManga;
|
||||
} catch (e: any) {
|
||||
if (e?.name === "AbortError") return;
|
||||
manga = seriesState.previewManga as Manga;
|
||||
fetchError = "Could not load full details — showing cached data";
|
||||
} finally {
|
||||
if (!ctrl.signal.aborted) loadingDetail = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleLibrary() {
|
||||
@@ -258,58 +231,68 @@
|
||||
|
||||
function openSeriesDetail() {
|
||||
if (!displayManga) return;
|
||||
setActiveManga(displayManga);
|
||||
app.setNavPage(originNavPage);
|
||||
goto(`/series/${displayManga.id}`);
|
||||
close();
|
||||
}
|
||||
|
||||
function loadCategories(mangaId: number) {
|
||||
function handleRead() {
|
||||
if (!continueChapter || !displayManga) return;
|
||||
const { ch, type, resumePage } = continueChapter;
|
||||
if (type === "continue" && resumePage && resumePage > 1) {
|
||||
const existing = seriesState.bookmarks.find((b) => b.chapterId === ch.id);
|
||||
if (!existing || existing.pageNumber < resumePage) {
|
||||
addBookmark({
|
||||
mangaId: displayManga.id,
|
||||
mangaTitle: displayManga.title,
|
||||
thumbnailUrl: displayManga.thumbnailUrl,
|
||||
chapterId: ch.id,
|
||||
chapterName: ch.name,
|
||||
pageNumber: resumePage,
|
||||
});
|
||||
}
|
||||
}
|
||||
openReaderForChapter(ch, displayManga);
|
||||
close();
|
||||
}
|
||||
|
||||
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 === mangaId));
|
||||
mangaCategories = allCategories.filter((c) => c.mangas?.nodes?.some((m) => m.id === id));
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => { catsLoading = false; });
|
||||
}
|
||||
|
||||
async function checkAndMarkCompleted(mangaId: number, chaps: Chapter[]) {
|
||||
const mangaStatus = (manga ?? displayManga)?.status;
|
||||
const isOngoing = mangaStatus === "ONGOING";
|
||||
async function checkAndMarkCompleted(id: number, chaps: Chapter[]) {
|
||||
const isOngoing = (manga ?? displayManga)?.status === "ONGOING";
|
||||
if (!chaps.length || isOngoing) return;
|
||||
|
||||
const allRead = chaps.every((c) => c.read);
|
||||
const completed = allCategories.find((c) => c.name === "Completed");
|
||||
if (!completed) return;
|
||||
|
||||
const inCompleted = mangaCategories.some((c) => c.id === completed.id);
|
||||
if (allRead && !inCompleted) {
|
||||
await getAdapter().updateMangaCategories(String(mangaId), [completed.id], []).catch(console.error);
|
||||
await getAdapter().updateMangaCategories(String(id), [completed.id], []).catch(console.error);
|
||||
mangaCategories = [...mangaCategories, completed];
|
||||
} else if (!allRead && inCompleted) {
|
||||
await getAdapter().updateMangaCategories(String(mangaId), [], [completed.id]).catch(console.error);
|
||||
await getAdapter().updateMangaCategories(String(id), [], [completed.id]).catch(console.error);
|
||||
mangaCategories = mangaCategories.filter((c) => c.id !== completed.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleCategory(cat: Category) {
|
||||
if (!seriesState.previewManga) return;
|
||||
const mangaId = seriesState.previewManga.id;
|
||||
const inCat = mangaCategories.some((c) => c.id === cat.id);
|
||||
await getAdapter().updateMangaCategories(
|
||||
String(mangaId),
|
||||
inCat ? [] : [cat.id],
|
||||
inCat ? [cat.id] : [],
|
||||
).catch(console.error);
|
||||
const id = seriesState.previewManga.id;
|
||||
const inCat = mangaCategories.some((c) => c.id === cat.id);
|
||||
await getAdapter().updateMangaCategories(String(id), inCat ? [] : [cat.id], inCat ? [cat.id] : []).catch(console.error);
|
||||
if (!inCat && !inLibrary) {
|
||||
await getAdapter().addToLibrary(String(mangaId)).catch(console.error);
|
||||
await getAdapter().addToLibrary(String(id)).catch(console.error);
|
||||
if (manga) manga = { ...manga, inLibrary: true };
|
||||
cache.clear(CACHE_KEYS.LIBRARY);
|
||||
}
|
||||
mangaCategories = inCat
|
||||
? mangaCategories.filter((c) => c.id !== cat.id)
|
||||
: [...mangaCategories, cat];
|
||||
mangaCategories = inCat ? mangaCategories.filter((c) => c.id !== cat.id) : [...mangaCategories, cat];
|
||||
}
|
||||
|
||||
async function handleFolderCreate() {
|
||||
@@ -349,7 +332,6 @@
|
||||
onDestroy(() => {
|
||||
window.removeEventListener("keydown", onKey);
|
||||
detailAbort?.abort();
|
||||
chapterAbort?.abort();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -548,24 +530,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if continueChapter}
|
||||
<button class="read-btn" onclick={() => {
|
||||
const { ch, type, resumePage } = continueChapter!;
|
||||
if (type === "continue" && resumePage && resumePage > 1) {
|
||||
const existing = seriesState.bookmarks.find((b) => b.chapterId === ch.id);
|
||||
if (!existing || existing.pageNumber < resumePage) {
|
||||
addBookmark({
|
||||
mangaId: displayManga!.id,
|
||||
mangaTitle: displayManga!.title,
|
||||
thumbnailUrl: displayManga!.thumbnailUrl,
|
||||
chapterId: ch.id,
|
||||
chapterName: ch.name,
|
||||
pageNumber: resumePage,
|
||||
});
|
||||
}
|
||||
}
|
||||
openReader(ch, chapters, displayManga);
|
||||
close();
|
||||
}}>
|
||||
<button class="read-btn" onclick={handleRead}>
|
||||
<Play size={12} weight="fill" />{continueLabel}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -676,8 +641,6 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.backdrop {
|
||||
position: fixed; inset: 0;
|
||||
|
||||
Reference in New Issue
Block a user