From a8ad9034fc9b11cc6ccc52f6a92fd5ce0f2d63e7 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Tue, 9 Jun 2026 22:52:11 -0500 Subject: [PATCH] Fix: Reader Longstrip Bookmark + ProgressBar --- src/lib/components/reader/PageView.svelte | 273 +++++++++++++----- src/lib/components/reader/Reader.svelte | 71 +++-- .../components/reader/ReaderControls.svelte | 15 +- .../components/reader/ReaderOverlay.svelte | 20 +- .../reader/ReaderProgressBar.svelte | 37 ++- .../components/reader/lib/chapterLoader.ts | 17 +- src/lib/components/reader/lib/navigation.ts | 26 +- .../components/reader/lib/scrollHandler.ts | 75 +++-- src/lib/components/series/SeriesHeader.svelte | 15 +- .../shared/manga/MangaPreview.svelte | 2 +- 10 files changed, 407 insertions(+), 144 deletions(-) diff --git a/src/lib/components/reader/PageView.svelte b/src/lib/components/reader/PageView.svelte index 2f0163d..2b28c57 100644 --- a/src/lib/components/reader/PageView.svelte +++ b/src/lib/components/reader/PageView.svelte @@ -1,10 +1,11 @@
+
{#if isLoaded && src} (".strip-slot"); if (slot && img.naturalWidth > 0) { - slot.style.setProperty("--aspect", String(img.naturalWidth / img.naturalHeight)); + const aspect = img.naturalWidth / img.naturalHeight; + slot.style.setProperty("--aspect", String(aspect)); + aspectMap.set(gi, aspect); } }} /> @@ -490,11 +629,11 @@ {:else if style === "fade" && pageReady}
- {#await resolveUrl(readerState.pageUrls[readerState.pageNumber - 1], 999)} + {#if currentSrc} + Page {readerState.pageNumber} + {:else} - {:then src} - Page {readerState.pageNumber} - {/await} + {/if}
{:else if style === "double" && pageReady} @@ -502,11 +641,11 @@ {#if pageGroups.length}
{#each currentGroup as pg, i (pg)} - {#await resolveUrl(readerState.pageUrls[pg - 1], 999)} + {#if currentGroupSrcs[i]} + Page {pg} + {:else} - {:then src} - Page {pg} - {/await} + {/if} {/each}
{:else} @@ -518,11 +657,11 @@ {:else if pageReady}
- {#await resolveUrl(readerState.pageUrls[readerState.pageNumber - 1], 999)} + {#if currentSrc} + Page {readerState.pageNumber} + {:else} - {:then src} - Page {readerState.pageNumber} - {/await} + {/if}
{/if}
diff --git a/src/lib/components/reader/Reader.svelte b/src/lib/components/reader/Reader.svelte index c697ea8..8dfaccb 100644 --- a/src/lib/components/reader/Reader.svelte +++ b/src/lib/components/reader/Reader.svelte @@ -12,9 +12,10 @@ import { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "$lib/components/reader/lib/zoomHelpers"; import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader"; import { historyState } from "$lib/state/history.svelte"; + import { setPreviewManga } from "$lib/state/series.svelte"; import { getAdapter } from "$lib/request-manager"; import { setReading, clearReading } from "$lib/core/discord"; - import { revokeBlobUrl } from "$lib/core/cache/imageCache"; + import { revokeBlobUrl, cancelQueuedFetches, preloadBlobUrls } from "$lib/core/cache/imageCache"; import type { ReaderSettings } from "$lib/state/reader.svelte"; import ReaderControls from "$lib/components/reader/ReaderControls.svelte"; import PageView from "$lib/components/reader/PageView.svelte"; @@ -211,6 +212,36 @@ const startAtLast = () => { startAtLastPageRef.current = true; }; + function flatIndexForPage(page: number): number { + const chId = readerState.visibleChapterId ?? readerState.activeChapter?.id; + const chunks = readerState.stripChapters; + let offset = 0; + for (const chunk of chunks) { + if (chunk.chapterId === chId) return offset + Math.max(0, page - 1); + offset += chunk.urls.length; + } + return Math.max(0, page - 1); + } + + function primedJump(page: number, commit = true) { + if (useBlob && commit && style !== "longstrip") { + cancelQueuedFetches(); + const urls = readerState.pageUrls; + const lo = Math.max(0, page - 2); + const hi = Math.min(urls.length, page + 4); + preloadBlobUrls(urls.slice(lo, hi), 999); + } + jumpToPage( + page, + style, + lastPage, + style === "longstrip" ? (idx) => pageViewRef.scrollToFlatIndex(idx) : null, + stripToRender.reduce((s, c) => s + c.urls.length, 0), + readerState.visibleChapterId ?? readerState.activeChapter?.id ?? 0, + readerState.stripChapters, + ); + } + const goNext = $derived(rtl ? () => goBack(style, adjacent, startAtLast) : () => goForward(style, adjacent, lastPage, maybeMarkCurrentRead, startAtLast)); @@ -218,7 +249,6 @@ ? () => goForward(style, adjacent, lastPage, maybeMarkCurrentRead, startAtLast) : () => goBack(style, adjacent, startAtLast)); - // clear Discord presence and free page blob textures before closing function handleCloseReader() { clearReading().catch(() => {}); for (const url of readerState.pageUrls) revokeBlobUrl(url); @@ -232,13 +262,13 @@ goNext: () => goNext(), goPrev: () => goPrev(), closeReader: () => handleCloseReader(), - goToPage: (p) => jumpToPage(p, style, lastPage, containerEl), + goToPage: (p) => primedJump(p), lastPage: () => lastPage, adjustZoom: (d) => { captureZoomAnchor(containerEl, style, zoomAnchor); applySettings({ readerZoom: clampZoom(zoom + d) }); restoreZoomAnchor(containerEl, zoomAnchor); }, resetZoom: () => { captureZoomAnchor(containerEl, style, zoomAnchor); applySettings({ readerZoom: 1.0 }); restoreZoomAnchor(containerEl, zoomAnchor); }, cycleStyle: () => { const idx = PAGE_STYLES.indexOf(style); applySettings({ pageStyle: PAGE_STYLES[(idx + 1) % PAGE_STYLES.length] }); }, toggleDirection: () => applySettings({ readingDirection: rtl ? "ltr" : "rtl" }), - openSettings: () => { app.setSettingsOpen(true); }, + openSettings: () => { app.setSettingsOpen(true); }, toggleBookmark: () => toggleBookmark(displayChapter, readerState.pageNumber), toggleAutoScroll: () => { if (style === "longstrip") updateSettings({ autoScroll: !(settingsState.settings.autoScroll ?? false) }); }, toggleMarker: () => { @@ -325,7 +355,6 @@ } }); - // Separate from chapter load: also re-fires when idle splash dismisses so presence is restored. $effect(() => { const ch = readerState.activeChapter; const manga = readerState.activeManga; @@ -361,26 +390,18 @@ if (style === "longstrip" && readerState.pageUrls.length && readerState.activeChapter) { const ch = readerState.activeChapter; const urls = readerState.pageUrls; - const targetPg = untrack(() => readerState.resumePage); + const resumeTo = untrack(() => readerState.resumePage); appending = false; readerState.stripChapters = [{ chapterId: ch.id, chapterName: ch.name, urls }]; readerState.visibleChapterId = ch.id; tick().then(() => { if (!containerEl) return; - if (targetPg > 1) { - const chId = ch.id; - const scrollToResumePage = () => { - const target = containerEl!.querySelector(`img[data-local-page="${targetPg}"][data-chapter="${chId}"]`); - if (!target) { requestAnimationFrame(scrollToResumePage); return; } - containerEl!.querySelectorAll(`img[data-chapter="${chId}"]`).forEach((img, i) => { if (i < targetPg) img.loading = "eager"; }); - const doScroll = () => { target.scrollIntoView({ block: "start" }); readerState.stripResumeReady = true; }; - if (target.complete && target.naturalHeight > 0) doScroll(); - else { target.loading = "eager"; target.addEventListener("load", doScroll, { once: true }); } - }; - scrollToResumePage(); + if (resumeTo > 1) { + pageViewRef.scrollToFlatIndex(resumeTo - 1); + readerState.stripResumeReady = true; return; } - containerEl!.scrollTop = 0; + containerEl.scrollTop = 0; }); } }); @@ -430,10 +451,11 @@ untrack(() => { cleanupScroll(); cleanupScroll = setupScrollTracking(containerEl!, { - onPageChange: (p) => { readerState.pageNumber = p; }, - onChapterChange: (id) => { readerState.visibleChapterId = id; }, - onMarkRead: (id) => markChapterRead(id, markedRead), - onAppend: () => { + onPageChange: (p) => { readerState.pageNumber = p; }, + onChapterChange: (id) => { readerState.visibleChapterId = id; }, + onCenterIdxChange: (idx) => { pageViewRef?.notifyScrollCenter(idx); }, + onMarkRead: (id) => markChapterRead(id, markedRead), + onAppend: () => { if (appending || !readerState.stripChapters.length) return; appending = true; appendNextChapter( @@ -628,6 +650,7 @@ onClampZoom={clampZoom} onApplySettings={applySettings} onSettingsOpen={() => { app.setSettingsOpen(true); }} + onOpenPreview={() => { if (readerState.activeManga) setPreviewManga(readerState.activeManga); }} {perMangaEnabled} /> @@ -688,7 +711,7 @@ {barPosition} onGoPrev={goPrev} onGoNext={goNext} - onJumpToPage={(p) => jumpToPage(p, style, lastPage, containerEl)} + onJumpToPage={(p, commit) => primedJump(p, commit)} /> {/snippet} @@ -702,7 +725,7 @@ {barPosition} onGoPrev={goPrev} onGoNext={goNext} - onJumpToPage={(p) => jumpToPage(p, style, lastPage, containerEl)} + onJumpToPage={(p, commit) => primedJump(p, commit)} /> {/if}
diff --git a/src/lib/components/reader/ReaderControls.svelte b/src/lib/components/reader/ReaderControls.svelte index db500dd..523e984 100644 --- a/src/lib/components/reader/ReaderControls.svelte +++ b/src/lib/components/reader/ReaderControls.svelte @@ -36,6 +36,7 @@ onClampZoom: (z: number) => number; onApplySettings: (patch: Partial) => void; onSettingsOpen: () => void; + onOpenPreview: () => void; perMangaEnabled: boolean; } @@ -47,7 +48,7 @@ barPosition, progressBar, onCaptureZoomAnchor, onRestoreZoomAnchor, onMaybeMarkRead, onToggleBookmark, onCommitMarker, onDeleteMarker, - onClampZoom, onApplySettings, onSettingsOpen, + onClampZoom, onApplySettings, onSettingsOpen, onOpenPreview, perMangaEnabled, }: Props = $props(); @@ -155,12 +156,12 @@ {:else} { e.stopPropagation(); (e.currentTarget as HTMLElement).scrollLeft += e.deltaY; }}> - - {readerState.activeManga?.title} - / - {displayChapter?.name} + - {/if} {#if !isVertical} @@ -494,6 +495,8 @@ .ch-marquee-track { overflow-x: auto; min-width: 0; flex: 1; scrollbar-width: none; } .ch-marquee-track::-webkit-scrollbar { display: none; } .ch-marquee-content { display: inline-flex; align-items: center; gap: var(--sp-2); white-space: nowrap; } + .ch-preview-btn { background: none; border: none; cursor: pointer; padding: 0; font-size: inherit; font-family: inherit; border-radius: var(--radius-sm); transition: opacity var(--t-fast); } + .ch-preview-btn:hover { opacity: 0.7; } .ch-title { color: var(--text-secondary); font-weight: var(--weight-medium); } .ch-sep { color: var(--text-faint); flex-shrink: 0; } .ch-name { color: var(--text-muted); } diff --git a/src/lib/components/reader/ReaderOverlay.svelte b/src/lib/components/reader/ReaderOverlay.svelte index 8328ba8..aef7c93 100644 --- a/src/lib/components/reader/ReaderOverlay.svelte +++ b/src/lib/components/reader/ReaderOverlay.svelte @@ -21,11 +21,27 @@ readerState.dlOpen = false; } + let bannerMounted = $state(false); + let bannerFading = $state(false); + + $effect(() => { + if (showResumeBanner) { + bannerMounted = true; + bannerFading = false; + } else if (bannerMounted) { + bannerFading = true; + } + }); + const queueable = $derived(adjacent.remaining.filter(c => !c.downloaded)); + + function onBannerAnimationEnd() { + if (bannerFading) { bannerMounted = false; bannerFading = false; } + } -{#if showResumeBanner} - {/if} diff --git a/src/lib/components/reader/ReaderProgressBar.svelte b/src/lib/components/reader/ReaderProgressBar.svelte index b164585..e5eeaaa 100644 --- a/src/lib/components/reader/ReaderProgressBar.svelte +++ b/src/lib/components/reader/ReaderProgressBar.svelte @@ -20,7 +20,7 @@ barPosition: "top" | "left" | "right"; onGoPrev: () => void; onGoNext: () => void; - onJumpToPage: (page: number) => void; + onJumpToPage: (page: number, commit?: boolean) => void; } const { @@ -32,12 +32,22 @@ const isVertical = $derived(barPosition === "left" || barPosition === "right"); - const hValue = $derived(rtl ? sliderMax - sliderPage + 1 : sliderPage); - const hPct = $derived(`--pct:${sliderPct}%`); + const hPct = $derived(`--pct:${sliderPct}%`); + + function sliderValToPage(raw: number): number { + return rtl ? sliderMax - raw + 1 : raw; + } + + function pageToSliderVal(page: number): number { + return rtl ? sliderMax - page + 1 : page; + } function handleH(e: Event) { - const raw = Number((e.target as HTMLInputElement).value); - onJumpToPage(rtl ? sliderMax - raw + 1 : raw); + onJumpToPage(sliderValToPage(Number((e.target as HTMLInputElement).value)), false); + } + + function handleHCommit(e: Event) { + onJumpToPage(sliderValToPage(Number((e.target as HTMLInputElement).value)), true); } function markerPct(pageNumber: number, forRtl = false): number { @@ -46,9 +56,9 @@ return ((ord - 1) / (sliderMax - 1)) * 100; } - // Custom vertical slider let trackEl = $state(null); let dragging = $state(false); + let pendingPage = 0; function pctFromPointer(clientY: number): number { if (!trackEl) return 0; @@ -64,22 +74,24 @@ if (e.button !== 0) return; e.preventDefault(); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); - dragging = true; + dragging = true; readerState.sliderDragging = true; - const pct = pctFromPointer(e.clientY); - onJumpToPage(pageFromPct(pct)); + pendingPage = pageFromPct(pctFromPointer(e.clientY)); + onJumpToPage(pendingPage, false); } function handleTrackPointerMove(e: PointerEvent) { if (!dragging) return; - const pct = pctFromPointer(e.clientY); - onJumpToPage(pageFromPct(pct)); + pendingPage = pageFromPct(pctFromPointer(e.clientY)); + onJumpToPage(pendingPage, false); } function handleTrackPointerUp(e: PointerEvent) { if (!dragging) return; dragging = false; readerState.sliderDragging = false; + readerState.sliderHover = false; + onJumpToPage(pendingPage, true); } @@ -102,8 +114,9 @@ style={hPct} min={1} max={sliderMax} - value={hValue} + value={pageToSliderVal(sliderPage)} oninput={handleH} + onchange={handleHCommit} onmousedown={() => readerState.sliderDragging = true} onmouseup={() => readerState.sliderDragging = false} /> diff --git a/src/lib/components/reader/lib/chapterLoader.ts b/src/lib/components/reader/lib/chapterLoader.ts index 5634eff..9e2248d 100644 --- a/src/lib/components/reader/lib/chapterLoader.ts +++ b/src/lib/components/reader/lib/chapterLoader.ts @@ -1,7 +1,7 @@ -import { readerState } from "$lib/state/reader.svelte"; -import { fetchPages } from "./pageLoader"; -import { cancelQueuedFetches, revokeBlobUrl } from "$lib/core/cache/imageCache"; -import { clearResolvedUrlCache, clearPageCache } from "$lib/core/cache/pageCache"; +import { readerState } from "$lib/state/reader.svelte"; +import { fetchPages } from "./pageLoader"; +import { cancelQueuedFetches, revokeBlobUrl, preloadBlobUrls } from "$lib/core/cache/imageCache"; +import { clearResolvedUrlCache, clearPageCache } from "$lib/core/cache/pageCache"; export function scheduleResumeDismiss() { setTimeout(() => { readerState.resumeFading = true; }, 1500); @@ -46,18 +46,23 @@ export async function loadChapter( const resumeTo = bookmark ? bookmark.pageNumber : 0; readerState.resumePage = resumeTo > 1 ? resumeTo : 0; readerState.resumeDismissed = false; - readerState.resumeVisible = resumeTo > 1; - if (resumeTo > 1) scheduleResumeDismiss(); + readerState.resumeVisible = false; readerState.pageNumber = 1; try { const urls = await fetchPages(id, useBlob, ctrl.signal, resumeTo > 1 ? resumeTo - 1 : 0); if (ctrl.signal.aborted) return; readerState.pageUrls = urls; + if (useBlob && resumeTo > 1) { + const lo = Math.max(0, resumeTo - 2); + const hi = Math.min(urls.length, resumeTo + 4); + preloadBlobUrls(urls.slice(lo, hi), 900); + } if (startAtLastPage.current) readerState.pageNumber = urls.length; else if (resumeTo > 1) readerState.pageNumber = Math.min(resumeTo, urls.length || resumeTo); readerState.pageReady = true; readerState.loading = false; + if (resumeTo > 1) readerState.resumeVisible = true; if (adjacent.next) { prefetchedChapterId = adjacent.next.id; fetchPages(adjacent.next.id, useBlob, ctrl.signal).catch(() => {}); diff --git a/src/lib/components/reader/lib/navigation.ts b/src/lib/components/reader/lib/navigation.ts index 6f99589..9259f07 100644 --- a/src/lib/components/reader/lib/navigation.ts +++ b/src/lib/components/reader/lib/navigation.ts @@ -64,16 +64,32 @@ export function goBack(style: string, adjacent: Adjacent, startAtLastPage: () => } else if (adjacent.prev) { startAtLastPage(); openReader(adjacent.prev, readerState.activeChapterList); } } -export function jumpToPage(page: number, style: string, lastPage: number, containerEl: HTMLElement | null) { +export function jumpToPage( + page: number, + style: string, + lastPage: number, + scrollToFlatIndex: ((idx: number) => void) | null, + flatPageCount: number, + activeChapterId: number, + stripChapters: { chapterId: number; urls: string[] }[], +) { if (style === "longstrip") { - const chId = readerState.visibleChapterId ?? readerState.activeChapter?.id; - containerEl?.querySelector(`img[data-local-page="${page}"][data-chapter="${chId}"]`)?.scrollIntoView({ block: "start" }); + if (!scrollToFlatIndex || flatPageCount === 0) return; + let offset = 0; + for (const chunk of stripChapters) { + if (chunk.chapterId === activeChapterId) { + scrollToFlatIndex(offset + Math.max(0, page - 1)); + return; + } + offset += chunk.urls.length; + } + scrollToFlatIndex(Math.max(0, page - 1)); return; } if (style === "double" && readerState.pageGroups.length) { - const group = readerState.pageGroups[page - 1]; + const group = readerState.pageGroups.find(g => g.includes(page)) ?? readerState.pageGroups.findLast(g => g[0] <= page); if (group) readerState.pageNumber = group[0]; } else { readerState.pageNumber = Math.max(1, Math.min(lastPage, page)); } -} +} \ No newline at end of file diff --git a/src/lib/components/reader/lib/scrollHandler.ts b/src/lib/components/reader/lib/scrollHandler.ts index 9a671ec..c3fd3ee 100644 --- a/src/lib/components/reader/lib/scrollHandler.ts +++ b/src/lib/components/reader/lib/scrollHandler.ts @@ -7,34 +7,65 @@ export interface StripChapter { } export interface ScrollHandlerCallbacks { - onPageChange: (page: number) => void; - onChapterChange: (chapterId: number) => void; - onMarkRead: (chapterId: number) => void; - onAppend: () => void; - getStripChapters: () => StripChapter[]; - getPageUrls: () => string[]; - shouldAutoMark: () => boolean; + onPageChange: (page: number) => void; + onChapterChange: (chapterId: number) => void; + onCenterIdxChange: (flatIdx: number) => void; + onMarkRead: (chapterId: number) => void; + onAppend: () => void; + getStripChapters: () => StripChapter[]; + getPageUrls: () => string[]; + shouldAutoMark: () => boolean; +} + +/** + * Returns true if the element is considered "at" the read-line. + * + * Ported from Suwayomi's ReaderPager.utils `isPageInViewport`: + * - If the element's top is above the line AND its bottom is below it → fully covers the line + * (handles a single page that is taller than the viewport). + * - If the element's top is at or below the line AND its bottom is also below it → leading edge + * has crossed the line (normal scroll-past case). + * + * Using Math.trunc to avoid floating-point jitter from getBoundingClientRect. + */ +function isPageAtReadLine(el: HTMLElement, readLineY: number): boolean { + const rect = el.getBoundingClientRect(); + const top = Math.trunc(rect.top); + const bottom = Math.trunc(rect.bottom); + const line = Math.trunc(readLineY); + // Element completely spans the read line (taller than viewport or very tall image) + if (top <= line && bottom >= line) return true; + // Element's top edge is at or above the line + if (top <= line) return true; + return false; } export function setupScrollTracking( containerEl: HTMLElement, callbacks: ScrollHandlerCallbacks, ): () => void { - const { onPageChange, onChapterChange, onMarkRead, onAppend, getStripChapters, getPageUrls, shouldAutoMark } = callbacks; + const { + onPageChange, onChapterChange, onCenterIdxChange, + onMarkRead, onAppend, getStripChapters, getPageUrls, shouldAutoMark, + } = callbacks; + let rafId: number | null = null; function tick() { rafId = null; const imgs = containerEl.querySelectorAll("img[data-local-page]"); + if (!imgs.length) return; - const containerTop = containerEl.getBoundingClientRect().top; - const readLineY = containerTop + containerEl.clientHeight * READ_LINE_PCT; + const containerRect = containerEl.getBoundingClientRect(); + const readLineY = containerRect.top + containerEl.clientHeight * READ_LINE_PCT; + // Find the last image whose top is at or above the read line. + // Binary search is still valid here since images are ordered top-to-bottom. let lo = 0, hi = imgs.length - 1, best = 0; while (lo <= hi) { const mid = (lo + hi) >>> 1; - if (imgs[mid].getBoundingClientRect().top <= readLineY) { best = mid; lo = mid + 1; } + if (isPageAtReadLine(imgs[mid], readLineY)) { best = mid; lo = mid + 1; } else hi = mid - 1; } @@ -45,10 +76,19 @@ export function setupScrollTracking( onPageChange(activePage); if (activeChId) onChapterChange(activeChId); + const chunks = getStripChapters(); + let flatOffset = 0; + for (const chunk of chunks) { + if (chunk.chapterId === activeChId) { + onCenterIdxChange(flatOffset + activePage - 1); + break; + } + flatOffset += chunk.urls.length; + } + if (shouldAutoMark() && activeChId) { - const chunks = getStripChapters(); - const chunk = chunks.find(c => c.chapterId === activeChId); - const total = chunk ? chunk.urls.length : getPageUrls().length; + const chunk = chunks.find(c => c.chapterId === activeChId); + const total = chunk ? chunk.urls.length : getPageUrls().length; if (total > 0 && activePage >= total) onMarkRead(activeChId); const atBottom = containerEl.scrollTop + containerEl.clientHeight >= containerEl.scrollHeight - 40; @@ -58,8 +98,9 @@ export function setupScrollTracking( } } - const pct = (containerEl.scrollTop + containerEl.clientHeight) / containerEl.scrollHeight; - if (pct >= 0.80) onAppend(); + if ((containerEl.scrollTop + containerEl.clientHeight) / containerEl.scrollHeight >= 0.80) { + onAppend(); + } } function onScroll() { @@ -97,4 +138,4 @@ export function appendNextChapter( onDone(); }) .catch(() => onDone()); -} +} \ No newline at end of file diff --git a/src/lib/components/series/SeriesHeader.svelte b/src/lib/components/series/SeriesHeader.svelte index ee1b8ce..eaebb81 100644 --- a/src/lib/components/series/SeriesHeader.svelte +++ b/src/lib/components/series/SeriesHeader.svelte @@ -9,7 +9,8 @@ import { get } from 'svelte/store' import Thumbnail from '$lib/components/shared/manga/Thumbnail.svelte' import { resolvedCover } from '$lib/core/cover/coverResolver' - import type { MangaPrefs } from '$lib/types/settings' + import type { Manga, Chapter, Category } from '$lib/types' + import { seriesState } from '$lib/state/series.svelte' import { setPreviewManga } from '$lib/state/series.svelte' @@ -59,6 +60,7 @@ let manageOpen: boolean = $state(false) let genresExpanded: boolean = $state(false) + let descExpanded: boolean = $state(false) let altOpen: boolean = $state(false) const statusLabel = $derived( @@ -104,7 +106,7 @@ {:else}
-

{manga?.title}

+ {#if manga?.author || manga?.artist} @@ -148,8 +150,8 @@ {#if manga?.description}
-

{manga.description}

- +

{manga.description}

+
{/if}
@@ -277,7 +279,11 @@ font-size: var(--text-base); font-weight: var(--weight-medium); color: var(--text-primary); line-height: var(--leading-snug); letter-spacing: var(--tracking-tight); + background: none; border: none; padding: 0; text-align: left; cursor: pointer; + transition: color var(--t-base); } + .title:hover:not(:disabled) { color: var(--accent-fg); } + .title:disabled { cursor: default; } .byline { font-size: var(--text-xs); color: var(--text-muted); font-family: var(--font-ui); } .badges { display: flex; flex-wrap: wrap; gap: var(--sp-1); } @@ -328,6 +334,7 @@ font-size: var(--text-xs); color: var(--text-muted); line-height: var(--leading-base); display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; } + .desc.desc-open { display: block; -webkit-line-clamp: unset; overflow: visible; } .expand-toggle { font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint); letter-spacing: var(--tracking-wide); align-self: flex-start; transition: color var(--t-base); diff --git a/src/lib/components/shared/manga/MangaPreview.svelte b/src/lib/components/shared/manga/MangaPreview.svelte index 6ca25f2..d375a32 100644 --- a/src/lib/components/shared/manga/MangaPreview.svelte +++ b/src/lib/components/shared/manga/MangaPreview.svelte @@ -259,7 +259,7 @@ function openSeriesDetail() { if (!displayManga) return; setActiveManga(displayManga); - setNavPage(originNavPage); + app.setNavPage(originNavPage); close(); }