diff --git a/Sources/StorageScope/Stores/ScanStore.swift b/Sources/StorageScope/Stores/ScanStore.swift index f02771d..96eb06e 100644 --- a/Sources/StorageScope/Stores/ScanStore.swift +++ b/Sources/StorageScope/Stores/ScanStore.swift @@ -1,3 +1,4 @@ +import Combine import Foundation import StorageScopeCore import SwiftUI @@ -230,18 +231,41 @@ func setSelectedView(_ view: SmartView) { } #endif - lazy var onDemandVerification: OnDemandVerificationStore = OnDemandVerificationStore( - hashCache: hashCache, - scanLookup: { [weak self] in self?.scan }, - coordinateInvalidate: { [weak self] in self?.invalidateDerivedCaches() }, - reportError: { [weak self] message in self?.errorMessage = message } - ) - - lazy var filters = FilterStore( - scanLookup: { [weak self] in self?.scan }, - coordinateInvalidate: { [weak self] in self?.invalidateDerivedCaches() }, - recordSearchRecent: { [weak self] term in self?.searchRecents.add(term) } - ) + /// Forwards `onDemandVerification.objectWillChange` the same way `filtersCancellable` + /// does for `filters` below — views read `store.onDemandVerification.verifyingGroupIDs` + /// directly through `@ObservedObject var store: ScanStore` without observing the nested + /// store itself, so without this the "Verify Now" spinner wouldn't live-update either. + private var onDemandVerificationCancellable: AnyCancellable? + + lazy var onDemandVerification: OnDemandVerificationStore = { + let store = OnDemandVerificationStore( + hashCache: hashCache, + scanLookup: { [weak self] in self?.scan }, + coordinateInvalidate: { [weak self] in self?.invalidateDerivedCaches() }, + reportError: { [weak self] message in self?.errorMessage = message } + ) + onDemandVerificationCancellable = store.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } + return store + }() + + /// Keeps `filters.objectWillChange` forwarded into this store's own `objectWillChange` + /// (set up alongside `filters` below). Without this, views that hold `@ObservedObject + /// var store: ScanStore` (every view in the app) never re-render when a `FilterStore` + /// property changes directly — e.g. flipping a Settings toggle updated the underlying + /// value, but the checkbox/stepper visually stayed stuck until some unrelated action + /// (like navigating to a different sidebar view) happened to trigger `ScanStore`'s own + /// `objectWillChange` and force a fresh re-render that picked up the already-changed value. + private var filtersCancellable: AnyCancellable? + + lazy var filters: FilterStore = { + let store = FilterStore( + scanLookup: { [weak self] in self?.scan }, + coordinateInvalidate: { [weak self] in self?.invalidateDerivedCaches() }, + recordSearchRecent: { [weak self] term in self?.searchRecents.add(term) } + ) + filtersCancellable = store.objectWillChange.sink { [weak self] in self?.objectWillChange.send() } + return store + }() /// Typed Binding for any writable FilterStore property — used by Picker/Stepper/Toggle /// in views since `$store.filters.X` can't traverse ObservableObject's sub-store boundary. diff --git a/Sources/StorageScope/Views/DetailView.swift b/Sources/StorageScope/Views/DetailView.swift index 6716d8a..e71e79d 100644 --- a/Sources/StorageScope/Views/DetailView.swift +++ b/Sources/StorageScope/Views/DetailView.swift @@ -93,9 +93,9 @@ private struct ScanHeaderView: View { VStack(alignment: .leading, spacing: 16) { HStack(alignment: .firstTextBaseline) { VStack(alignment: .leading, spacing: 4) { - Text(store.scan?.rootURL.lastPathComponent.nonEmpty ?? "Scanning...") + Text(titleText) .font(.title2.weight(.semibold)) - Text(store.scan?.rootURL.path ?? store.progress.currentPath) + Text(pathText) .foregroundStyle(.secondary) .lineLimit(1) .truncationMode(.middle) @@ -138,6 +138,22 @@ private struct ScanHeaderView: View { } .padding(20) } + + private var titleText: String { + guard let rootURL = store.scan?.rootURL else { return "Scanning..." } + guard store.filters.redactionEnabled else { return rootURL.lastPathComponent.nonEmpty ?? rootURL.path } + return store.filters.displayName(forURL: rootURL, isDirectory: true) + } + + private var pathText: String { + guard let rootURL = store.scan?.rootURL else { + guard store.filters.redactionEnabled else { return store.progress.currentPath } + let url = URL(fileURLWithPath: store.progress.currentPath) + return "\(store.filters.displayParentPath(forURL: url))/\(store.filters.displayName(forURL: url, isDirectory: false))" + } + guard store.filters.redactionEnabled else { return rootURL.path } + return "…/\(store.filters.displayName(forURL: rootURL, isDirectory: true))" + } } private struct MetricCard: View { diff --git a/Sources/StorageScope/Views/SidebarView.swift b/Sources/StorageScope/Views/SidebarView.swift index d11b7c5..173f01e 100644 --- a/Sources/StorageScope/Views/SidebarView.swift +++ b/Sources/StorageScope/Views/SidebarView.swift @@ -42,7 +42,13 @@ struct SidebarView: View { VStack(spacing: 2) { ForEach(store.recents.entries) { entry in - RecentScanRow(entry: entry) { + RecentScanRow( + entry: entry, + displayTitle: store.filters.displayName(forURL: URL(fileURLWithPath: entry.path), isDirectory: true), + displayPath: store.filters.redactionEnabled + ? "…/\(store.filters.displayName(forURL: URL(fileURLWithPath: entry.path), isDirectory: true))" + : entry.path + ) { store.scanRecentPath(entry.path) } forgetAction: { store.recents.forget(path: entry.path) @@ -223,6 +229,8 @@ private struct SidebarPathButton: View { private struct RecentScanRow: View { let entry: RecentScanEntry + let displayTitle: String + let displayPath: String let scanAction: () -> Void let forgetAction: () -> Void @@ -233,7 +241,8 @@ private struct RecentScanRow: View { // subtitle frozen after first paint, so rescans showed the wrong number. TimelineView(.periodic(from: .now, by: 60)) { _ in SidebarPathButton( - path: entry.path, + path: displayPath, + title: displayTitle, systemImage: "clock.arrow.circlepath", subtitle: subtitleText ) { @@ -289,7 +298,7 @@ private struct ScanStatusFooter: View { } } } - Text(store.progress.currentPath) + Text(currentPathDisplay) .font(.caption2) .foregroundStyle(.tertiary) .lineLimit(2) @@ -300,11 +309,11 @@ private struct ScanStatusFooter: View { Text("\(scan.scannedItemCount.formatted()) items scanned") .font(.caption) .foregroundStyle(.secondary) - Text(scan.rootURL.path) + Text(rootPathDisplay(for: scan.rootURL)) .font(.caption2) .foregroundStyle(.tertiary) .lineLimit(2) - .help(scan.rootURL.path) + .help(store.filters.redactionEnabled ? "" : scan.rootURL.path) } else { Label("No scan yet", systemImage: "internaldrive") .font(.headline) @@ -318,6 +327,17 @@ private struct ScanStatusFooter: View { .background(.bar) } + private var currentPathDisplay: String { + guard store.filters.redactionEnabled else { return store.progress.currentPath } + let url = URL(fileURLWithPath: store.progress.currentPath) + return "\(store.filters.displayParentPath(forURL: url))/\(store.filters.displayName(forURL: url, isDirectory: false))" + } + + private func rootPathDisplay(for rootURL: URL) -> String { + guard store.filters.redactionEnabled else { return rootURL.path } + return "…/\(store.filters.displayName(forURL: rootURL, isDirectory: true))" + } + private func elapsedText(from start: Date, to now: Date) -> String { let elapsed = max(0, Int(now.timeIntervalSince(start))) let minutes = elapsed / 60 diff --git a/VERSION b/VERSION index ee6cdce..faef31a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.7.0 diff --git a/docs/architecture.html b/docs/architecture.html index f50eddc..232dd96 100644 --- a/docs/architecture.html +++ b/docs/architecture.html @@ -335,7 +335,7 @@

