Adapter für alles inklusive Browser, AppleScript für Spotify und Musik, Medientasten als letzte Reserve. Die Reihenfolge ist keine Vorliebe, sondern Absicherung: Apple hat die Now-Playing-Schnittstelle in macOS 15.4 schon einmal zugemacht. Fällt der Adapter aus, verliert das Widget seine Anzeige — aber nicht seine Funktion. Deshalb bleiben die Transporttasten auch dann bedienbar, wenn gar keine Metadaten da sind. Der Fortschritt zählt zwischen den Ereignissen selbst hoch. Spike B hatte gezeigt, dass elapsedTime nicht laufend nachgeliefert wird: ein Scrubber, der stur den letzten Wert anzeigt, steht still, während der Titel läuft. Gerechnet wird aus asOf und playbackRate, begrenzt auf die Titellänge — sonst zeigt die Anzeige nach dem Ende weiter steigende Zahlen. Teilmeldungen (diff = true) ergänzen den Stand, statt ihn zu ersetzen. Eine Meldung mit nur der Position würde sonst Titel, Interpret und Cover löschen. In AnyDecodable wird Bool vor Double geprüft. Andernfalls wird "playing": true zu 1 und die Wiedergabe gilt für immer als angehalten. Play/Pause schaltet sofort um, statt auf das nächste Ereignis zu warten. Ein Knopf, der eine halbe Sekunde später reagiert, fühlt sich kaputt an; das echte Ereignis korrigiert den Stand ohnehin. Der Adapter liegt als Quellcode unter Vendor/ (BSD-3, Stand in ONYX-VERSION.txt) und wird beim Bauen kompiliert, statt als fertige Binary eingecheckt zu werden. Er wird bewusst nicht gelinkt: geladen wird er von /usr/bin/perl, im eigenen Prozess greift Apples Entitlement-Prüfung. 124 Tests grün.
71 lines
3.3 KiB
Swift
71 lines
3.3 KiB
Swift
import SwiftUI
|
|
import OnyxDesign
|
|
import OnyxWidgetKit
|
|
|
|
/// Vorläufige Widgets für Phase 2.
|
|
///
|
|
/// Sie tragen keine echten Daten — ihr Zweck ist zu belegen, dass Registry,
|
|
/// Layout-Engine, Persistenz und Panelgröße zusammenspielen, bevor die Provider
|
|
/// dazukommen. Jedes wird in seiner Phase durch das echte Widget ersetzt; die
|
|
/// Kennungen bleiben dabei gleich, damit gespeicherte Layouts weitergelten.
|
|
struct PlaceholderWidget: OnyxWidget {
|
|
let id: String
|
|
let displayName: String
|
|
let symbolName: String
|
|
let supportedSizes: [WidgetSize]
|
|
/// In welcher Phase dieses Widget echt wird.
|
|
let phase: String
|
|
|
|
func makeView(size: WidgetSize) -> AnyView {
|
|
AnyView(
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: symbolName)
|
|
.font(.system(size: 12, weight: .medium))
|
|
.foregroundStyle(Onyx.Color.accent)
|
|
Text(displayName)
|
|
.font(Onyx.Font.caption)
|
|
.foregroundStyle(Onyx.Color.textSecondary)
|
|
}
|
|
Spacer(minLength: 0)
|
|
Text(phase)
|
|
.font(Onyx.Font.caption)
|
|
.foregroundStyle(Onyx.Color.textTertiary)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
enum PlaceholderWidgets {
|
|
/// Registriert nur noch die Karten, deren echte Fassung fehlt. Sobald ein
|
|
/// Widget umgesetzt ist, verschwindet es hier — die Kennung bleibt gleich,
|
|
/// damit gespeicherte Layouts weitergelten.
|
|
static func registerAll() {
|
|
let widgets: [PlaceholderWidget] = [
|
|
.init(id: "cpu", displayName: "CPU", symbolName: "cpu",
|
|
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
|
.init(id: "gpu", displayName: "GPU", symbolName: "cpu.fill",
|
|
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
|
.init(id: "memory", displayName: "Speicher", symbolName: "memorychip",
|
|
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
|
.init(id: "battery", displayName: "Akku", symbolName: "battery.100",
|
|
supportedSizes: [.small, .medium], phase: "Phase 5"),
|
|
.init(id: "sensors", displayName: "Sensoren", symbolName: "thermometer",
|
|
supportedSizes: [.medium, .large], phase: "Phase 5"),
|
|
.init(id: "network", displayName: "Netzwerk", symbolName: "network",
|
|
supportedSizes: [.small, .medium, .large], phase: "Phase 5"),
|
|
.init(id: "fans", displayName: "Lüfter", symbolName: "fan",
|
|
supportedSizes: [.small, .medium], phase: "Phase 6"),
|
|
.init(id: "audio", displayName: "Audio-Mixer", symbolName: "slider.horizontal.3",
|
|
supportedSizes: [.medium, .large], phase: "Phase 7"),
|
|
.init(id: "shelf", displayName: "Ablage", symbolName: "tray.full",
|
|
supportedSizes: [.small, .medium], phase: "Phase 8"),
|
|
.init(id: "airdrop", displayName: "AirDrop", symbolName: "shareplay",
|
|
supportedSizes: [.small], phase: "Phase 8"),
|
|
]
|
|
widgets.forEach { WidgetRegistry.shared.register($0) }
|
|
}
|
|
}
|