Skip to content

Multi-page illust carousel + grid pill + page-aware lightbox

jedmund requested to merge claude/multi-page-illusts into main

Created by: jedmund

Summary

Multi-page illust support. Tapping a multi-page post in the detail view now swipes horizontally through all pages, and the grid cell gets a pill in the bottom-right indicating how many images are inside.

  • Grid cell pill — pixillate/Views/MasonryFeed.swift:191-200, 234-242. Second .overlay(alignment: .bottomTrailing) on PixivImage renders when illust.pageCount > 1, using a square.stack.fill SF Symbol + count in a black capsule at 0.6 opacity. Stacks cleanly above the existing centered R-18 badge because they live in different alignments. 6pt inset from the rounded clip edge so it hugs the corner without touching.

  • Unified PixivIllust.pages computed — pixillate/Models/PixivIllust.swift:56-89. One list of Page entries for both single- and multi-page shapes. Each entry carries large (for the detail hero) and original (for the lightbox). Single-page illusts build one synthetic entry from the top-level imageURLs + metaSinglePage.originalImageURL; multi-page maps over metaPages. bestImageURL collapses to pages.first?.original ?? imageURLs.large, so callers that only need the first page's full-res URL still work unchanged. MetaPage.ImageURLs grew a large field alongside original — Pixiv's per-page image_urls carries all the standard sizes, we just weren't decoding them before.

  • Detail-view carousel — pixillate/Views/IllustDetailView.swift:146-218. The hero ZStack now wraps a TabView(selection: $currentPage) with one entry per illust.pages element. Single-page illusts still route through the same TabView with one entry so control flow stays uniform. Each tab holds a PixivImage(url: page.large, ...) at the 1200pt downsample cap.

  • Custom page indicator — pixillate/Views/IllustDetailView.swift:207-218. The user asked for the indicator to sit below the image, inside the grey hero container — .tabViewStyle(.page)'s built-in dots overlay the image's bottom edge, so I disable them with indexDisplayMode: .never and draw my own capsule row. Apple-style elongated pill: current-page capsule stretches to 22pt wide, others stay 6pt dots, animated with .spring(response: 0.3) so the width tracks the swipe. Only renders when pages.count > 1.

  • Page-aware lightbox — pixillate/Views/IllustDetailView.swift:113-127. Previously the lightbox always opened illust.bestImageURL (effectively page 1). Now it reads currentPage and loads illust.pages[currentPage].original, so swiping to page 3 and tapping the hero opens page 3. Clamped with max(0, min(currentPage, pages.count - 1)) to guard stale state.

Known carryovers / limitations

  • Lightbox is still single-page. Swipe to page 3 in the hero, tap to open, tap close → back at page 3 in the hero. But inside the lightbox you can't swipe between pages yet. That would require nesting a paging container around ZoomableImageView and dealing with gesture-priority contention between the pager's horizontal drag and the scroll view's pan/zoom. Deferred.
  • Memory on long manga illusts. Each page's large is cached by Nuke (~1200pt, decoded). A 20-page illust keeps up to 20 decoded bitmaps in the pipeline's 150MB byte-cost limit; eviction should handle this automatically, but if you see jetsam on older devices with heavy manga, flag it and we can drop the cap further.
  • The black-flash-on-next-tap bug from PR #3's follow-ups still affects the zoom navigation transition. Multi-page illusts inherit the same regression since they use the same push pattern.

Test plan

  • Grid cell renders a black pill with a stacked-squares icon and the page count on multi-page illusts, nothing on single-page.
  • R-18 badge (when Show R-18 is off) and the page count pill don't overlap visually.
  • Tap a multi-page cell → detail view opens with page 1 visible and an elongated-pill indicator showing "1 of N" below the image.
  • Swipe horizontally through pages → indicator animates, scrolling the detail view still works, metadata below doesn't jump.
  • Tap the hero on page 3 → lightbox opens page 3 at full resolution.
  • Close the lightbox → back at page 3 in the hero (not reset to page 1).
  • Single-page illusts still render normally — no indicator, no behavior change, lightbox opens the single image.
  • iPad: the hero carousel still respects containerRelativeFrame(.vertical) { h - 200 } with the metadata peek below.

https://claude.ai/code/session_01RDp8iEpHQ3GgpvDesCBR3s

Merge request reports

Loading