AppDelegate.makeLaunchModel() built `paths` via the AirDrop-only FolderSettings.resolvedAppSupportPaths(), so ReturnWatcher's internal watchFolder (seeded from paths.watchFolder in its own init) was the AirDrop folder even when OneDrive was the persisted transport, and bootstrap() never reconciled it before starting. Net effect: on every relaunch with OneDrive selected, PDFs went to OneDrive but FSEvents kept watching the stale AirDrop folder for the whole session — marked-up returns were never detected. Fix: makeLaunchModel() now calls TransportSettings.resolvedAppSupportPaths() (single source of truth for the transport-folder mapping); made internal (not private) with an optional appSupportRoot override so PickerSelfTest's relaunch-simulation sub-step can call the exact same function against a temp root instead of the real Application Support folder. bootstrap() now unconditionally calls watcher.updateWatchFolder(watchFolderURL) before watcher.start() (belt-and-suspenders reconciliation, even though the paths fix alone already makes this a no-op in the normal case), and sets recordUncommented before start as it already did. Regression tests proving this land in the same PR (ReturnWatcherTests.swift): one characterizing the old bug's exact construction still missing a marked OneDrive-mode return, one proving the fixed launch-construction path detects it end to end. The ONEDRIVE-SELFTEST phase also gains a relaunch sub-step using this same makeLaunchModel() function. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
94 lines
3.7 KiB
Swift
94 lines
3.7 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
import ShotdeckCore
|
|
|
|
// SwiftPM treats a file named main.swift as top-level code, which forbids `@main`.
|
|
// App.main() is the equivalent entry point.
|
|
if ProcessInfo.processInfo.environment["SHOTDECK_SNAPSHOT_DIR"] != nil {
|
|
MainActor.assumeIsolated { PanelSnapshot.runIfRequested() }
|
|
}
|
|
if ProcessInfo.processInfo.environment["REDLINE_SELFTEST_PHASE"] == "onedrive" {
|
|
MainActor.assumeIsolated { PickerSelfTest.runOneDriveOnlyIfRequested() }
|
|
}
|
|
if ProcessInfo.processInfo.environment["SHOTDECK_PICKER_SELFTEST"] != nil {
|
|
MainActor.assumeIsolated { PickerSelfTest.runIfRequested() }
|
|
}
|
|
ShotdeckApp.main()
|
|
|
|
struct ShotdeckApp: App {
|
|
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
|
|
|
|
var body: some Scene {
|
|
MenuBarExtra {
|
|
MenuBarView()
|
|
.environment(appDelegate.model)
|
|
} label: {
|
|
let state = appDelegate.model.iconState
|
|
HStack(spacing: 4) {
|
|
Image(systemName: state.symbolName)
|
|
if let count = state.countText {
|
|
Text(count).font(.system(size: 11, weight: .semibold))
|
|
}
|
|
}
|
|
.accessibilityLabel("Redline")
|
|
}
|
|
.menuBarExtraStyle(.window)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
|
let model: AppModel
|
|
|
|
override init() {
|
|
NSApplication.shared.setActivationPolicy(.accessory)
|
|
model = AppDelegate.makeLaunchModel()
|
|
super.init()
|
|
}
|
|
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
Task { await model.bootstrap() }
|
|
}
|
|
|
|
/// Builds the model exactly the way the real app launches: paths come from
|
|
/// `TransportSettings.resolvedAppSupportPaths()` — transport-aware, so the watcher
|
|
/// this feeds is never seeded with a stale AirDrop folder while OneDrive is the
|
|
/// persisted transport (that was the BLOCKER this function used to have, when it
|
|
/// called the AirDrop-only `FolderSettings.resolvedAppSupportPaths()` instead).
|
|
/// `appSupportRoot` exists only so PickerSelfTest's relaunch-simulation sub-step can
|
|
/// point this at a temp directory instead of the real
|
|
/// ~/Library/Application Support/Shotdeck — production always calls this with no
|
|
/// argument (the real root). Internal, not private, for that same reason.
|
|
static func makeLaunchModel(appSupportRoot: URL? = nil) -> AppModel {
|
|
do {
|
|
let paths = try TransportSettings.resolvedAppSupportPaths(root: appSupportRoot)
|
|
return try makeModel(paths: paths)
|
|
} catch {
|
|
Log.ui.critical(
|
|
"AppModel init failed: \(String(describing: error), privacy: .public)"
|
|
)
|
|
let tmp = FileManager.default.temporaryDirectory
|
|
let fallbackRoot = tmp.appendingPathComponent("Shotdeck-fallback", isDirectory: true)
|
|
// Safe: temp-dir creation for a path this process controls cannot legitimately fail.
|
|
let fallback = try! AppSupportPaths(root: fallbackRoot, outbox: tmp, watchFolder: tmp)
|
|
let model = try! makeModel(paths: fallback)
|
|
model.setStatus("Redline could not access its storage folder. Captures will not persist.")
|
|
return model
|
|
}
|
|
}
|
|
|
|
private static func makeModel(paths: AppSupportPaths) throws -> AppModel {
|
|
let ledger = try ReturnLedger(paths: paths)
|
|
return AppModel(
|
|
paths: paths,
|
|
spool: try SpoolStore(paths: paths),
|
|
composer: PDFComposer(),
|
|
capturer: ScreenCapturer(),
|
|
hotkeys: HotkeyCenter(),
|
|
picker: RegionPickerController(),
|
|
ledger: ledger,
|
|
watcher: ReturnWatcher(paths: paths, ledger: ledger)
|
|
)
|
|
}
|
|
}
|