Polish clipboard panel UX and interactions
This commit is contained in:
@@ -1,64 +1,76 @@
|
||||
# Architecture
|
||||
|
||||
ClipBored is a single-process AppKit utility built with Swift Package Manager.
|
||||
ClipBored is a single-process AppKit utility built with Swift Package Manager. UI, capture, persistence, preview generation, and paste orchestration stay in-process. Capture persistence, card-thumbnail loading, archive and sync operations, image rotation, and OCR use bounded background queues; their UI state and completion feedback return to the main thread.
|
||||
|
||||
## Runtime Shape
|
||||
|
||||
- `ClipBoredApp` creates `NSApplication`, sets accessory activation, installs `AppDelegate`, and starts the run loop.
|
||||
- `AppDelegate` wires shared services, status menu items, settings observers, and global shortcuts.
|
||||
- `ClipboardMonitorService` polls `NSPasteboard.changeCount` on a utility queue with adaptive active/idle intervals.
|
||||
- `ClipboardStore` keeps the in-memory item list and persists rows to SQLite on a serial queue.
|
||||
- `ClipboardCacheService` stores bounded image previews under Application Support and keeps a small `NSCache`.
|
||||
- `ClipboardCloudSyncService` resolves the app-private iCloud ubiquity container when sync is enabled and pushes or pulls the portable archive file.
|
||||
- `ShortcutManager` registers Carbon hotkeys for app-wide commands.
|
||||
- `ClipboardPanelController` owns the side panel lifecycle, Dock-aware frame planning, configured left/right placement, and target-app tracking.
|
||||
- `ClipboardPanelViewModel` filters, sorts, selects, copies, pastes, pins, organizes, deletes, opens, and reveals items.
|
||||
- `LinkPreviewWindowController` opens selected HTTP(S) links in an ephemeral WebKit preview window instead of handing them to another browser.
|
||||
- `OnboardingWindowController` shows the first-run setup assistant for shortcut, retention, system entry points, launch-at-login, iCloud sync, and Accessibility permission choices.
|
||||
- `SettingsWindowController` exposes native controls for capture, privacy, performance, shortcuts, and data management.
|
||||
- `ClipBoredApp` creates `NSApplication`, installs `AppDelegate`, and starts the run loop.
|
||||
- `AppDelegate` wires services, menu-bar commands, settings observers, and global shortcuts.
|
||||
- `ClipboardMonitorService` watches `NSPasteboard.changeCount` on a utility queue with adaptive active/idle polling intervals selected by the Performance setting.
|
||||
- `ClipboardStore` owns the in-memory item list and SQLite persistence on a serial queue.
|
||||
- `ClipboardCacheService` stores bounded encrypted preview sidecars under Application Support and maintains a small in-memory cache.
|
||||
- `ClipboardCloudSyncService` resolves the private iCloud ubiquity container and pushes or pulls portable archives only when sync is enabled.
|
||||
- `ShortcutManager` registers only intentional system-wide Carbon hotkeys (open shelf and Stack capture). The Settings binding is handled by the shelf's local key monitor and is never registered globally.
|
||||
- `ClipboardPanelController` owns shelf lifecycle, current-screen placement, left/right frame planning, target-app tracking, and show/hide/reflow animation.
|
||||
- `ClipboardPanelViewModel` owns query parsing, indexed category unions, sorting, selection, copy/paste, pinning, Pinboards, Stack, deletion, opening, and asynchronous thumbnail request coalescing.
|
||||
- `ClipboardPanelView` renders the toolbar, vertical category icon rail, and viewport-aware card list beside it.
|
||||
- `LinkPreviewWindowController` opens user-selected HTTP(S) links in an ephemeral WebKit preview window.
|
||||
- `OnboardingWindowController` handles first-run shortcut, retention, lifecycle, sync, and Accessibility choices.
|
||||
- `SettingsWindowController` presents six resizable, vertically scrollable settings pages and routes common settings changes through targeted control refreshes.
|
||||
|
||||
## Data Flow
|
||||
## Capture And Presentation Flow
|
||||
|
||||
1. The monitor notices a pasteboard change.
|
||||
2. Source app metadata is checked against ignored apps.
|
||||
3. Pasteboard content is normalized into a `ClipboardItem`.
|
||||
4. Sensitive text is skipped when exclusion is enabled.
|
||||
5. Copied images run local Vision OCR only when `Search in image labels` is enabled; image cards can also run the same local OCR on demand from their quick actions.
|
||||
6. The store deduplicates, preserves pinned and collection-assigned items, enforces limits, and persists the mutation.
|
||||
7. The panel view model receives store updates and recomputes the visible list.
|
||||
2. Capture rules check paused state, ignored source apps, allowed content kinds, and optional sensitive-text exclusion.
|
||||
3. Pasteboard content is normalized into a `ClipboardItem`; local Vision OCR runs when image-label search is enabled.
|
||||
4. The store deduplicates, preserves pinned and Pinboard-assigned items, enforces retention/length limits, and persists the mutation.
|
||||
5. The panel view model maintains category/Pinboard indexes, applies the text query and selected category union, and caches parsed search matches across counts and category changes.
|
||||
6. The view lays out card slots beside the category rail in one vertical document and materializes cards near the visible viewport.
|
||||
7. Cards render immediately with a fallback presentation. Preview thumbnails load on a bounded operation queue; identical in-flight requests share work, and a still-relevant card is replaced in place on the main thread when its image arrives.
|
||||
|
||||
## Persistence
|
||||
## Shelf Interaction Model
|
||||
|
||||
History is stored in SQLite at:
|
||||
The shelf uses a fixed vertical layout on the configured left or right edge of the active screen.
|
||||
|
||||
- Header row one contains the collapsed/expanded search control plus clear-history and settings actions.
|
||||
- Header row two is a labeled, horizontally scrollable category rail. Built-in categories without clips are omitted unless currently selected; custom Pinboards remain available when empty.
|
||||
- A normal chip click replaces the active category filter. Command-click adds or removes chips from a union. Hover changes chrome only and never changes filtering.
|
||||
- An empty, unfocused search field collapses to an icon. Click, typing, or `Command + F` expands and focuses it; clicking elsewhere collapses it only when the query is empty. Repeated `Command + F` keeps focus in the same field.
|
||||
- Cards scroll vertically and fill the usable shelf width. Hover expansion changes visual presentation only: it does not mutate keyboard selection, the selected range, or query/category state.
|
||||
- Card commands are discoverable through context menus, VoiceOver descriptions, and keyboard shortcuts. Hover-only action controls are not part of the interaction contract.
|
||||
- Category changes and card/search expansion use short AppKit/Core Animation transitions. Both panel-controller and panel-view durations resolve to zero when macOS Reduce Motion is enabled.
|
||||
|
||||
The panel has no user-facing resize lip, alternate density/layout mode, close button, new-text action, or persistent status bar. It is dismissed with `Esc` or the configured global shortcut.
|
||||
|
||||
## Settings UI
|
||||
|
||||
Settings uses a custom segmented selector backed by a borderless `NSTabView`:
|
||||
|
||||
- `General` - history, sorting, shelf side, launch, and menu-bar/Dock presence
|
||||
- `Shortcuts` - a system-wide open-shelf binding and a pane-local open-settings binding
|
||||
- `Capture` - pause state, content kinds, image-label search, likely-secret exclusion, ignored apps, and capture status
|
||||
- `Privacy` - local-data behavior, screen-capture hiding, Accessibility permission, and paste status
|
||||
- `Performance` - adaptive polling profile and thumbnail-cache cap
|
||||
- `Data` - iCloud archive sync, local archive import/export, storage location, and destructive clears
|
||||
|
||||
Each tab is a top-aligned document inside its own vertical scroll view. The window has a practical minimum size, no horizontal scrollers, and commits focused text/shortcut drafts when it closes. Common narrow settings changes update their bound controls without rebuilding the whole window; expensive cloud status checks are cached across unrelated refreshes.
|
||||
|
||||
## Persistence And Privacy Boundaries
|
||||
|
||||
History is stored in:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/ClipBored/history.sqlite
|
||||
```
|
||||
|
||||
Images are stored under:
|
||||
Image previews are stored under `images/`; restorable audio, video, rich-text, and PDF payloads are stored under `attachments/`. Legacy JSON import remains for migration from early builds.
|
||||
|
||||
```text
|
||||
~/Library/Application Support/ClipBored/images/
|
||||
```
|
||||
Portable `.clipboredarchive` files preserve item metadata, Pinboards, and decrypted bytes for app-managed sidecars so another Mac can re-cache them with its own storage paths and encryption key. External file references remain path-based. Optional iCloud sync uses the same unencrypted archive format inside the app-private ubiquity container and is disabled by default.
|
||||
|
||||
Restorable non-image payloads such as audio clips, rich text, and PDFs are stored under:
|
||||
|
||||
```text
|
||||
~/Library/Application Support/ClipBored/attachments/
|
||||
```
|
||||
|
||||
Legacy JSON import still exists for migration from early builds.
|
||||
|
||||
Portable `.clipboredarchive` files are JSON exports created by `ClipboardArchiveService`. They preserve item metadata and include decrypted bytes for app-managed image, URL-thumbnail, audio, video, rich-text, and PDF sidecars so a different Mac can re-cache them under its own storage directory and encryption key. External file references remain path-based and are not copied into the archive.
|
||||
|
||||
Optional iCloud Sync uses that same archive format at `Documents/ClipBored/ClipBored.clipboredarchive` inside the app-private ubiquity container. It is disabled by default, requires a signed build with iCloud entitlement access, pulls once when enabled at launch, and debounces pushes after local store changes. Shared Pinboard collaboration is not implemented.
|
||||
|
||||
Textual SQLite fields, including optional collection names and image OCR text, are encrypted and decrypted at the `ClipboardStore` boundary. App-managed image cache files, URL preview thumbnails, audio clips, rich text sidecars, and PDF attachments are encrypted and decrypted at the `ClipboardCacheService` boundary. The encryption key is stored in Keychain when available, with an owner-only app-local fallback key if Keychain access blocks or fails. Full history clears remove the local fallback key when present and reset cached key state after SQLite deletion succeeds. Runtime `ClipboardItem` values remain plaintext in memory so search, duplicate detection, copy, paste, organization, archive export/import, and cache cleanup operate normally. Opening or revealing encrypted media creates a temporary decrypted copy for macOS handoff; stale temporary previews are cleared on launch, cache/history clear, and quit. Link previews are user-triggered and use a non-persistent WebKit data store.
|
||||
Textual SQLite fields are encrypted and decrypted at the `ClipboardStore` boundary. Managed cache and attachment files are encrypted and decrypted at the `ClipboardCacheService` boundary. The key lives in Keychain when available, with an owner-only local fallback if Keychain access fails. Runtime `ClipboardItem` values remain plaintext in memory for search and clipboard operations. Opening encrypted media may create a temporary decrypted file; stale previews are cleared on launch, cache/history clear, and quit. Link previews are user-triggered and use a non-persistent WebKit data store.
|
||||
|
||||
## Size And Power Constraints
|
||||
|
||||
The release build intentionally avoids SwiftUI, Combine, Swift Concurrency, third-party packages, bundled media, and app resources beyond `Info.plist`.
|
||||
The release build intentionally avoids SwiftUI, Combine, Swift Concurrency, third-party packages, and bundled media. The shelf avoids continuous layout work, renders only cards near the viewport, coalesces preview requests, keeps card-thumbnail decoding off the main thread, and bounds both memory and disk caches. Clipboard monitoring uses change-count polling with selectable adaptive profiles rather than continuous file scans.
|
||||
|
||||
The side shelf is anchored to the current screen's visible frame and can be placed on the left or right edge. It uses compact horizontal rows in a vertical list; the active or hovered row expands in place while the panel stays clear of side Docks and reserves bottom Dock space for content padding.
|
||||
|
||||
The build script uses `-Osize`, whole-module optimization, disabled reflection metadata, linker dead stripping, symbol stripping, and hardened-runtime signing. The current public targets, enforced by `scripts/build-macos-app.sh`, are 2 MiB gates for both the executable and app bundle.
|
||||
The build script uses `-Osize`, whole-module optimization, disabled reflection metadata, linker dead stripping, symbol stripping, and hardened-runtime signing. `scripts/build-macos-app.sh` enforces 2 MiB gates for both the executable and app bundle.
|
||||
|
||||
Reference in New Issue
Block a user