978959bdc4
Operator feedback: thumbnails too small to judge order, no obvious way to mark
'this installment is Part 2', and the permanent two-pane picker was busy and
competed with the ordering work.
- Full-width parts, each a card with a big page grid (150px, contain so whole
pages are visible) and drag-to-reorder; positional page number as a badge.
- Editable Part # (hero field) backed by new series_chapter.stated_part —
separate from the auto-managed chapter_number, mirroring the page_number vs
stated_page split so reorder/delete renumbering can't wipe a hand-set part.
Missing-Part hints when consecutive parts' stated_part jump >1.
- Each part labels its source post (derived from pages' primary_post_id) and
shows the printed-page range with clear labels.
- Picker demoted to an on-demand right slide-over ('Add pages') with a target-
part selector; part actions (move/merge/delete) collapsed into an overflow ⋮.
alembic 0042 adds series_chapter.stated_part (nullable int).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.4 KiB
Python
55 lines
2.4 KiB
Python
"""SeriesChapter — an ordered chapter/part within a series.
|
|
|
|
A series IS a Tag(kind='series'); a chapter groups ordered SeriesPages under it.
|
|
Reading order is (chapter.chapter_number, series_page.page_number): chapter_number
|
|
sets the order of chapters, page_number orders pages within a chapter.
|
|
|
|
chapter_number is an ordering key only (not unique) — reorder rewrites 1..N
|
|
wholesale, mirroring series_page.page_number, so a reorder can't transiently
|
|
collide on a unique index.
|
|
|
|
A chapter may be a placeholder (is_placeholder=True) — a reserved empty slot for
|
|
a section the operator doesn't have yet; it holds no pages and shows as a gap in
|
|
the reader. stated_page_start/end carry the page range parsed from the source
|
|
post (FC-6.2), used to flag missing-page gaps; both are nullable when unknown.
|
|
|
|
stated_part is the operator-facing "Part N" label (FC-6.4), separate from the
|
|
positional chapter_number: chapter_number is auto-managed ordering (rewritten
|
|
1..N on reorder/delete), while stated_part is the real installment number the
|
|
operator types — e.g. a series authored from a post that is Part 2 of a story.
|
|
Nullable when unset (the UI then falls back to showing chapter_number).
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Text, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from .base import Base
|
|
|
|
|
|
class SeriesChapter(Base):
|
|
__tablename__ = "series_chapter"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
series_tag_id: Mapped[int] = mapped_column(
|
|
ForeignKey("tag.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
chapter_number: Mapped[int] = mapped_column(Integer, nullable=False)
|
|
stated_part: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
title: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
is_placeholder: Mapped[bool] = mapped_column(
|
|
Boolean, nullable=False, server_default="false"
|
|
)
|
|
stated_page_start: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
stated_page_end: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), nullable=False, server_default=func.now()
|
|
)
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=func.now(),
|
|
onupdate=func.now(),
|
|
)
|