v0.4.5 — Sidebar UX arc, ScanStore split, accessibility
v0.4.5 — Sidebar UX arc, ScanStore split, accessibility
39 PRs since v0.4.0 (#33–#72). All backwards-compatible additions; no breaking API changes. The cumulative story is the sidebar becoming a real Mac-app surface (tappable cards, hotkeys, dismissible chips) plus tier-1/tier-2 of the ScanStore split for v0.5.0.
Sidebar / Filter Bar
- Filter chips individually dismissible (#39) — new
FilterStore.ActiveFilterstruct +clearActiveFilter(_:). The single "Clear" button became "Clear All"; each chip is now a Button with anxmark. - Sidebar scan buttons disabled mid-scan (#40) — Grant Access / Recent Scans / Volumes buttons gate on
store.isScanning. Recent scan context menu stays interactive so "Forget" still works. - Toolbar Choose Folder gated mid-scan (#49) — same gate applied to the SwiftUI
ToolbarItemGroupbutton missed from #40. - Auto-clear stale file-type focus on view changes (#47) — new
SmartView.supportsFileTypeFocus. Navigating from.largestFiles(where theType: mp4chip applies) to.largestFolders(where folders lack extensions) auto-drops the chip instead of silently emptying the view. - File-type filter separated from search query (#46) — new
FilterStore.fileTypeFocus: String?. Chip now rendersType: mp4rather than mislabeling asSearch: mp4.ItemsCacheKeyfoldsfileTypeFocusin so the cache invalidates on focus change. - File-type row highlight in Type Breakdown (#62) — focused extension row shows
checkmark.circle.fill+selectionBackground. - Instant search-clear on empty field (#45) —
searchText.didSetcancels the debounce task and propagatesquery = ""synchronously when the field empties, so the chip drops immediately instead of waiting 200ms. - Context-aware search prompt (#64) — Tree → "Search folders and paths", File Types → "Search extensions", Cleanup Review → "Search cleanup candidates", Largest Folders → "Search largest folders".
- Highlight the focused
Type: <ext>row in Type Breakdown (#62) — per-row checkmark + selection tint shows which extension is currently set. - Auto-refreshing recent scan timestamps (#56) — recent-scan
RecentScanRowsubtitle now usesTimelineView(.periodic)so "5 minutes ago" stays current without manual state.
Item rows & tables
- Double-click-to-open across item rows (#43) —
simultaneousGesture(TapGesture(count: 2))onStorageItemRow,DuplicateItemList, andTreeNodeRow. Preserves single-tap selection; containers open in Finder, files in their handler. - Clickable sortable column headers (#65) —
StorageItemTableheaders (Name / Size / Kind / Modified) now sort on click withchevron.up/chevron.downindicator. Modified cycles Newest ↔ Oldest per click. - Voice sort direction in column header a11y values (#68) — accessibilityValue reports "Active sort, descending/ascending" instead of just "Active sort".
- Relative time in Modified column (#66) —
StorageFormat.relativeOrAbsoluteDate(_:relativeTo:)shows "2 hours ago" for files <7 days old, falls back to the absolute date beyond. Sort still uses the underlyingDate.
Trash review sheet
- Reveal All (#50) — per-section
revealAllTrashReviewItems(_:)opens every URL in one Finder window viaNSWorkspace.activateFileViewerSelecting. - Open button on rows (#53) — new
openTrashReviewItem(_:)+ icon button so users preview files in their default app before confirming Trash. - Remove All (#67) — paired with Reveal All; drops an entire section (e.g. all review-suggested items leaving verified-only) in one tap via
Set.subtract. - Dismissible scan-canceled acknowledgment (#55) —
ScanNoticeViewgains optionaldismissAction.scanNoticeIsDismissibleis true only for transient cancel notices; stale-options and results-need-refresh notices still require Rescan.
Mac menu / window structure
- View menu + Cmd+1–8 hotkeys (#41) — Smart View entries with
selectSidebarView(_:)sharing a single selector keyed offtag. Active item marked.onso the menu doubles as a where-am-I indicator. - Window menu Close (#52) —
NSWindow.performClose(_:)with Cmd+W. Was missing entirely; standard Mac Window menu convention. - Edit menu Find… (Cmd+F) (#69) —
ScanStore.searchFieldFocusRequestcounter +requestSearchFieldFocus()helper.ContentViewobserves via.onChangeand applies.searchFocused()through anif #available(macOS 15.0, *)-guardedSearchFocusModifier(the modifier API lands in macOS 15; project target is macOS 14, so older builds track state but field doesn't auto-focus — graceful fallback). - View menu Enter Full Screen (#70) — Cmd+Ctrl+F wired to
NSWindow.toggleFullScreen(_:). System auto-toggles the title between "Enter Full Screen" / "Exit Full Screen". - Help menu (#59) —
StorageScope on GitHubandReport an Issueitems. Registered asNSApp.helpMenuso macOS's standard Help-search integration will work later when docs exist. - Dynamic window title with scan root (#48) — title re-renders to "StorageScope — " when a scan completes, via Combine subscription on
store.$session.sessionbumped fromprivatetoprivate(set)so the projected publisher is observable.
Overview / Welcome / Inspector
- Tappable Overview insight cards (#58) — InsightCard gains
onTapclosure +chevron.rightaffordance when an item is present. Tap selects the item in the Inspector (Reveal/Open/Copy Path/Move to Trash already wired). - Recent Scans quick-launch on Welcome (#57) — chip row below capability cards surfaces the last 8 scan paths as tap-to-scan chips via horizontal ScrollView. Lazy rendering for overflow; disabled while scanning; row hidden on fresh installs.
- Gate Open on inaccessible items (#60) —
canOpenSelectedItemchecksselectedItem.isReadable(not just "selected && not scanning"). Inspector Open button help-tag explains "File is not readable; try Reveal in Finder instead" when disabled. Reveal/Copy Path keep the broader gate (they're pure URL/path operations). - Disambiguate Reclaim Plan "Suggestions" label (#42) —
.reviewVerifiedDuplicates.shortTitlestays"Review";.reviewCleanupSuggestions.shortTitlebecomes"Suggestions". Two side-by-side action buttons no longer both say "Review". - Seconds precision in scan duration (#54) —
StorageFormat.duration(from:to:)shows "1 min 30 sec" for sub-10-min scans (where seconds still matter); rounded-minute ("12 min") beyond. - Volume free-space in sidebar (#51) —
mountedVolumesfetch addsvolumeAvailableCapacityKey+volumeTotalCapacityKey;volumeCapacityDescription(for:)formats the pair ("247 GB free of 1 TB"). ReusesSidebarPathButton.subtitle. - Persist selected sidebar view across launches (#63) — selected SmartView persists via
@AppStorage("StorageScope.selectedView"). Returning users reopen where they left them, not always Overview. DefensiveobjectWillChange.send()since@AppStorageinside an ObservableObject-derived class doesn't reliably fire for@ObservedObjectsubscribers.
Refactors (v0.5.0 preliminaries)
- Tier 1a: RecentsStore (#34) — extracted
RecentsStore(entries, UserDefaults JSON, one-time migration from legacy string-array). - Tier 1b: OnDemandVerificationStore (#35) — extracted verified-groups and per-group verify.
- Tier 2: FilterStore (#38) — extracted all filter state into
FilterStore. Same lazy-var + closure-injection pattern. Critical discovery: SwiftUI's@ObservedObjectwrapper can't traverse ObservableObject sub-stores via dynamic member lookup —$store.filters.Xproduces no validBinding<T>.ScanStore.filterBinding<T>(_:)bridges via explicitBinding<T>for Picker/Stepper/Toggle sites. - First-run permission card body to .footnote (#33).
- Inline StorageFormatless.kindLabel (#36) — dead indirection removed.
- Remove dead import AppKit from ScanStore (#37).
Accessibility
- File Type Category Mix rows labeled for VoiceOver (#71) —
accessibilityLabel("<category> category, <total bytes>")+accessibilityValue("<ext count>, <file count label>"). Percent-of-bar geometry stays inaccessible (visual summaries duplicated as digits would mislead).
Validation
swift test— 54 tests across 5 suites passplutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy— cleanbash -nacross all 5 scripts — clean./script/public_upload_audit.sh— passes- DMG:
exports/StorageScope-0.4.5.dmg, SHA256913ca5618ea5162dafaab50f15cec84286c0088c97b9184718766b50a4f79fb1—hdiutil verifyconfirmed
Why v0.4.5 (not v0.5.0 or v0.4.0)
Per semver: pre-1.0 minor is the standard "feature-bearing release" bump. v0.4.x patch feels right versus v0.5.0 because the ScanStore split is half-done — Tiers 3 (SelectionState), 4 (TrashReviewState), and 5 (@Observable macro migration) remain pending. The 39 shipped UX improvements are all backwards-compatible additions; no breaking API changes to FileSystemScanner.scan(...) or ScanStore's public surface.
Deferred
- Tier 3 SelectionState / Tier 4 TrashReviewState extractions — pending; same lazy-var + closure-injection template.
- Tier 5
@Observablemacro migration — still blocked bydidSetobservers on tracked properties (deferred to v1.0.0). - Sparkle 2.x auto-update — still gated on notarization (PR #19's
script/notarize_dmg.shscaffolding from v0.4.0 waits on Apple Developer ID credentials).
How to upgrade
Existing v0.4.0 users can move directly to v0.4.5. No settings migrations; the hash cache, recent scans, bookmarks, and @AppStorage keys (StorageScope.didDismissFirstRunCard) persist unchanged. New StorageScope.selectedView is the only added UserDefaults key.
Building from source
bash ./script/build_and_run.sh # build app bundle, sign ad-hoc, launch
./script/export_dmg.sh # export a local DMG (ad-hoc signed)🤖 Generated with Code