Stemify iOS 1.1 and WebUI updates, repo restructure into desktop/ios/server/web
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
894dd5a695
commit
df5d847169
157 files changed
+3935
-742
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import AppIntents
|
||||
|
||||
/// What a widget or control button asks the player to do. [MixRemote] carries it out: the app's version drives
|
||||
/// the player, the widget extension's does nothing, since audio intents run in the app's process.
|
||||
enum MixAction: Sendable {
|
||||
case cycle
|
||||
case nudge(String, Int)
|
||||
case mute(String)
|
||||
case playPause
|
||||
}
|
||||
|
||||
struct CycleMixIntent: AudioPlaybackIntent {
|
||||
static let title: LocalizedStringResource = "Change Mix"
|
||||
static let description = IntentDescription("Moves to the next stem mix, in the order set in the Stem Editor.")
|
||||
|
||||
init() {}
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
await MixRemote.run(.cycle)
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct NudgeStemIntent: AudioPlaybackIntent {
|
||||
static let title: LocalizedStringResource = "Turn a Stem Up or Down"
|
||||
static let isDiscoverable = false
|
||||
|
||||
@Parameter(title: "Stem") var stem: String
|
||||
@Parameter(title: "Steps") var steps: Int
|
||||
|
||||
init() {}
|
||||
init(stem: String, steps: Int) {
|
||||
self.stem = stem
|
||||
self.steps = steps
|
||||
}
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
await MixRemote.run(.nudge(stem, steps))
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct MuteStemIntent: AudioPlaybackIntent {
|
||||
static let title: LocalizedStringResource = "Mute or Unmute a Stem"
|
||||
static let isDiscoverable = false
|
||||
|
||||
@Parameter(title: "Stem") var stem: String
|
||||
|
||||
init() {}
|
||||
init(stem: String) { self.stem = stem }
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
await MixRemote.run(.mute(stem))
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
struct PlayPauseIntent: AudioPlaybackIntent {
|
||||
static let title: LocalizedStringResource = "Play or Pause"
|
||||
static let isDiscoverable = false
|
||||
|
||||
init() {}
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
await MixRemote.run(.playPause)
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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 = "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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user