Skip to content

Detail: hold-heart-drag-up to bookmark privately

jedmund requested to merge jedmund/sao-paulo-v4 into main

Created by: jedmund

Summary

The detail's floating heart gains a springy direct-manipulation gesture that exposes Pixiv's existing private-bookmark capability without a separate menu or settings screen.

Tap = toggle public bookmark (unchanged). Hold ~0.22s = a second glass button slides up above the heart with a bouncy spring; labels fade in to its left. Drag onto either button = it grows under the thumb, the other shrinks back. Release on a button commits that kind (public toggle, or private toggle); release off either button cancels cleanly.

Behavior details

  • HeartButtonState now tracks a three-case BookmarkState (.unbookmarked / .publiclyBookmarked / .privatelyBookmarked) instead of a single bool. The public heart fills only when publicly bookmarked; the lock-heart fills only when privately bookmarked — never both.
  • Primary flips by state: privately-bookmarked posts show lock-heart as the resting bottom button with the "Bookmark privately" label; held drag reveals the public heart above it. Unbookmarked and publicly-bookmarked posts keep the public heart as primary.
  • Tapping the primary toggles that kind off (if already set) or on. Dragging to the secondary always switches kinds — Pixiv's .bookmarkAdd(restrict:) handles both the insert and the publicprivate transition via the same endpoint.
  • Labels render in .callout weight-medium with .primary foreground for OS-appearance contrast. Held row: opacity 1.0 + scale 1.08. Other: opacity 0.8 + scale 1.0. Trailing-aligned, 12pt gap from buttons.
  • .bookmarkDetail(illustID:) fires on detail open + each swipe-advance so the true restrict level comes back over the wire. A .privatelyBookmarked response flips the primary to lock-heart with no bounce animation (not a user action, so no scale-pop).
  • Race-safe across swipe-paging: heartDetailTask is cancelled on advance; the apply handler bails if the user moved to a different illust while the fetch was in flight.

Animations

One shared bouncy spring (.spring(response: 0.32, dampingFraction: 0.6)) drives:

  • The secondary button's slide-up + fade + scale on reveal.
  • The active-button scale (1.12) + inactive back-to-1.0 on thumb crossover.
  • The label opacity (0.8 1.0) + scale (1.0 1.08) on the held row.

Layout

Heart host's size constraint bumped from 56×56 to 260×124 to fit the expanded stack + widest label. Non-interactive regions (labels, collapsed-secondary slot) are .allowsHitTesting(false) so the wider footprint doesn't absorb taps meant for the related-posts grid below. Heart button still pinned to bottom-right of safe area via the SwiftUI HStack's trailing-alignment.

Test plan

  • Tap heart on an unbookmarked post → public heart fills, normal behavior.
  • Tap heart on a publicly-bookmarked post → unbookmarks.
  • Hold heart ~1/4s → privately button slides up with spring, labels fade in.
  • Hold + drag to private → privately button grows, heart shrinks, labels follow. Release → lock-heart fills red, public heart empties. Confirm on pixiv.net that the bookmark is private.
  • Open detail on a post that was privately bookmarked outside this session → after .bookmarkDetail resolves, the primary button flips to lock-heart (filled) with no bounce animation.
  • From privately-bookmarked, tap the lock-heart → unbookmarks.
  • From privately-bookmarked, hold lock-heart + drag up to public heart → switches to public bookmark. Confirm on pixiv.net.
  • Drag off either button + release → cancels cleanly, no API call, no state change.
  • Dark + Light mode: glass chrome + label contrast both read correctly.
  • Tap a related-posts cell in the lower-right of the detail → still pushes to that illust's detail (the wider heart host doesn't absorb the tap).
  • Swipe-page the pedestal mid-hold → gesture resets cleanly on advance.
  • Airplane mode: hold + drag to private → heart flips optimistically, rolls back to pre-tap state when the API errors out.

🤖 Generated with Claude Code

Merge request reports

Loading