import SwiftUI import WidgetKit @main struct StemifyWidgets: WidgetBundle { var body: some Widget { MixerWidget() MixWidget() if #available(iOS 18.0, *) { MixControl() } } } /// Audio intents run in the app's process, so the extension's copy never does anything. @MainActor enum MixRemote { static func run(_ a: MixAction) {} } // ---- timeline ----------------------------------------------------------------------------- struct MixerEntry: TimelineEntry { var date = Date() var snap: WidgetSnapshot var art: UIImage? /// The app reloads the widgets whenever the song or mix changes, so one entry is enough. static func now() -> MixerEntry { let s = WidgetStore.load() let art = s.art == nil ? nil : WidgetStore.artURL.flatMap { UIImage(contentsOfFile: $0.path) } return MixerEntry(snap: s, art: art) } static let sample = MixerEntry(snap: WidgetSnapshot( title: "Midnight City", artist: "M83", mix: "Karaoke", next: "A Cappella", playing: true, stems: [ WidgetStem(name: "vocals", label: "Vocals", color: 0xFF7AB6, level: 0.8, percent: 100, muted: true), WidgetStem(name: "drums", label: "Drums", color: 0xFFB547, level: 0.8, percent: 100), WidgetStem(name: "bass", label: "Bass", color: 0x7C8CFF, level: 0.9, percent: 127), WidgetStem(name: "other", label: "Other", color: 0x4CD07D, level: 0.7, percent: 77), ])) } struct MixerProvider: TimelineProvider { func placeholder(in context: Context) -> MixerEntry { .sample } func getSnapshot(in context: Context, completion: @escaping (MixerEntry) -> Void) { let e = MixerEntry.now() completion(context.isPreview && !e.snap.hasStems ? .sample : e) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { completion(Timeline(entries: [.now()], policy: .never)) } } // ---- widgets ------------------------------------------------------------------------------ struct MixerWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: WidgetStore.mixerKind, provider: MixerProvider()) { e in MixerView(entry: e).containerBackground(for: .widget) { Look.bg } } .configurationDisplayName("Stem Mixer") .description("A knob for each stem. Tap the arrows to turn it, tap the knob to mute it.") .supportedFamilies([.systemMedium, .systemLarge]) } } struct MixWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: WidgetStore.mixKind, provider: MixerProvider()) { e in MixView(entry: e) } .configurationDisplayName("Mix") .description("The mix that's playing, and a button for the next one.") .supportedFamilies([.systemSmall, .accessoryRectangular, .accessoryCircular]) } } /// Swaps the Lock Screen's flashlight or camera for a Mix button. Also works in Control Center and on the Action button. @available(iOS 18.0, *) struct MixControl: ControlWidget { var body: some ControlWidgetConfiguration { StaticControlConfiguration(kind: WidgetStore.controlKind, provider: Provider()) { mix in ControlWidgetButton(action: CycleMixIntent()) { Label(mix, systemImage: "slider.vertical.3") } } .displayName("Change Mix") .description("Moves to the next stem mix while stems play.") } struct Provider: ControlValueProvider { var previewValue: String { "Karaoke" } func currentValue() async throws -> String { let s = WidgetStore.load() return s.hasStems ? s.mix : "Mix" } } }