Create BearWave macOS radio-first app
This commit is contained in:
130
Sources/BearWaveCore/Stores/PersistenceStore.swift
Normal file
130
Sources/BearWaveCore/Stores/PersistenceStore.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
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<T: Decodable>(_ 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<T: Encodable>(_ 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user