import Foundation /// The two ways a composed PDF can reach the iPad and come back marked up. public enum SendTransport: String, Codable, Sendable, CaseIterable { case airDrop case oneDrive public var displayName: String { switch self { case .airDrop: return "AirDrop" case .oneDrive: return "OneDrive folder" } } } /// User-configurable transport choice plus the OneDrive folder override, backed by /// UserDefaults the same way `FolderSettings` is. See `FolderSettings` for why a plain /// path (not a security-scoped bookmark) is correct for this unsandboxed app. public enum TransportSettings { public static let transportDefaultsKey = "ai.flowmaster.shotdeck.transport" public static let oneDriveFolderDefaultsKey = "ai.flowmaster.shotdeck.oneDriveFolder" /// Defaults to `.airDrop` when unset or when the stored value cannot be parsed. public static func transport(defaults: UserDefaults = .standard) -> SendTransport { guard let raw = defaults.string(forKey: transportDefaultsKey), let value = SendTransport(rawValue: raw) else { return .airDrop } return value } public static func setTransport(_ value: SendTransport, defaults: UserDefaults = .standard) { defaults.set(value.rawValue, forKey: transportDefaultsKey) } /// Raw stored path (or nil if never set / cleared). Does NOT validate that the /// directory still exists. public static func storedOneDriveFolderPath(defaults: UserDefaults = .standard) -> String? { defaults.string(forKey: oneDriveFolderDefaultsKey) } public static func setOneDriveFolder(_ url: URL, defaults: UserDefaults = .standard) { defaults.set(url.path, forKey: oneDriveFolderDefaultsKey) } public static func resetOneDriveFolder(defaults: UserDefaults = .standard) { defaults.removeObject(forKey: oneDriveFolderDefaultsKey) } /// The outbox/watch folders Redline should actually use right now, for the current /// transport. AirDrop mode delegates to `FolderSettings.resolve()` unchanged. /// OneDrive mode uses the SAME folder for both outbox and watch — see /// `OneDriveLocator.resolveOneDriveFolder`. When no OneDrive folder can be resolved /// at all (no sync root, no override), this falls back to the AirDrop folders so the /// app always has somewhere to write; `send(anchor:)` performs its own live /// existence check before ever composing into a OneDrive send, so that fallback is /// never mistaken for a valid OneDrive destination. public static func effectiveFolders( defaults: UserDefaults = .standard, fileManager: FileManager = .default ) -> (outbox: URL, watch: URL, transport: SendTransport) { let transport = transport(defaults: defaults) switch transport { case .airDrop: let folders = FolderSettings.resolve(defaults: defaults, fileManager: fileManager) return (folders.outbox, folders.watch, transport) case .oneDrive: if let folder = OneDriveLocator.resolveOneDriveFolder( defaults: defaults, home: fileManager.homeDirectoryForCurrentUser, fileManager: fileManager ) { return (folder, folder, transport) } let folders = FolderSettings.resolve(defaults: defaults, fileManager: fileManager) return (folders.outbox, folders.watch, transport) } } } /// Pure path logic for locating a OneDrive sync root under /// `~/Library/CloudStorage` and the Redline folder inside it. No side effects — never /// creates a directory. Fully unit-testable with a fake home tree. public enum OneDriveLocator { /// Every directory directly under `/Library/CloudStorage` whose name starts /// with "OneDrive-", sorted so a name containing "MMD" (case-insensitive) sorts /// first, then alphabetically. Empty when CloudStorage does not exist. public static func syncRoots( home: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default ) -> [URL] { let cloudStorage = home.appendingPathComponent("Library/CloudStorage", isDirectory: true) var isDirectory: ObjCBool = false guard fileManager.fileExists(atPath: cloudStorage.path, isDirectory: &isDirectory), isDirectory.boolValue else { return [] } let items = (try? fileManager.contentsOfDirectory( at: cloudStorage, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles] )) ?? [] let roots = items.filter { url in guard url.lastPathComponent.hasPrefix("OneDrive-") else { return false } var itemIsDirectory: ObjCBool = false let exists = fileManager.fileExists(atPath: url.path, isDirectory: &itemIsDirectory) return exists && itemIsDirectory.boolValue } return roots.sorted { a, b in let aName = a.lastPathComponent let bName = b.lastPathComponent let aIsMMD = aName.localizedCaseInsensitiveContains("MMD") let bIsMMD = bName.localizedCaseInsensitiveContains("MMD") if aIsMMD != bIsMMD { return aIsMMD } return aName.localizedStandardCompare(bName) == .orderedAscending } } /// First sync root's "Redline" subfolder, or nil when there is no sync root at all. public static func defaultRedlineFolder( home: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default ) -> URL? { guard let first = syncRoots(home: home, fileManager: fileManager).first else { return nil } return first.appendingPathComponent("Redline", isDirectory: true) } /// The stored override when it is set AND still exists as a directory; otherwise /// `defaultRedlineFolder`. Never creates anything. public static func resolveOneDriveFolder( defaults: UserDefaults = .standard, home: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default ) -> URL? { if let storedPath = TransportSettings.storedOneDriveFolderPath(defaults: defaults) { var isDirectory: ObjCBool = false let exists = fileManager.fileExists(atPath: storedPath, isDirectory: &isDirectory) if exists, isDirectory.boolValue { return URL(fileURLWithPath: storedPath, isDirectory: true) } } return defaultRedlineFolder(home: home, fileManager: fileManager) } }