Skip to content

feat: stack preview cards for successive captures + optional manual dismiss#83

Open
iOSDevSK wants to merge 1 commit into
KartikLabhshetwar:mainfrom
iOSDevSK:feat/stacked-preview-cards
Open

feat: stack preview cards for successive captures + optional manual dismiss#83
iOSDevSK wants to merge 1 commit into
KartikLabhshetwar:mainfrom
iOSDevSK:feat/stacked-preview-cards

Conversation

@iOSDevSK

Copy link
Copy Markdown

What

Two quality-of-life changes to the floating preview overlay, matching CleanShot X behavior:

  1. Stacked preview cards — screenshots taken in quick succession now stack up in the screen corner (newest at the bottom, up to 5) instead of replacing each other. Each card keeps its own auto-dismiss timer and its own delete / edit / pin / close actions, and drag-out to Finder/Slack/Figma still works per card. Cards are ~30% larger for easier targets.
  2. "Keep visible until closed" — new toggle in Capture settings. When enabled, cards never auto-dismiss and stay on screen until closed via their ✕ button.

How it works

  • PreviewOverlay now manages an items: [Item] stack rendered by a PreviewStackView (VStack of cards) in a single panel that grows with the stack; per-item dismiss tasks replace the single global timer
  • Oldest card is evicted when a 6th capture arrives
  • Respects the existing position setting (bottom-right / bottom-left)
  • New AppPreferences.overlayNeverDismiss + toggle in Capture settings; the dismiss-delay slider is disabled while it's on

Scope

PreviewOverlay.swift, AppPreferences.swift, PreferencesView.swift (Capture tab), CHANGELOG.md under Unreleased. No version bump.

Testing

  • make release builds clean
  • Manually verified: 3 rapid captures stack correctly; each card dismisses independently; 6th capture evicts the oldest; "Keep visible until closed" keeps cards until ✕; drag-out provides the file URL; works on both corner positions and with recordings (video thumbnails load async)

Note: on the newest Xcode toolchains main itself doesn't compile — see #80 for minimal Swift 6 concurrency fixes. This PR is independent of it.

🤖 Generated with Claude Code

…smiss

Screenshots taken in quick succession now stack up in the screen
corner (newest at the bottom, up to 5 cards) instead of replacing each
other - like CleanShot X. Each card keeps its own auto-dismiss timer,
delete/edit/pin/close actions, and drag-out support; cards are ~30%
larger for easier targets.

Adds a "Keep visible until closed" toggle in Capture settings: when
enabled, cards never auto-dismiss and stay until closed via their X
button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the knox projects Team on Vercel.

A member of the Team first needs to authorize it.

/// When true, preview overlay cards stay visible until manually closed
/// (no auto-dismiss timer).
static var overlayNeverDismiss: Bool {
get { UserDefaults.standard.object(forKey: overlayNeverDismissKey) as? Bool ?? false }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: bool(forKey:) is simpler here and avoids the object(forKey:) as? Bool bridge.

Suggested change
get { UserDefaults.standard.object(forKey: overlayNeverDismissKey) as? Bool ?? false }
get { UserDefaults.standard.bool(forKey: overlayNeverDismissKey) }

dismissTask = Task {
private func scheduleDismiss(for id: UUID) {
cancelDismiss(for: id)
guard !AppPreferences.overlayNeverDismiss else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One behavioral edge case: if a card already has a dismiss task scheduled and the user later enables “Keep visible until closed”, those already-scheduled tasks will still fire. Might be worth cancelling outstanding dismiss tasks when overlayNeverDismiss flips to true (UserDefaults change notification / onChange from prefs).

let ext = item.url.pathExtension.lowercased()
let isVideo = ext == "mov" || ext == "mp4"
// Images load synchronously here; video thumbnails generate async in onAppear.
_thumbnail = State(initialValue: isVideo ? nil : NSImage(contentsOf: item.url))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With stacking, this sync NSImage(contentsOf:) can happen multiple times back-to-back on the main thread. If you see UI hitching on rapid captures, consider loading images async (similar to the video thumbnail path) and setting thumbnail on the main actor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant