import Foundation public struct AirAcceptConfig: Codable, Equatable, Sendable { public static let defaultPollIntervalMilliseconds = 2_500 public var pollIntervalMilliseconds: Int public init( pollIntervalMilliseconds: Int = Self.defaultPollIntervalMilliseconds ) { self.pollIntervalMilliseconds = pollIntervalMilliseconds } private enum CodingKeys: String, CodingKey { case pollIntervalMilliseconds = "poll_interval_ms" } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.pollIntervalMilliseconds = try container.decodeIfPresent( Int.self, forKey: .pollIntervalMilliseconds ) ?? Self.defaultPollIntervalMilliseconds } } public final class ConfigStore { public let configURL: URL private let fileManager: FileManager public init( configURL: URL = ConfigStore.defaultConfigURL(), fileManager: FileManager = .default ) { self.configURL = configURL self.fileManager = fileManager } public static func defaultConfigURL() -> URL { let baseURL = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/AirAccept", isDirectory: true) return baseURL.appendingPathComponent("config.json", isDirectory: false) } public func loadOrCreate() throws -> AirAcceptConfig { if !fileManager.fileExists(atPath: configURL.path) { let config = AirAcceptConfig() try save(config) return config } let data = try Data(contentsOf: configURL) var config = try JSONDecoder().decode(AirAcceptConfig.self, from: data) var shouldSave = false if config.pollIntervalMilliseconds == 1_000 { config.pollIntervalMilliseconds = AirAcceptConfig.defaultPollIntervalMilliseconds shouldSave = true } if let rawConfig = String(data: data, encoding: .utf8), rawConfig.contains("dry_run") || rawConfig.contains("fallback_click_visible_airplay_banner") { shouldSave = true } if shouldSave { try save(config) } return config } public func save(_ config: AirAcceptConfig) throws { try fileManager.createDirectory( at: configURL.deletingLastPathComponent(), withIntermediateDirectories: true ) let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] let data = try encoder.encode(config) try data.write(to: configURL, options: [.atomic]) } }