feat(detail): mark read chapters and count new ones on the manga page
On the manga detail page, dim chapters at or before the reader's last-read chapter (with an accent check + screen-reader "Read." label) and show a "N new since last read" badge counting chapters that have landed since. All client-side from the already-loaded chapter list and per-manga read progress — no extra requests. The count reflects the loaded (paginated) chapter list. Logic lives in a pure, unit-tested chapterProgress helper; e2e covers the mid-series and never-opened cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
} from '$lib/api/mangas';
|
||||
import { resyncManga } from '$lib/api/admin';
|
||||
import { chapterLabel } from '$lib/api/chapters';
|
||||
import { isChapterRead, countNewChapters } from '$lib/chapterProgress';
|
||||
import { formatBytes } from '$lib/upload-validation';
|
||||
import { listTags, type Tag } from '$lib/api/tags';
|
||||
import type { ContentWarning } from '$lib/api/page_tags';
|
||||
@@ -27,6 +28,7 @@
|
||||
import UploadCloud from '@lucide/svelte/icons/upload-cloud';
|
||||
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
|
||||
import ChevronLeft from '@lucide/svelte/icons/chevron-left';
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import MoreHorizontal from '@lucide/svelte/icons/more-horizontal';
|
||||
import BookmarkIcon from '@lucide/svelte/icons/bookmark';
|
||||
import BookmarkCheck from '@lucide/svelte/icons/bookmark-check';
|
||||
@@ -68,6 +70,12 @@
|
||||
: null
|
||||
);
|
||||
|
||||
/** The reader's last-read chapter number, used to mark chapters as read
|
||||
* and count what's landed since. `null` for guests / never-read. */
|
||||
const lastReadNumber = $derived(readProgress?.chapter_number ?? null);
|
||||
/** New chapters since last read, over the loaded chapter list. */
|
||||
const newChapterCount = $derived(countNewChapters(chapters, lastReadNumber));
|
||||
|
||||
const authors = $derived<AuthorRef[]>(manga.authors);
|
||||
const genres = $derived<GenreRef[]>(manga.genres);
|
||||
/** Deduped content warnings across the manga's pages (analysis worker). */
|
||||
@@ -597,7 +605,14 @@
|
||||
|
||||
<section aria-label="chapters">
|
||||
<div class="chapters-head">
|
||||
<h2>Chapters</h2>
|
||||
<div class="chapters-head-left">
|
||||
<h2>Chapters</h2>
|
||||
{#if newChapterCount > 0}
|
||||
<span class="new-chapters" data-testid="new-chapters-summary">
|
||||
{newChapterCount} new since last read
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if contentBytes === null || contentBytes > 0}
|
||||
<span class="content-size" data-testid="manga-content-size">
|
||||
Content: {contentBytes === null ? '—' : formatBytes(contentBytes)}
|
||||
@@ -624,7 +639,12 @@
|
||||
{:else}
|
||||
<ol class="chapter-list" data-testid="chapter-list">
|
||||
{#each chapters as c (c.id)}
|
||||
<li>
|
||||
{@const read = isChapterRead(c.number, lastReadNumber)}
|
||||
<li class:read data-testid={read ? `chapter-read-${c.id}` : undefined}>
|
||||
{#if read}
|
||||
<Check size={15} class="read-check" aria-hidden="true" />
|
||||
<span class="sr-only">Read.</span>
|
||||
{/if}
|
||||
<a href="/manga/{manga.id}/chapter/{c.id}">
|
||||
{chapterLabel(c)}
|
||||
</a>
|
||||
@@ -1009,6 +1029,40 @@
|
||||
padding: var(--space-1) 0;
|
||||
}
|
||||
|
||||
/* Already-read chapters (at or before the reader's last-read chapter)
|
||||
dim so the eye is drawn to what's still unread; the check keeps its
|
||||
accent colour as a positive "done" marker. */
|
||||
.chapter-list li.read {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.chapter-list li.read :global(svg) {
|
||||
vertical-align: -2px;
|
||||
margin-right: var(--space-1);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.new-chapters {
|
||||
color: var(--primary);
|
||||
background: var(--primary-soft-bg);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: 0 var(--space-2);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--weight-semibold);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.pages {
|
||||
color: var(--text-muted);
|
||||
margin-left: var(--space-2);
|
||||
@@ -1022,6 +1076,13 @@
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.chapters-head-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-size {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-sm);
|
||||
|
||||
Reference in New Issue
Block a user