import Foundation public struct PersistenceStore: Sendable { let applicationSupportDirectory: URL let cacheDirectory: URL let linuxConfigDirectory: URL public init( applicationSupportDirectory: URL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("BearWave"), cacheDirectory: URL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].appendingPathComponent("BearWave"), linuxConfigDirectory: URL = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".config/bearwave") ) { self.applicationSupportDirectory = applicationSupportDirectory self.cacheDirectory = cacheDirectory self.linuxConfigDirectory = linuxConfigDirectory } var apiCacheDirectory: URL { cacheDirectory.appendingPathComponent("api_cache") } func bootstrapImportIfNeeded() { if !FileManager.default.fileExists(atPath: favoritesURL.path) { copyIfPresent(from: linuxConfigDirectory.appendingPathComponent("favorites.json"), to: favoritesURL) } if !FileManager.default.fileExists(atPath: stateURL.path) { copyIfPresent(from: linuxConfigDirectory.appendingPathComponent("state.json"), to: stateURL) } } func loadFavorites() -> [RadioStation] { load([RadioStation].self, from: favoritesURL) ?? [] } func saveFavorites(_ stations: [RadioStation]) { save(stations, to: favoritesURL) } func loadManualStations() -> [RadioStation] { load([RadioStation].self, from: manualURL) ?? [] } func saveManualStations(_ stations: [RadioStation]) { save(stations, to: manualURL) } func loadGroups() -> [StationGroup] { load([StationGroup].self, from: groupsURL) ?? [] } func saveGroups(_ groups: [StationGroup]) { save(groups, to: groupsURL) } func loadPlaybackState() -> PlaybackState { load(PlaybackState.self, from: stateURL) ?? PlaybackState() } func savePlaybackState(_ state: PlaybackState) { save(state, to: stateURL) } private var favoritesURL: URL { applicationSupportDirectory.appendingPathComponent("favorites.json") } private var manualURL: URL { applicationSupportDirectory.appendingPathComponent("manual-stations.json") } private var stateURL: URL { applicationSupportDirectory.appendingPathComponent("state.json") } private var groupsURL: URL { applicationSupportDirectory.appendingPathComponent("groups.json") } private func load(_ type: T.Type, from url: URL) -> T? { guard let data = try? Data(contentsOf: url) else { return nil } return try? JSONDecoder().decode(type, from: data) } private func save(_ value: T, to url: URL) { do { try FileManager.default.createDirectory(at: applicationSupportDirectory, withIntermediateDirectories: true) let data = try JSONEncoder.prettyBearWave.encode(value) try data.write(to: url, options: .atomic) } catch { assertionFailure("Failed to save \(url.lastPathComponent): \(error)") } } private func copyIfPresent(from source: URL, to destination: URL) { guard FileManager.default.fileExists(atPath: source.path) else { return } do { try FileManager.default.createDirectory(at: destination.deletingLastPathComponent(), withIntermediateDirectories: true) try FileManager.default.copyItem(at: source, to: destination) } catch { assertionFailure("Failed to import \(source.lastPathComponent): \(error)") } } } struct PlaybackState: Codable, Equatable, Sendable { var lastStationName: String var lastStationURL: String var volume: Double var recentStations: [RadioStation] init( lastStationName: String = "", lastStationURL: String = "", volume: Double = 0.55, recentStations: [RadioStation] = [] ) { self.lastStationName = lastStationName self.lastStationURL = lastStationURL self.volume = volume self.recentStations = recentStations } } extension JSONEncoder { static var prettyBearWave: JSONEncoder { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] return encoder } }