101 lines
3.7 KiB
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"
|
|
}
|
|
}
|
|
}
|