Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions Sources/StorageScope/Stores/ScanStore.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Combine
import Foundation
import StorageScopeCore
import SwiftUI
Expand Down Expand Up @@ -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<T> for any writable FilterStore property — used by Picker/Stepper/Toggle
/// in views since `$store.filters.X` can't traverse ObservableObject's sub-store boundary.
Expand Down
20 changes: 18 additions & 2 deletions Sources/StorageScope/Views/DetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
30 changes: 25 additions & 5 deletions Sources/StorageScope/Views/SidebarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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
) {
Expand Down Expand Up @@ -289,7 +298,7 @@ private struct ScanStatusFooter: View {
}
}
}
Text(store.progress.currentPath)
Text(currentPathDisplay)
.font(.caption2)
.foregroundStyle(.tertiary)
.lineLimit(2)
Expand All @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.1
0.7.0
2 changes: 1 addition & 1 deletion docs/architecture.html
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ <h2>6. Store hierarchy</h2>

<footer class="site-footer">
<div>StorageScope &middot; MIT License &middot; <a href="https://github.com/RasputinKaiser/StorageScope">GitHub</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases">Releases</a> &middot; <a href="privacy.html">Privacy</a> &middot; <a href="changelog.html">Changelog</a> &middot; <a href="faq.html">FAQ</a> &middot; <a href="architecture.html">Architecture</a></div>
<div>v0.6.0 &middot; macOS 14+ &middot; SwiftUI/AppKit</div>
<div>v0.7.0 &middot; macOS 14+ &middot; SwiftUI/AppKit</div>
</footer>
</main>
</body>
Expand Down
26 changes: 23 additions & 3 deletions docs/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,26 @@
<h1>Changelog</h1>
<p>StorageScope release history. Download the latest from <a href="https://github.com/RasputinKaiser/StorageScope/releases">GitHub Releases</a>.</p>

<section class="release">
<h2>v0.7.0 &mdash; Scan Control and Privacy</h2>
<div class="meta">Released 2026-07-01 &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/tag/v0.7.0">release notes</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.7.0/StorageScope-0.7.0.dmg">download .dmg</a></div>
<p>Scan control and privacy release. Adds a folder exclusion list so scans can skip <code>node_modules</code>, <code>.git</code>, 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.</p>

<h3>Scan control</h3>
<ul>
<li><strong>Folder exclusion list</strong> &mdash; Settings gains an on/off toggle plus an editable list of excluded folders (defaults: <code>node_modules</code>, <code>.git</code>, <code>Library/Caches</code>, <code>~/Library/Application Support</code>). Excluded folders are skipped entirely during traversal, not just hidden from results. A right-click "Exclude This Folder" action is available from list views.</li>
<li><strong>Streaming results</strong> &mdash; Largest Files/Folders, Old Large Files, and other ranked views populate and re-sort live while a scan is still running, instead of showing "No Items" until it finishes.</li>
<li><strong>Cancel keeps partial results</strong> &mdash; cancelling an in-progress scan leaves the last streamed snapshot browsable instead of resetting back to an empty state.</li>
<li><strong>Pause/resume</strong> &mdash; a scan can be paused and resumed from the toolbar (in-memory only; does not survive quitting the app).</li>
<li><strong>Rate/elapsed display</strong> &mdash; the scan header shows a smoothed items/sec estimate and elapsed time while scanning.</li>
</ul>

<h3>Privacy</h3>
<ul>
<li><strong>Redaction toggle</strong> &mdash; a new Settings section masks file and folder names/paths across the UI with stable, per-session generic placeholders (e.g. "File 3.mp4", "Folder 1") while keeping sizes, dates, and counts real. Trash/move/reveal actions are unaffected since only display text is masked.</li>
</ul>
</section>

<section class="release">
<h2>v0.6.0 &mdash; Visual Refresh</h2>
<div class="meta">Released 2026-06-22 &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/tag/v0.6.0">release notes</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.6.0/StorageScope-0.6.0.dmg">download .dmg</a></div>
Expand Down Expand Up @@ -351,14 +371,14 @@ <h2>v0.1.1 &mdash; Trash confirmation sheet and scan slices</h2>
</section>

