From bc43b9d05fe837c02f649ab11eab79965578fdc5 Mon Sep 17 00:00:00 2001 From: RasputinKaiser <178525839+RasputinKaiser@users.noreply.github.com> Date: Wed, 1 Jul 2026 04:54:24 -0400 Subject: [PATCH] =?UTF-8?q?release:=20v0.7.0=20=E2=80=94=20fix=20Settings?= =?UTF-8?q?=20controls=20not=20live-updating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump VERSION to 0.7.0 and update docs/changelog for the exclusion list, streaming results, cancel-keeps-partial, pause/resume, and redaction toggle features from PRs #113/#114. While verifying the release build, found that FilterStore's @Published changes never propagated into ScanStore's own objectWillChange (filters is a lazy var, not @Published, and nothing forwarded its changes). Every view holds @ObservedObject var store: ScanStore, so Settings toggles/steppers appeared stuck — the underlying value did change, but nothing forced SwiftUI to re-render the control showing it, until some unrelated action (e.g. switching sidebar views) happened to trigger ScanStore's own objectWillChange. This predates this release (reproduces on the already-shipped v0.6.0 build) but was only now caught during manual verification. Fixed by forwarding filters.objectWillChange and onDemandVerification.objectWillChange into ScanStore's own objectWillChange. Also fixed two redaction-toggle path leaks found during manual testing: the sidebar's recent-scans list/status footer and the Overview header both showed the real scan root path even with redaction enabled. --- Sources/StorageScope/Stores/ScanStore.swift | 48 +++++++++++++++----- Sources/StorageScope/Views/DetailView.swift | 20 +++++++- Sources/StorageScope/Views/SidebarView.swift | 30 ++++++++++-- VERSION | 2 +- docs/architecture.html | 2 +- docs/changelog.html | 26 +++++++++-- docs/duplicate-file-finder-macos.html | 6 +-- docs/faq.html | 4 +- docs/index.html | 25 +++++----- docs/keyboard-shortcuts.html | 2 +- docs/mac-storage-cleaner.html | 6 +-- docs/macos-disk-space-analyzer.html | 6 +-- docs/open-source-cleanmymac-alternative.html | 6 +-- docs/privacy.html | 2 +- 14 files changed, 133 insertions(+), 52 deletions(-) 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