import Foundation /// One stem as the widgets draw it. [level] is the fader position (0.8 is unity), [percent] the gain the mixer shows. struct WidgetStem: Codable, Hashable, Identifiable { var name: String var label: String var color: UInt32 var level: Double var percent: Int var muted = false var solo = false var id: String { name } } /// What's playing and how it's mixed, written by the app for the widgets and the lock screen control. struct WidgetSnapshot: Codable, Equatable { var title = "" var artist = "" /// The current mix ("Karaoke", "Custom Mix"…), empty unless stems are playing. var mix = "" /// What the Mix button moves to. var next = "" var playing = false /// Empty unless stems are playing. var stems: [WidgetStem] = [] /// The art key the saved artwork belongs to, nil while it isn't saved yet. var art: String? var hasTrack: Bool { !title.isEmpty } var hasStems: Bool { !stems.isEmpty } /// Muted, or left out by another stem's solo. func silent(_ s: WidgetStem) -> Bool { s.muted || stems.contains(where: \.solo) && !s.solo } } enum WidgetStore { static let group = Bundle.main.object(forInfoDictionaryKey: "StemifyAppGroup") as? String ?? "group.org.cdnutter.stemify" static let mixerKind = "org.cdnutter.stemify.mixer" static let mixKind = "org.cdnutter.stemify.mix" static let controlKind = "org.cdnutter.stemify.mix-control" private static let key = "widgetSnapshot" /// Without the App Group (some sideloading tools drop it) this is a private store and the widgets show Nothing Playing. private static var defaults: UserDefaults? { UserDefaults(suiteName: group) } static var artURL: URL? { FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: group)?.appendingPathComponent("art.jpg") } static func load() -> WidgetSnapshot { guard let d = defaults?.data(forKey: key), let s = try? JSONDecoder().decode(WidgetSnapshot.self, from: d) else { return WidgetSnapshot() } return s } static func save(_ s: WidgetSnapshot) { guard let d = try? JSONEncoder().encode(s) else { return } defaults?.set(d, forKey: key) } }