diff --git a/Sources/Shotdeck/SettingsView.swift b/Sources/Shotdeck/SettingsView.swift index 5291120..9c8ba42 100644 --- a/Sources/Shotdeck/SettingsView.swift +++ b/Sources/Shotdeck/SettingsView.swift @@ -82,21 +82,20 @@ struct SettingsView: View { model.chooseOneDriveFolder() } } else { - HStack(spacing: 8) { - Text("No OneDrive folder found — sign in to OneDrive or choose a folder.") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .leading) - Button("Choose…") { model.chooseOneDriveFolder() } + // One-line row, same shape as the normal path row: "Not found" + // where the path would be, Choose… stays live. The explanation + // moves to the caption below instead of wrapping this row. + folderValue(path: "Not found") { + model.chooseOneDriveFolder() } - .frame(minHeight: 22) } } GridRow { Text( - "The PDF is saved here and this same folder is watched for the marked-up copy. On the iPad open it from Files > OneDrive." + model.resolvedOneDriveFolder != nil + ? "The PDF is saved here and this same folder is watched for the marked-up copy. On the iPad open it from Files > OneDrive." + : "No OneDrive folder found. Sign in to OneDrive, or choose a folder." ) .font(.caption) .foregroundStyle(.secondary) @@ -249,7 +248,18 @@ extension AppModel: SettingsWindowPresenting { /// doesn't exist yet, and re-points the running watcher (folder + recordUncommented) /// at the new state. Switching back to AirDrop restores its own stored overrides /// untouched, since AirDrop and OneDrive folder settings are stored under separate keys. + /// Refuses while a send is in flight (send() snapshots its own folder/transport, but + /// switching mid-send is still confusing UX — nothing to gain by allowing it). + /// The async reconcile below is generation-guarded: `reconcileGeneration` is bumped + /// synchronously before the Task starts, and the Task checks its own snapshot against + /// the live value before every mutating step, so rapid toggling (this function or + /// chooseOneDriveFolder, in any order) always lets the LAST choice win instead of an + /// earlier, superseded call applying its stale folder/flag after a later one already won. func chooseTransport(_ value: SendTransport) { + guard !isSending else { + setStatus("Finish the current send first.") + return + } guard value != transport else { return } TransportSettings.setTransport(value) setTransport(value) @@ -261,11 +271,17 @@ extension AppModel: SettingsWindowPresenting { } setFolderURLs(outbox: folders.outbox, watch: folders.watch) setResolvedOneDriveFolder(OneDriveLocator.resolveOneDriveFolder()) + + reconcileGeneration += 1 + let generation = reconcileGeneration Task { + guard generation == self.reconcileGeneration else { return } await watcher.setRecordUncommented(value == .airDrop) + guard generation == self.reconcileGeneration else { return } do { try await watcher.updateWatchFolder(folders.watch) } catch { + guard generation == self.reconcileGeneration else { return } setStatus( (error as? ShotdeckError)?.errorDescription ?? "Could not switch the watch folder." ) @@ -273,7 +289,13 @@ extension AppModel: SettingsWindowPresenting { } } + /// Refuses while a send is in flight, same reasoning as chooseTransport. See + /// chooseTransport's doc comment for the generation-guard mechanism shared here. func chooseOneDriveFolder() { + guard !isSending else { + setStatus("Finish the current send first.") + return + } let start = resolvedOneDriveFolder ?? FileManager.default.homeDirectoryForCurrentUser guard let url = chooseDirectory(startingAt: start) else { return } TransportSettings.setOneDriveFolder(url) @@ -281,11 +303,17 @@ extension AppModel: SettingsWindowPresenting { setResolvedOneDriveFolder(OneDriveLocator.resolveOneDriveFolder()) guard transport == .oneDrive else { return } setFolderURLs(outbox: url, watch: url) + + reconcileGeneration += 1 + let generation = reconcileGeneration Task { + guard generation == self.reconcileGeneration else { return } do { try await watcher.updateWatchFolder(url) + guard generation == self.reconcileGeneration else { return } setStatus("OneDrive folder set to \(url.lastPathComponent).") } catch { + guard generation == self.reconcileGeneration else { return } setStatus( (error as? ShotdeckError)?.errorDescription ?? "Could not switch the watch folder." )