feat(series): chapter-aware manage view + reader — frontend (FC-6.1)
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 25s
CI / frontend-build (push) Successful in 26s
CI / integration (push) Successful in 3m5s

Completes FC-6.1: the series management UI now works in chapters.

- SeriesManageView: chapters as cards (inline-rename, stated-page range inputs,
  move up/down, merge-into-previous, delete, pick-as-add-target), pages
  drag-reorder WITHIN a chapter, a "gap: N-M missing" badge between chapters
  with a stated-page hole, and Add chapter / Add placeholder. The picker adds
  the selection into the targeted chapter.
- seriesManage store: chapter CRUD + reorderChapters/moveChapter/mergeChapter/
  reorderPages actions; consumes chapters[]/gaps[]; addSelected targets a chapter.
- Reader: page_number is now within-chapter, so anchors switched to a global
  `seq` (reading-order position) — fixes scroll/jump/active collisions across
  chapters — plus chapter-title dividers at each chapter boundary.
- Updated seriesManage.spec to the chaptered store shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 16:43:10 -04:00
parent 1804a2c622
commit 8ad40da145
5 changed files with 385 additions and 93 deletions
+26 -15
View File
@@ -20,9 +20,9 @@
:value="currentPage" @change="jumpTo($event.target.value)"
>
<option
v-for="p in store.pages" :key="p.page_number"
:value="p.page_number"
>Page {{ p.page_number }}</option>
v-for="p in store.pages" :key="p.seq"
:value="p.seq"
>Page {{ p.seq }}</option>
</select>
<v-btn
icon="mdi-menu" variant="text" size="small"
@@ -43,23 +43,27 @@
<div
v-for="p in store.pages" :key="p.image_id"
class="fc-reader__thumb"
:class="{ active: p.page_number === currentPage }"
@click="jumpTo(p.page_number, true)"
:class="{ active: p.seq === currentPage }"
@click="jumpTo(p.seq, true)"
>
<img :src="p.thumbnail_url" alt="" loading="lazy" />
<span class="fc-reader__thumbnum">{{ p.page_number }}</span>
<span class="fc-reader__thumbnum">{{ p.seq }}</span>
</div>
</div>
</aside>
<div ref="scrollEl" class="fc-reader__content" @scroll="onScroll">
<div
v-for="p in store.pages" :key="p.image_id"
class="fc-reader__page" :id="'fc-page-' + p.page_number"
:data-page="p.page_number"
>
<img :src="p.image_url" :alt="'Page ' + p.page_number" loading="lazy" />
</div>
<template v-for="p in store.pages" :key="p.image_id">
<div v-if="p.isChapterStart" class="fc-reader__chdiv">
{{ p.chapterLabel }}
</div>
<div
class="fc-reader__page" :id="'fc-page-' + p.seq"
:data-page="p.seq"
>
<img :src="p.image_url" :alt="'Page ' + p.seq" loading="lazy" />
</div>
</template>
<div v-if="store.error" class="fc-reader__empty">
{{ store.error }}
<v-btn variant="text" color="accent" @click="goBack">Back</v-btn>
@@ -115,9 +119,9 @@ function pageMetrics() {
const root = scrollEl.value
if (!root) return []
return store.pages.map(p => {
const el = root.querySelector('#fc-page-' + p.page_number)
const el = root.querySelector('#fc-page-' + p.seq)
return {
page_number: p.page_number,
page_number: p.seq,
top: el ? el.offsetTop : 0,
height: el ? el.offsetHeight : 0
}
@@ -280,6 +284,13 @@ onUnmounted(() => {
display: flex; flex-direction: column; align-items: center;
gap: 2px; padding: 0.25rem;
}
.fc-reader__chdiv {
width: 100%; max-width: 1200px; margin: 0.5rem auto 0;
padding: 0.4rem 0.75rem;
font-family: 'Fraunces', Georgia, serif; font-size: 0.95rem;
color: rgb(var(--v-theme-on-surface-variant));
border-top: 1px solid rgb(var(--v-theme-surface-light));
}
.fc-reader__page { width: 100%; display: flex; justify-content: center; }
.fc-reader__page img {
max-width: min(100%, 1200px); height: auto; display: block;