Skip to content

Tag feeds with premium-gated popular search

jedmund requested to merge claude/tag-feeds into claude/illust-detail-view

Created by: jedmund

Summary

Tap a tag pill in the detail view to open a tag feed for that tag. Segmented Popular / Recent sort toggle in the nav bar, and a gear button that opens a sheet with a time range picker. Premium accounts get paginated sort=popular_desc popular search with a time range; non-premium fall through to popular-preview. Premium status now shows as a golden pill on the Profile screen.

  • Tag pill → TagFeedView (pixillate/Views/TagFeedView.swift) — the tag pill inside IllustDetailView is now a NavigationLink(value: tag) that pushes a new feed screen. HomeView registers the PixivIllust.Tag destination so any view in the home stack can push one.
  • Endpoint split (pixillate/Networking/PixivEndpoint.swift) — .tagSearchRecent(word:timeRange:) for date-sorted search, .tagSearchPopular(word:timeRange:) for the premium-gated paginated popular search, .tagSearchPopularPreview(word:) as the free top-30 fallback. Both /v1/search/illust cases funnel through a private searchIllustURL(word:sort:timeRange:) helper so the duration / start_date / end_date translation lives in one place.
  • Premium plumbingHomeView reads is_premium off the signed-in PixivUser via @Environment(AuthSession.self) and passes isPremium into each TagFeedView push. Navigation destination closures re-read on every push, so a user who upgrades mid-session picks up the paginated endpoint on the next tag-feed open with no extra plumbing.
  • Time range picker (pixillate/Views/TagSearchSettingsView.swift) — day / week / month / 6 months / year / all time. SearchTimeRange owns the translation to Pixiv's native duration buckets (day/week/month) or to explicit start_date/end_date strings (half-year/year). Sheet footer is dynamic: premium users see "Applies to both modes", non-premium see the "Recent only" caveat.
  • Premium badge on Profile — golden-gradient pill + crown.fill under the user's @handle when is_premium == true. Pulled out of the plain info-card row so it's visible at a glance and signals the unlocked capabilities.
  • MasonryFeed extraction — identical rendering (shortest-column distribution, LazyVStack-per-column, prefetch via cell .task, full state overlay for loading / empty / error / pagination) factored out of HomeView into a reusable component so HomeView and TagFeedView share one implementation. HomeView is now a ~70-line composer.

Known gaps (for follow-up)

  • "Popular with all / male / female" demographic filter is not wired. Pixiv's mobile /v1/search/illust endpoint doesn't appear to expose a demographic weighting query param; the ranking endpoint (/v1/illust/ranking?mode=day_male / day_female) does, but it isn't tag-filtered. The settings sheet has a placeholder "Popularity audience" section documenting the gap in-place. When we find the right API surface (or decide to blend ranking results client-side), it's a one-line addition to TagSearchOptions plus a new endpoint case.
  • Non-premium popular results are capped at ~30. popular-preview isn't paginated. This is a Pixiv limitation, not something we can fix without premium.
  • Black-cell-on-next-tap bug from the detail-view PR carries over. TagFeedView uses the same zoom-navigation + matchedTransitionSource pattern, so the same regression applies here. Tracked on the parent detail-view PR (jedmund/pixillate#3).

Stacked branch

This PR is stacked on top of claude/illust-detail-view (PR #3) because the tag pill push depends on IllustDetailView existing. Targeting that branch as the base keeps the diff here focused on the tag-feed work. When the detail-view PR lands, this branch should be rebased onto main before merge — or merged in sequence so the two land as two commits on main.

Test plan

  • Tap a tag pill in a detail view → pushes TagFeedView with the tag's translated name (or #name) as the nav title.
  • Popular / Recent segmented toggle swaps the feed; fresh load each time with loadInitial.
  • Gear in top-trailing opens the settings sheet; time range picker is an inline list; Done button dismisses.
  • On a premium account: time range in Popular mode paginates and respects the range (try day vs. all time).
  • On a premium account: the sheet footer reads "Applies to both Recent and Popular modes".
  • On a non-premium account: Popular mode returns top ~30 with no pagination, time range has no effect on Popular.
  • Profile screen shows a golden Premium pill under the @handle when the signed-in user is premium, and no pill when they're not.
  • Tapping a cell inside TagFeedView still pushes IllustDetailView with the zoom transition stitched to this view's namespace (not HomeView's).
  • Pull-to-refresh, pagination prefetch, error retry, empty state all work — they're the same MasonryFeed component the home feed uses.

https://claude.ai/code/session_01RDp8iEpHQ3GgpvDesCBR3s

Merge request reports

Loading