<div class="actions">
<a class="button primary" href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.6.0/StorageScope-0.6.0.dmg">Download v0.6.0 (.dmg)</a>
<a class="button primary" href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.7.0/StorageScope-0.7.0.dmg">Download v0.7.0 (.dmg)</a>
<a class="button" href="https://github.com/RasputinKaiser/StorageScope/releases">All releases</a>
</div>
<p class="actions-note">Requires macOS 14 or later &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.6.0/StorageScope-0.6.0.dmg.sha256">verify with SHA-256</a> &middot; <a href="privacy.html">Privacy</a></p>
<p class="actions-note">Requires macOS 14 or later &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.7.0/StorageScope-0.7.0.dmg.sha256">verify with SHA-256</a> &middot; <a href="privacy.html">Privacy</a></p>

<footer class="site-footer">
<div>StorageScope &middot; MIT License &middot; <a href="https://github.com/RasputinKaiser/StorageScope">GitHub</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases">Releases</a> &middot; <a href="privacy.html">Privacy</a> &middot; <a href="faq.html">FAQ</a></div>
<div>v0.6.0 &middot; macOS 14+ &middot; SwiftUI/AppKit</div>
<div>v0.7.0 &middot; macOS 14+ &middot; SwiftUI/AppKit</div>
</footer>
</main>
</body>
Expand Down
6 changes: 3 additions & 3 deletions docs/duplicate-file-finder-macos.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ <h2>Related StorageScope pages</h2>
</ul>

<div class="actions">
<a class="button primary" href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.6.0/StorageScope-0.6.0.dmg">Download v0.6.0 (.dmg)</a>
<a class="button primary" href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.7.0/StorageScope-0.7.0.dmg">Download v0.7.0 (.dmg)</a>
<a class="button" href="https://github.com/RasputinKaiser/StorageScope">View on GitHub</a>
<a class="button" href="https://github.com/RasputinKaiser/StorageScope#build-and-run">Build from source</a>
</div>
<p class="actions-note">Requires macOS 14 or later · <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.6.0/StorageScope-0.6.0.dmg.sha256">verify with SHA-256</a> · <a href="https://github.com/RasputinKaiser/StorageScope/releases">all releases</a></p>
<p class="actions-note">Requires macOS 14 or later · <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.7.0/StorageScope-0.7.0.dmg.sha256">verify with SHA-256</a> · <a href="https://github.com/RasputinKaiser/StorageScope/releases">all releases</a></p>
<footer class="site-footer">
<div>StorageScope · MIT License · <a href="https://github.com/RasputinKaiser/StorageScope">GitHub</a> · <a href="https://github.com/RasputinKaiser/StorageScope/releases">Releases</a> · <a href="privacy.html">Privacy</a> · <a href="changelog.html">Changelog</a> · <a href="faq.html">FAQ</a></div>
<div>v0.6.0 · macOS 14+ · SwiftUI/AppKit</div>
<div>v0.7.0 · macOS 14+ · SwiftUI/AppKit</div>
</footer>
</main>
</body>
Expand Down
4 changes: 2 additions & 2 deletions docs/faq.html
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ <h1>Frequently asked questions</h1>
<section class="qa"><h2>Do I need Full Disk Access?</h2><p>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.</p></section>
<section class="qa"><h2>Where do bug reports and feature requests go?</h2><p>Issues and pull requests are welcome on the <a href="https://github.com/RasputinKaiser/StorageScope">GitHub repository</a>. The full release history with notes and download checksums is on the <a href="https://github.com/RasputinKaiser/StorageScope/releases">GitHub Releases</a> page.</p></section>

<p class="actions-note"><a href="privacy.html">Privacy</a> &middot; <a href="changelog.html">Changelog</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.6.0/StorageScope-0.6.0.dmg">Download v0.6.0 (.dmg)</a></p>
<p class="actions-note"><a href="privacy.html">Privacy</a> &middot; <a href="changelog.html">Changelog</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases/download/v0.7.0/StorageScope-0.7.0.dmg">Download v0.7.0 (.dmg)</a></p>

<footer class="site-footer">
<div>StorageScope &middot; MIT License &middot; <a href="https://github.com/RasputinKaiser/StorageScope">GitHub</a> &middot; <a href="https://github.com/RasputinKaiser/StorageScope/releases">Releases</a> &middot; <a href="privacy.html">Privacy</a> &middot; <a href="changelog.html">Changelog</a> &middot; <a href="faq.html">FAQ</a></div>
<div>v0.6.0 &middot; macOS 14+ &middot; SwiftUI/AppKit</div>
<div>v0.7.0 &middot; macOS 14+ &middot; SwiftUI/AppKit</div>
</footer>
</main>
</body>
Expand Down
Loading
Loading