feat(series): manage-view redesign — big pages, editable Part #, slide-over picker (FC-6.4)
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>
This commit is contained in:
@@ -113,3 +113,52 @@ async def test_list_series_cards(db):
|
||||
# artist filter keeps it; a different artist id drops it.
|
||||
assert any(r["id"] == out["series_tag_id"] for r in await svc.list_series(artist_id=artist.id))
|
||||
assert all(r["id"] != out["series_tag_id"] for r in await svc.list_series(artist_id=artist.id + 99999))
|
||||
|
||||
|
||||
# --- FC-6.4: stated_part, part_gaps, source_post label ---------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_chapter_sets_and_clears_stated_part(db):
|
||||
svc = SeriesService(db)
|
||||
sid = (await TagService(db).find_or_create("Part Series", TagKind.series)).id
|
||||
ch = await svc.create_chapter(sid)
|
||||
# Set the installment to Part 2 (chapter_number stays its positional value).
|
||||
await svc.update_chapter(sid, ch["id"], stated_part=2, set_part=True)
|
||||
data = await svc.list_pages(sid)
|
||||
assert data["chapters"][0]["stated_part"] == 2
|
||||
assert data["chapters"][0]["chapter_number"] == 1
|
||||
# Clearing it writes NULL back.
|
||||
await svc.update_chapter(sid, ch["id"], stated_part=None, set_part=True)
|
||||
data = await svc.list_pages(sid)
|
||||
assert data["chapters"][0]["stated_part"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_part_gaps_flagged_from_stated_part(db):
|
||||
svc = SeriesService(db)
|
||||
sid = (await TagService(db).find_or_create("Gappy Series", TagKind.series)).id
|
||||
c1 = await svc.create_chapter(sid)
|
||||
c3 = await svc.create_chapter(sid)
|
||||
await svc.update_chapter(sid, c1["id"], stated_part=1, set_part=True)
|
||||
await svc.update_chapter(sid, c3["id"], stated_part=3, set_part=True)
|
||||
data = await svc.list_pages(sid)
|
||||
assert len(data["part_gaps"]) == 1
|
||||
gap = data["part_gaps"][0]
|
||||
assert gap["after_chapter_id"] == c1["id"]
|
||||
assert gap["start"] == 2
|
||||
assert gap["end"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_post_label_when_pages_share_one_post(db):
|
||||
svc = SeriesService(db)
|
||||
artist = await _artist(db, "Src Artist")
|
||||
post = await _post(db, artist, "Source Comic pages 1-2", "pp5")
|
||||
await _post_images(db, post, artist, 2)
|
||||
out = await svc.promote_post_to_series(post.id)
|
||||
data = await svc.list_pages(out["series_tag_id"])
|
||||
sp = data["chapters"][0]["source_post"]
|
||||
assert sp is not None
|
||||
assert sp["id"] == post.id
|
||||
assert sp["title"] == "Source Comic pages 1-2"
|
||||
|
||||
Reference in New Issue
Block a user