Reading history {store.history.length} entries
diff --git a/src/shared/manga/MangaPreview.svelte b/src/shared/manga/MangaPreview.svelte
index a2a9efb..953eb7b 100644
--- a/src/shared/manga/MangaPreview.svelte
+++ b/src/shared/manga/MangaPreview.svelte
@@ -2,7 +2,7 @@
import { onMount, onDestroy } from "svelte";
import {
X, BookmarkSimple, ArrowSquareOut, Play, CircleNotch,
- Books, CaretDown, FolderSimplePlus, Folder, LinkSimpleHorizontalBreak,
+ Books, CaretDown, FolderSimplePlus, Folder, LinkSimpleHorizontalBreak, Image,
} from "phosphor-svelte";
import { gql } from "@api/client";
import Thumbnail from "@shared/manga/Thumbnail.svelte";
@@ -10,10 +10,14 @@
import { FETCH_MANGA, FETCH_CHAPTERS, UPDATE_MANGA, CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, ENQUEUE_CHAPTERS_DOWNLOAD } from "@api/mutations";
import { cache, CACHE_KEYS } from "@core/cache";
import {
- store, openReader, addToast, linkManga, unlinkManga,
+ store, openReader, addToast,
setPreviewManga, setActiveManga, setNavPage, setGenreFilter,
checkAndMarkCompleted as storeCheckAndMarkCompleted, addBookmark,
} from "@store/state.svelte";
+ import { resolvedCover } from "@features/series/lib/coverResolver";
+ import CoverPickerPanel from "@features/series/panels/CoverPickerPanel.svelte";
+ import SeriesLinkPanel from "@features/series/panels/SeriesLinkPanel.svelte";
+ import { autoLinkLibrary } from "@features/series/lib/autoLink";
import type { Manga, Chapter, Category } from "@types/index";
@@ -33,25 +37,18 @@
let fetchError: string | null = $state(null);
let folderRef: HTMLDivElement = $state() as HTMLDivElement;
-
let linkPickerOpen = $state(false);
- let linkSearch = $state("");
let allMangaForLink: Manga[] = $state([]);
let loadingLinkList = $state(false);
+ let coverPickerOpen = $state(false);
const linkedIds = $derived(
store.previewManga ? (store.settings.mangaLinks?.[store.previewManga.id] ?? []) : [],
);
- const linkPickerResults = $derived.by(() => {
- const others = allMangaForLink.filter((m) => m.id !== store.previewManga?.id);
- const q = linkSearch.trim().toLowerCase();
- const filtered = q ? others.filter((m) => m.title.toLowerCase().includes(q)) : others;
- const linked = filtered.filter((m) => linkedIds.includes(m.id));
- const rest = filtered.filter((m) => !linkedIds.includes(m.id)).slice(0, 30);
- return [...linked, ...rest];
- });
-
+ const hasCoverOverride = $derived(
+ !!store.settings.mangaPrefs?.[store.previewManga?.id ?? -1]?.coverUrl
+ );
const displayManga = $derived(manga ?? store.previewManga);
const totalCount = $derived(chapters.length);
@@ -128,7 +125,21 @@
}
async function openLinkPicker() {
- linkPickerOpen = true; linkSearch = "";
+ linkPickerOpen = true;
+ if (allMangaForLink.length) return;
+ loadingLinkList = true;
+ gql<{ mangas: { nodes: Manga[] } }>(GET_ALL_MANGA)
+ .then((d) => {
+ allMangaForLink = d.mangas.nodes;
+ })
+ .catch(console.error)
+ .finally(() => { loadingLinkList = false; });
+ }
+
+ function closeLinkPicker() { linkPickerOpen = false; }
+
+ async function openCoverPicker() {
+ coverPickerOpen = true;
if (allMangaForLink.length) return;
loadingLinkList = true;
gql<{ mangas: { nodes: Manga[] } }>(GET_ALL_MANGA)
@@ -137,18 +148,28 @@
.finally(() => { loadingLinkList = false; });
}
- function closeLinkPicker() { linkPickerOpen = false; linkSearch = ""; }
-
- function handleLink(other: Manga) {
- if (!store.previewManga) return;
- if (linkedIds.includes(other.id)) unlinkManga(store.previewManga.id, other.id);
- else linkManga(store.previewManga.id, other.id);
- }
-
$effect(() => {
- if (store.previewManga) {
- load(store.previewManga.id);
- loadCategories(store.previewManga.id);
+ const shouldAutoLink = store.settings.autoLinkOnOpen;
+ const focal = store.previewManga;
+ if (focal) {
+ load(focal.id);
+ loadCategories(focal.id);
+ if (shouldAutoLink) {
+ if (allMangaForLink.length) {
+ autoLinkLibrary(focal, allMangaForLink)
+ .then(n => { if (n > 0) addToast({ kind: "success", title: "Series linked", body: `${n} new link${n === 1 ? "" : "s"} found` }); });
+ } else {
+ loadingLinkList = true;
+ gql<{ mangas: { nodes: Manga[] } }>(GET_ALL_MANGA)
+ .then((d) => {
+ allMangaForLink = d.mangas.nodes;
+ return autoLinkLibrary(focal, d.mangas.nodes);
+ })
+ .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; });
+ }
+ }
}
});
@@ -343,7 +364,7 @@
-
+
{#if loadingDetail}
@@ -435,6 +456,17 @@
{linkedIds.length > 0 ? `Series Link (${linkedIds.length})` : "Series Link"}
+
+
+
+
+
+ Cover Image
+
@@ -632,53 +664,20 @@
{/if}
-{#if linkPickerOpen}
-
{ if (e.target === e.currentTarget) closeLinkPicker(); }}
- onkeydown={(e) => e.key === "Escape" && closeLinkPicker()}
- >
-
-
-
- Mark two manga as the same series so duplicates are merged in search.
- Click a linked entry again to unlink.
-
-
-
-
-
- {#if loadingLinkList}
-
Loading…
- {:else if linkPickerResults.length === 0}
-
No results
- {:else}
- {#each linkPickerResults as m (m.id)}
- {@const isLinked = linkedIds.includes(m.id)}
-
handleLink(m)}>
-
-
- {m.title}
- {#if m.source?.displayName}
- {m.source.displayName}
- {/if}
-
- {isLinked ? "✓ Linked" : "Link"}
-
- {/each}
- {/if}
-
-
-
+{#if linkPickerOpen && store.previewManga}
+
+{/if}
+
+{#if coverPickerOpen && store.previewManga}
+
{ coverPickerOpen = false; }}
+ />
{/if}