6. Store hierarchy

diff --git a/docs/changelog.html b/docs/changelog.html index a43832e..9afc95e 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -70,6 +70,26 @@

Changelog

StorageScope release history. Download the latest from GitHub Releases.

+
+

v0.7.0 — Scan Control and Privacy

+
Released 2026-07-01 · release notes · download .dmg
+

Scan control and privacy release. Adds a folder exclusion list so scans can skip node_modules, .git, and cache directories entirely; results now stream into list views live during a scan instead of only appearing at the end; cancelling a scan keeps its partial results browsable instead of discarding them; scans can be paused and resumed in place, with a live items/sec and elapsed-time readout; and a new redaction toggle masks file/folder names in the UI for screen-sharing.

+ +

Scan control

+ + +

Privacy

+ +
+

v0.6.0 — Visual Refresh

Released 2026-06-22 · release notes · download .dmg
@@ -351,14 +371,14 @@

v0.1.1 — Trash confirmation sheet and scan slices

- Download v0.6.0 (.dmg) + Download v0.7.0 (.dmg) All releases
-

Requires macOS 14 or later · verify with SHA-256 · Privacy

+

Requires macOS 14 or later · verify with SHA-256 · Privacy

diff --git a/docs/duplicate-file-finder-macos.html b/docs/duplicate-file-finder-macos.html index ef6861d..eaffc47 100644 --- a/docs/duplicate-file-finder-macos.html +++ b/docs/duplicate-file-finder-macos.html @@ -91,14 +91,14 @@

Related StorageScope pages

- Download v0.6.0 (.dmg) + Download v0.7.0 (.dmg) View on GitHub Build from source
-

Requires macOS 14 or later · verify with SHA-256 · all releases

+

Requires macOS 14 or later · verify with SHA-256 · all releases

