feature: model-owned resolvedOneDriveFolder + OneDrive Settings/menu panel snapshots

SettingsView's OneDrive row called OneDriveLocator.resolveOneDriveFolder()
directly with the real UserDefaults.standard and the real home directory,
which made that row impossible to drive from a fake/isolated environment.
Moved that resolution into AppModel as a tracked resolvedOneDriveFolder
property (nil means "no OneDrive folder found"), refreshed at init,
bootstrap, chooseTransport, chooseOneDriveFolder, and inside send()'s live
folder check. SettingsView and chooseOneDriveFolder's picker-start path now
read model.resolvedOneDriveFolder instead of calling OneDriveLocator
directly — state flows through the model like everything else in this app.

PanelSnapshot (SHOTDECK_SNAPSHOT_DIR) adds three panels on a SEPARATE
isolated model so the transport switch never bleeds into the six existing
AirDrop-mode panels:
- panel-07-settings-onedrive.png: transport=oneDrive with a resolved folder,
  built by pointing OneDriveLocator.defaultRedlineFolder at a fake home tree
  (Library/CloudStorage/OneDrive-MMDGROUP under this snapshot's own temp
  root) so the displayed path is shaped like the real default without ever
  touching the real home.
- panel-08-settings-onedrive-missing.png: a fake home with no
  Library/CloudStorage at all, resolved through a throwaway UserDefaults
  suite (never .standard) so the "no OneDrive folder found" state and its
  still-usable Choose... button are exercised for real.
- panel-09-captures-present-onedrive.png: 3 captures + transport=oneDrive,
  confirming the menu row reads "Send to OneDrive".

Extracted the 3-swatch capture seeding (panel 04) into addSampleCaptures(to:)
so panel 09 reuses it instead of duplicating the loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
This commit is contained in:
2026-09-05 09:10:59 +04:00
co-authored by Claude Fable 5.1
parent 2937d2d5e0
commit 209921f084
4 changed files with 106 additions and 15 deletions
+8
View File
@@ -35,6 +35,11 @@ public final class AppModel {
/// Live transport choice; WP-onedrive reads this to pick the send path and to drive
/// the Settings "Send via" picker and the menu's "Send" label.
public private(set) var transport: SendTransport
/// Ground truth for the Settings OneDrive row: nil means "no OneDrive folder found".
/// Views read this instead of calling `OneDriveLocator.resolveOneDriveFolder()`
/// directly, so state (and testing with a fake home) flows through the model like
/// everything else never a View reaching past the model for real UserDefaults/home.
public private(set) var resolvedOneDriveFolder: URL?
/// Live outbox; WP-4b reads this (not `paths.outbox`) so Settings folder changes take effect.
public private(set) var outboxURL: URL
/// Live watch folder; WP-4c updates this alongside `ReturnWatcher.updateWatchFolder`.
@@ -93,6 +98,7 @@ public final class AppModel {
self.outboxDisplayName = folders.outbox.lastPathComponent
self.watchFolderDisplayName = folders.watch.lastPathComponent
self.transport = folders.transport
self.resolvedOneDriveFolder = OneDriveLocator.resolveOneDriveFolder()
self.captureHotkey = HotkeyPreference.load()
self.updateChecker = UpdateChecker()
self.updateChecker.onChecked = { [weak self] in
@@ -125,6 +131,7 @@ public final class AppModel {
setFolderDisplayNames(outbox: outbox.lastPathComponent, watch: watch.lastPathComponent)
}
func setTransport(_ value: SendTransport) { transport = value }
func setResolvedOneDriveFolder(_ value: URL?) { resolvedOneDriveFolder = value }
func rememberLastComposedPDF(_ url: URL) { lastComposedPDFURL = url }
/// True when a last-composed PDF path is known this run, or the newest
@@ -200,6 +207,7 @@ public final class AppModel {
if transport == .oneDrive {
try? FileManager.default.createDirectory(at: outboxURL, withIntermediateDirectories: true)
}
setResolvedOneDriveFolder(OneDriveLocator.resolveOneDriveFolder())
await watcher.setRecordUncommented(transport == .airDrop)
do {