SnipSnaps for macOS — native Mac app (Photos + Files)#2
Open
NickReisenauer wants to merge 10 commits into
Open
SnipSnaps for macOS — native Mac app (Photos + Files)#2NickReisenauer wants to merge 10 commits into
NickReisenauer wants to merge 10 commits into
Conversation
Phased plan for bringing SnipSnaps to macOS (Mac App Store, sandboxed): Photos surface port + new on-disk file-cleanup surface. Verification-grounded against current Apple docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nly code Phase 0 (project config): - Add macosx to SUPPORTED_PLATFORMS and MACOSX_DEPLOYMENT_TARGET=14.0 on the app target's two configs only (test targets untouched). Native Mac destination on the existing SwiftUI target (not Catalyst). - Add SnipSnaps-macOS.entitlements (app-sandbox + photos-library) wired via CODE_SIGN_ENTITLEMENTS[sdk=macosx*]; iOS keeps the existing entitlements file. Phase 1 (make the Photos build compile + run on macOS): - AppColors: cross-platform semantic colors; AppKit (NSColor) branch for the background/card/chip/fill/separator families that have no UIKit equivalent. - Guard UIKit-only haptics (ContentView) behind #if canImport(UIKit). - SettingsView: UIApplication.shared.open -> @Environment(.openURL). - Replace Color(UIColor.tertiaryLabel) / Color(.tertiarySystemGroupedBackground) etc. with .tertiary / AppColor tokens. - Guard iOS-only SwiftUI modifiers (navigationBarTitleDisplayMode, .tabBar, statusBarHidden, presentationDetents) with #if os(iOS); swap topBar* toolbar placements for cross-platform cancellation/confirmation/primaryAction; fullScreenCover -> sheet on macOS. Verified: macOS build SUCCEEDED (xcodebuild, signing disabled). Similar/ duplicate scan remains a no-op on macOS pending the Phase 2 CGImage refactor. iOS build not run here (iOS 26.5 platform not installed on this machine); iOS code paths are unchanged or behind #if guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SettingsView's Form used the macOS-default columnar form style, whose label-column layout failed to converge and tripped AppKit's 'Update Constraints in Window pass' cycle (NSGenericException / SIGABRT) at launch. SwiftUI's TabView eagerly lays out all tabs on macOS, so the whole app aborted even though Home was frontmost. .formStyle(.grouped) lays rows edge-to-edge (no column solve) and matches the grouped look the app already uses on iOS (where .grouped is the Form default, so iOS is visually unchanged). Verified: app builds, launches, and renders Home + Settings on macOS without crashing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e core) The entire similar-photo pipeline (fetchSimilarPhotoGroups, thumbnailImage, featurePrint, differenceHash) was wrapped in #if canImport(UIKit) with the AppKit branch returning [], so Similar mode was a silent no-op on macOS. Refactor so the comparison core is platform-shared: - thumbnailImage(->UIImage) becomes thumbnailCGImage(->CGImage); the only platform-conditional step is PlatformImage->CGImage (UIImage.cgImage vs NSImage.cgImage(forProposedRect:context:hints:)). - differenceHash now takes a CGImage; featurePrint requests a CGImage directly. - Lift fetchSimilarPhotoGroups + the helpers out of the UIKit gate (Vision, CoreGraphics, PhotoKit are all available on macOS). iOS behavior is unchanged by construction: the same CGImage is extracted from the same PHImageManager UIImage and fed to the same dHash/Vision math, so the matching thresholds are identical. Verified: macOS build SUCCEEDED and the app launches/runs with the scan compiled in. NOT yet verified: real duplicate matching on macOS — this Mac's Photos library is empty (0 assets), so thumbnailCGImage/dHash/Vision never receive real input. Needs a populated macOS Photos library to confirm matching accuracy + any NSImage->CGImage color-space threshold drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New macOS-only 'Files' tab that reviews loose files in user-granted folders (Downloads/Desktop/Documents/any) and moves confirmed files to the Trash. All new code is #if os(macOS) so iOS is untouched. - FileFolderStore: user grants folders via NSOpenPanel; access persisted as app-scoped security-scoped bookmarks, reopened each launch. No silent full-disk scan (sandbox: user picks each folder). - FileLibrary: enumerates granted folders into FileItems; categories Everything / Large (>=50MB) / Old (>180d) / Screenshots (image + name heuristic); single-pass counts; deletion via FileManager.trashItem (recoverable) — never removeItem. Skips dirs/symlinks/packages; capped scan. - FilesView: onboarding + folder management + category cards with live counts. - FileReviewSessionView: swipe + ←/→ keyboard + ⌘Z undo review flow, async QuickLook thumbnails, deferred 'Move N to Trash' summary. - Entitlements: files.user-selected.read-write (build setting flipped to readwrite) + files.bookmarks.app-scope; Info.plist Desktop/Documents/ Downloads usage strings. Verified live on macOS: granted the real Downloads folder, scan produced real counts (4715 everything / 3229 old / 6 large / 5 screenshots), categories render. Review-card + trash UI built; trash action not exercised on real files by me (destructive). MVP scope — content-hash dedup and persistent file review memory are deferred (Phase 4b). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Files review now uses the shared @AppStorage("reviewLimit") "Review Size" setting instead of a separate hardcoded limit. - Files deletions accumulate into the shared totalDeletedCount/totalDeletedBytes lifetime stats; Settings 'Lifetime Stats' relabeled 'Deleted photos' -> 'Deleted items' since it now covers both surfaces. - FilesView re-tallies category counts when a review session closes (deletions now reflected), mirroring HomeView's post-review refresh. - Files summary gains the same Lifetime card + kept/trashed tally as Photos. Still Phase 4b: 'Remember Reviewed' skip-memory for files (needs persistent file identity) — Files currently re-shows every file each scan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the last cohesion gaps between Files and Photos: - FileReviewHistory: on-disk analog of PhotoReviewHistory, keyed on file path, honoring the same ReviewMemoryOption windows. Files review marks items reviewed on keep/delete and unmarks on undo; scans exclude reviewed paths. - FilesView cards now show 'X not reviewed - Y total' and respect the shared 'Remember Reviewed' setting (counts refresh when it changes); FileLibrary.counts returns total + notReviewed per category (FileCounts), mirroring Photos. - New Duplicates category: exact-content dupes via size-bucket + streamed SHA-256 (CryptoKit), surfacing redundant copies (keeps the oldest). Shown as a scan-on-demand 'Scan' card like Photos' Similar. - Reset Local Settings also clears file review memory; app launch compacts it. Builds on macOS. Deferred: near-duplicate image matching (could reuse the Phase 2 dHash+Vision core fed CGImageSource from disk). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records what shipped (Phases 0-2, 4, 4b + cohesion), deviations (skipped Phase 3, Form crash fix, per-SDK entitlements), verification reality (macOS green; iOS unbuildable here; Photos lib empty; destructive paths not driven), and what remains. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Glass, Finder, bug fixes)
Make the Mac build feel like a real native app rather than a ported phone app.
Bumps MACOSX_DEPLOYMENT_TARGET 14.0 -> 26.0 (iOS target stays 18.5 - they are
independent settings) so Liquid Glass APIs are used directly inside #if os(macOS)
with no availability gating; iOS is untouched.
Shell & window:
- Replace the iOS bottom TabView with a native NavigationSplitView sidebar on
macOS (Photos / Files); iOS keeps its tabs.
- Move Settings into a macOS Settings{} scene (Cmd-,); add default/min window
size + resizability; Mac title-bar Refresh; persist sidebar selection.
- Replace deprecated .accentColor with .tint.
Keyboard & menu bar:
- New Commands/AppCommands.swift: Edit > Undo (Cmd-Z), File > Add Folder
(Shift-Cmd-O), a Review menu, and a Help menu wired to the real support/legal
URLs, all routed to the active review via focusedSceneValue.
- Keyboard shortcuts on all three review surfaces (left/right/Delete/Space/Esc/
Return) on hidden buttons; Cmd-keys owned by the menu.
Finder integration (Files surface):
- Reveal in Finder, Quick Look (Space), Open (double-click / Cmd-O), right-click
context menus, drag-a-folder-to-add, a sort menu, and Show-in-Trash.
Liquid Glass & visual polish:
- Design/GlassStyle.swift helpers (glass buttons, glass info-chips, hover lift +
pointer cursor); adaptive AppColor.cardEdge; monospaced hero digits.
- Fix a Light-mode bug where the window and card colors collapsed to the same
gray (cards were near-invisible) - now a proper recessed/raised hierarchy.
Bug fixes (Files engine + counts):
- Skip iCloud (.icloud) placeholder files so a scan can't trigger multi-GB
downloads; use on-disk allocated size for "space freed".
- Report the 50k-file scan cap via ScanResult.truncated instead of silently
returning an empty result.
- Make scans cancellable; harden security-scoped bookmarks (preserve transiently
offline grants, allow removing dead ones, recover an inaccessible folder via
re-add, surface an inaccessible warning).
- Fix a HomeView count-cache generation race (cross-platform).
- Surface trash/hash failures; snapshot the Remember-Reviewed option per session;
bucket duplicates by logical (not allocated) size.
Accessibility & icon:
- Honor Reduce Motion; VoiceOver (hide the decorative count, merge cards, add
Files swipe actions); Dynamic Type growth; localized screenshot-name detection.
- Add a macOS app icon (squircle generated from the iOS artwork).
macOS build verified green and launches. Not yet runtime-tested on a populated
Photos library or a real iOS build (neither available on this machine); shared-
file changes are #if os(macOS)-gated or cross-platform-safe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a native macOS build of SnipSnaps (not Catalyst) with a Mac-first shell, menu/keyboard support, and a new sandboxed “Files” cleanup surface that complements the existing Photos review flow, while keeping iOS behavior gated/unchanged where needed.
Changes:
- Introduces macOS-only Files cleanup: folder grants via security-scoped bookmarks, file scanning/categories, and a review+trash workflow.
- Updates the macOS app shell/UI:
NavigationSplitViewsidebar, nativeSettings {}window, Commands-based menu bar actions, and Liquid Glass-style helpers. - Refactors shared utilities for cross-platform correctness (e.g., Photos “Similar” scan CGImage path, semantic colors, openURL).
Reviewed changes
Copilot reviewed 22 out of 29 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| SnipSnaps/Views/Tabs/SettingsView.swift | macOS Settings window rendering + cross-platform URL opening + reset integration with Files history. |
| SnipSnaps/Views/Tabs/HomeView.swift | macOS title/toolbar tweaks, hover styling, and count-cache race fix. |
| SnipSnaps/Views/Review/ReviewSessionView.swift | macOS keyboard/menu integration via focused values; Reduce Motion handling; styling consolidation. |
| SnipSnaps/Views/Marketing/ScreenshotDemoView.swift | Updates demo UI to match new semantic colors/tertiary styling. |
| SnipSnaps/Views/Files/FilesView.swift | New Files landing page: folder grants, category cards, navigation into file review. |
| SnipSnaps/Views/Files/FileReviewSessionView.swift | New Files review flow (scan → swipe/keys → summary → trash) with Quick Look + Finder actions. |
| SnipSnaps/Utils/Photos.swift | Makes Similar scan cross-platform by using CGImage thumbnails and shared hashing/Vision pipeline. |
| SnipSnaps/SnipSnapsApp.swift | macOS window sizing, Commands hookup, native Settings scene; compacts file review history. |
| SnipSnaps/SnipSnaps-macOS.entitlements | Adds macOS sandbox/Photos/bookmark/user-selected read-write entitlements. |
| SnipSnaps/Info.plist | Adds Desktop/Documents/Downloads usage descriptions for macOS folder access prompts. |
| SnipSnaps/Files/FinderActions.swift | NSWorkspace wrappers for Reveal/Open/Trash Finder integration (macOS-only). |
| SnipSnaps/Files/FileReviewHistory.swift | Remembers reviewed file paths (session + persisted windows) mirroring Photos behavior. |
| SnipSnaps/Files/FileModels.swift | Defines Files domain models (items, categories, sort options, counts). |
| SnipSnaps/Files/FileLibrary.swift | Implements folder enumeration, categorization, duplicate hashing, and trashing. |
| SnipSnaps/Files/FileFolderStore.swift | Persists and manages security-scoped folder bookmarks and access lifetimes. |
| SnipSnaps/Design/GlassStyle.swift | Adds Liquid Glass/button/card hover helpers with iOS fallbacks. |
| SnipSnaps/Design/AppColors.swift | Introduces cross-platform semantic color mapping (UIKit vs AppKit). |
| SnipSnaps/ContentView.swift | Switches to macOS sidebar shell; keeps iOS TabView; (needs UIKit import fix). |
| SnipSnaps/Commands/AppCommands.swift | Adds macOS menu bar commands wired via focused scene values. |
| SnipSnaps/Assets.xcassets/AppIcon.appiconset/Contents.json | Wires macOS icon sizes into the shared app icon set. |
| SnipSnaps.xcodeproj/project.pbxproj | Adds macOS destination, sets macOS deployment target, macOS entitlements, and readwrite user-selected files. |
| docs/macos-port-plan.md | Documents the macOS port plan, phases, and verification notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -8,20 +8,34 @@ | |||
| import SwiftUI | |||
Comment on lines
+253
to
+270
| private func refreshCounts() { | ||
| let folders = store.folders.map(\.url) | ||
| guard !folders.isEmpty else { | ||
| counts = [:] | ||
| return | ||
| } | ||
| isCounting = true | ||
| let reviewed = FileReviewHistory.reviewedPaths(memoryOption: reviewMemoryOption) | ||
| Task { | ||
| let tally = await Task.detached(priority: .utility) { | ||
| FileLibrary.counts(folders: folders, reviewedPaths: reviewed) | ||
| }.value | ||
| await MainActor.run { | ||
| counts = tally | ||
| isCounting = false | ||
| } | ||
| } | ||
| } |
Comment on lines
+326
to
+334
| Button { | ||
| applyDecision(.delete) | ||
| } label: { | ||
| Label("Trash", systemImage: "xmark") | ||
| .frame(maxWidth: .infinity) | ||
| } | ||
| .tint(AppColor.delete) | ||
| .keyboardShortcut(.leftArrow, modifiers: []) | ||
|
|
Comment on lines
+343
to
+350
| Button { | ||
| applyDecision(.keep) | ||
| } label: { | ||
| Label("Keep", systemImage: "checkmark") | ||
| .frame(maxWidth: .infinity) | ||
| } | ||
| .tint(AppColor.success) | ||
| .keyboardShortcut(.rightArrow, modifiers: []) |
Comment on lines
+335
to
+342
| Button { | ||
| undo() | ||
| } label: { | ||
| Image(systemName: "arrow.uturn.backward") | ||
| } | ||
| .disabled(lastUndo == nil) | ||
| .help("Undo (⌘Z)") | ||
|
|
| } else if value.translation.width > threshold { | ||
| applyDecision(.keep) | ||
| } else { | ||
| withAnimation(.snappy(duration: 0.25)) { dragOffset = .zero } |
Comment on lines
+67
to
+83
| for folder in folders { | ||
| guard let enumerator = enumerator(for: folder) else { continue } | ||
| for case let url as URL in enumerator { | ||
| if examined >= maxFilesExamined { break } | ||
| guard let item = makeItem(url) else { continue } | ||
| examined += 1 | ||
| let notReviewed = !reviewed.contains(item.url.path) | ||
| func bump(_ category: FileReviewCategory) { | ||
| total[category, default: 0] += 1 | ||
| if notReviewed { fresh[category, default: 0] += 1 } | ||
| } | ||
| bump(.everything) | ||
| if item.size >= largeFileMinimumBytes { bump(.large) } | ||
| if item.modified < oldCutoff { bump(.old) } | ||
| if item.isScreenshot { bump(.screenshots) } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SnipSnaps for macOS — native Mac app (Photos + Files)
Brings SnipSnaps to macOS as a native SwiftUI destination (not Catalyst), shipping two cleanup surfaces — the existing Photos review/delete flow and a new macOS-only Files on-disk cleanup surface — and then makes the Mac build feel genuinely native. Distribution target: Mac App Store + App Sandbox. iOS is untouched throughout (all macOS-only code is
#if os(macOS)-gated; iOS deployment target stays 18.5).Port foundation (earlier phases on this branch)
macosxinSUPPORTED_PLATFORMS), sandbox + photos/files entitlements, fixed unguarded UIKit and iOS-only SwiftUI modifiers. Fixed a launch crash (Formcolumnar style → AppKit constraint-cycle abort) with.formStyle(.grouped).CGImagedHash + Vision core.NSOpenPanel→ app-scoped security-scoped bookmarks; categories Everything / Large / Old / Screenshots / Duplicates; swipe + keyboard review; recoverabletrashItemdeletes; shared review-size/lifetime stats and "Remember Reviewed" with Photos.Native UI/UX & desktop pass (this session)
Driven by a 70-finding audit, adversarially verified, then re-reviewed (3 issues caught + fixed). Bumps
MACOSX_DEPLOYMENT_TARGET14.0 → 26.0 so Liquid Glass APIs are used directly on Mac.NavigationSplitViewsidebar replaces the bottom tab bar on macOS; Settings →Settings{}scene (⌘,); window default/min size + resizability; persisted sidebar selection;.accentColor→.tint.focusedSceneValue; ←/→/Delete/Space/Esc/Return shortcuts across all three review surfaces..icloudplaceholders (no multi-GB download storms) + on-disk allocated size; report the 50k-file scan cap instead of silent-empty; cancellable scans; hardened security-scoped bookmarks (preserve offline grants, remove dead ones, re-add recovery, inaccessible warning); HomeView count-cache race; trash/hash failure surfacing; duplicate bucketing by logical size.Verification status
Deferred (follow-ups)
AppKit video/Live-Photo playback on macOS; Photos-side Quick Look + VoiceOver swipe actions; String Catalog pluralization.
🤖 Generated with Claude Code