diff --git a/docs/faq.html b/docs/faq.html index 468e8d5..4e438b5 100644 --- a/docs/faq.html +++ b/docs/faq.html @@ -158,11 +158,11 @@

Frequently asked questions

Do I need Full Disk Access?

Only for folder-level scanning of locations outside what a sandboxed app can read by default. For most folders you select through the macOS folder picker, no Full Disk Access is needed. The first-run card links directly to System Settings so you can grant it only if a scan reports an access gap.

Where do bug reports and feature requests go?

Issues and pull requests are welcome on the GitHub repository. The full release history with notes and download checksums is on the GitHub Releases page.

-

Privacy · Changelog · Download v0.6.0 (.dmg)

+

Privacy · Changelog · Download v0.7.0 (.dmg)

diff --git a/docs/index.html b/docs/index.html index 2ffc432..a78735f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -22,7 +22,7 @@ "name": "StorageScope", "applicationCategory": "UtilitiesApplication", "operatingSystem": "macOS 14+", - "softwareVersion": "0.6.0", + "softwareVersion": "0.7.0", "datePublished": "2026-06-21", "dateModified": "2026-06-22", "programmingLanguage": "Swift", @@ -646,7 +646,7 @@
- Free & Open Source · v0.6.0 + Free & Open Source · v0.7.0

StorageScope

@@ -654,9 +654,9 @@

StorageScope

stale files, and verified duplicates — all processed locally, nothing uploaded.

-
-

What's new in v0.6.0

+
+

What's new in v0.7.0

    -
  • Visual refresh: welcome hero, inspector redesign with tinted icon header, card radius 12, hover states across list rows, smooth view transitions
  • -
  • Storage Map unified into a single card with divider rows; metric cards use tabular figures and an opaque material
  • -
  • Performance: StorageItemRow, DuplicateItemRow, CleanupCandidateRow adopted Equatable so SwiftUI skips diff/recompute for identity-equal rows
  • -
  • WelcomeView extracted into its own module (#90) and empty-state ownership moved from ContentView into DetailView
  • +
  • Folder exclusion list (node_modules, .git, caches by default) with an on/off toggle and a right-click "Exclude This Folder" action
  • +
  • Live streaming results: list views populate and re-rank during a scan instead of waiting for it to finish
  • +
  • Cancel keeps partial results — a cancelled scan stays browsable instead of resetting to empty
  • +
  • In-memory pause/resume for long scans, plus a live items/sec and elapsed-time readout
  • +
  • Redaction toggle: mask file and folder names in the UI with stable per-session placeholders, for screen-sharing or screenshots
@@ -794,7 +795,7 @@

Guides

· Architecture
-
v0.6.0 · macOS 14+ · SwiftUI/AppKit
+
v0.7.0 · macOS 14+ · SwiftUI/AppKit
diff --git a/docs/keyboard-shortcuts.html b/docs/keyboard-shortcuts.html index 38ea27b..6589f4f 100644 --- a/docs/keyboard-shortcuts.html +++ b/docs/keyboard-shortcuts.html @@ -470,7 +470,7 @@

Notes

diff --git a/docs/mac-storage-cleaner.html b/docs/mac-storage-cleaner.html index 5c35a52..b869eae 100644 --- a/docs/mac-storage-cleaner.html +++ b/docs/mac-storage-cleaner.html @@ -91,14 +91,14 @@

Related StorageScope pages

- Download v0.6.0 (.dmg) + Download v0.7.0 (.dmg) View on GitHub Build from source
-

Requires macOS 14 or later · verify with SHA-256 · all releases

+

Requires macOS 14 or later · verify with SHA-256 · all releases

diff --git a/docs/macos-disk-space-analyzer.html b/docs/macos-disk-space-analyzer.html index 335b6f1..7314f28 100644 --- a/docs/macos-disk-space-analyzer.html +++ b/docs/macos-disk-space-analyzer.html @@ -92,14 +92,14 @@

Related StorageScope pages

- Download v0.6.0 (.dmg) + Download v0.7.0 (.dmg) View on GitHub Build from source
-

Requires macOS 14 or later · verify with SHA-256 · all releases

+

Requires macOS 14 or later · verify with SHA-256 · all releases

diff --git a/docs/open-source-cleanmymac-alternative.html b/docs/open-source-cleanmymac-alternative.html index c205be8..c684de9 100644 --- a/docs/open-source-cleanmymac-alternative.html +++ b/docs/open-source-cleanmymac-alternative.html @@ -91,14 +91,14 @@

Related StorageScope pages

- Download v0.6.0 (.dmg) + Download v0.7.0 (.dmg) View on GitHub Build from source
-

Requires macOS 14 or later · verify with SHA-256 · all releases

+

Requires macOS 14 or later · verify with SHA-256 · all releases

diff --git a/docs/privacy.html b/docs/privacy.html index 2745240..e4823ed 100644 --- a/docs/privacy.html +++ b/docs/privacy.html @@ -99,7 +99,7 @@

Open source, verifiable