Skip to content

v0.4.5 — Sidebar UX arc, ScanStore split, accessibility

Choose a tag to compare

@RasputinKaiser RasputinKaiser released this 21 Jun 17:07
6fee1a9

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.ActiveFilter struct + clearActiveFilter(_:). The single "Clear" button became "Clear All"; each chip is now a Button with an xmark.
  • 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 ToolbarItemGroup button missed from #40.
  • Auto-clear stale file-type focus on view changes (#47) — new SmartView.supportsFileTypeFocus. Navigating from .largestFiles (where the Type: mp4 chip 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 renders Type: mp4 rather than mislabeling as Search: mp4. ItemsCacheKey folds fileTypeFocus in 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.didSet cancels the debounce task and propagates query = "" 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 RecentScanRow subtitle now uses TimelineView(.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)) on StorageItemRow, DuplicateItemList, and TreeNodeRow. Preserves single-tap selection; containers open in Finder, files in their handler.
  • Clickable sortable column headers (#65) — StorageItemTable headers (Name / Size / Kind / Modified) now sort on click with chevron.up/chevron.down indicator. 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 underlying Date.

Trash review sheet

  • Reveal All (#50) — per-section revealAllTrashReviewItems(_:) opens every URL in one Finder window via NSWorkspace.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) — ScanNoticeView gains optional dismissAction. scanNoticeIsDismissible is 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 off tag. Active item marked .on so 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.searchFieldFocusRequest counter + requestSearchFieldFocus() helper. ContentView observes via .onChange and applies .searchFocused() through an if #available(macOS 15.0, *)-guarded SearchFocusModifier (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 GitHub and Report an Issue items. Registered as NSApp.helpMenu so 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. session bumped from private to private(set) so the projected publisher is observable.

Overview / Welcome / Inspector

  • Tappable Overview insight cards (#58) — InsightCard gains onTap closure + chevron.right affordance 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) — canOpenSelectedItem checks selectedItem.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.shortTitle stays "Review"; .reviewCleanupSuggestions.shortTitle becomes "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) — mountedVolumes fetch adds volumeAvailableCapacityKey + volumeTotalCapacityKey; volumeCapacityDescription(for:) formats the pair ("247 GB free of 1 TB"). Reuses SidebarPathButton.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. Defensive objectWillChange.send() since @AppStorage inside an ObservableObject-derived class doesn't reliably fire for @ObservedObject subscribers.

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 @ObservedObject wrapper can't traverse ObservableObject sub-stores via dynamic member lookup — $store.filters.X produces no valid Binding<T>. ScanStore.filterBinding<T>(_:) bridges via explicit Binding<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 pass
  • plutil -lint Config/StorageScope.entitlements Resources/PrivacyInfo.xcprivacy — clean
  • bash -n across all 5 scripts — clean
  • ./script/public_upload_audit.sh — passes
  • DMG: exports/StorageScope-0.4.5.dmg, SHA256 913ca5618ea5162dafaab50f15cec84286c0088c97b9184718766b50a4f79fb1hdiutil verify confirmed

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 @Observable macro migration — still blocked by didSet observers on tracked properties (deferred to v1.0.0).
  • Sparkle 2.x auto-update — still gated on notarization (PR #19's script/notarize_dmg.sh scaffolding 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