Tag feeds with premium-gated popular search
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 insideIllustDetailViewis now aNavigationLink(value: tag)that pushes a new feed screen.HomeViewregisters thePixivIllust.Tagdestination 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/illustcases funnel through a privatesearchIllustURL(word:sort:timeRange:)helper so theduration/start_date/end_datetranslation lives in one place. -
Premium plumbing —
HomeViewreadsis_premiumoff the signed-inPixivUservia@Environment(AuthSession.self)and passesisPremiuminto eachTagFeedViewpush. 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.SearchTimeRangeowns the translation to Pixiv's nativedurationbuckets (day/week/month) or to explicitstart_date/end_datestrings (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.fillunder the user's@handlewhenis_premium == true. Pulled out of the plain info-card row so it's visible at a glance and signals the unlocked capabilities. -
MasonryFeedextraction — identical rendering (shortest-column distribution,LazyVStack-per-column, prefetch via cell.task, full state overlay for loading / empty / error / pagination) factored out ofHomeViewinto a reusable component soHomeViewandTagFeedViewshare one implementation.HomeViewis 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/illustendpoint 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 toTagSearchOptionsplus a new endpoint case. -
Non-premium popular results are capped at ~30.
popular-previewisn'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.
TagFeedViewuses the same zoom-navigation +matchedTransitionSourcepattern, 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 TagFeedViewwith 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 @handlewhen the signed-in user is premium, and no pill when they're not. -
Tapping a cell inside TagFeedViewstill pushesIllustDetailViewwith 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 MasonryFeedcomponent the home feed uses.