Files
stemify/ios/Widgets/StemifyWidgets.swift

101 lines
3.7 KiB
Swift

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<MixerEntry>) -> 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"
}
}
}