Phase 4: Medien-Widget mit dreistufiger Kaskade

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.
This commit is contained in:
Guido Schmit
2026-08-10 21:02:13 +02:00
parent 03dd7b945e
commit 433cdcc3dd
53 changed files with 4888 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import OnyxMenuBar
import OnyxWidgetKit
import CalendarProvider
import WeatherProvider
import MediaProvider
@main
struct OnyxApp: App {
@@ -32,6 +33,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var onyxItem: NSStatusItem?
private var calendarModel: CalendarModel?
private var weatherModel: WeatherModel?
private var mediaModel: MediaModel?
private let settingsWindow = SettingsWindowController()
func applicationDidFinishLaunching(_ notification: Notification) {
@@ -127,6 +129,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
self.weatherModel = weatherModel
WidgetRegistry.shared.register(WeatherWidget(model: weatherModel))
let mediaModel = MediaModel()
self.mediaModel = mediaModel
WidgetRegistry.shared.register(MediaWidget(model: mediaModel))
PlaceholderWidgets.registerAll()
}

View File

@@ -44,8 +44,6 @@ enum PlaceholderWidgets {
/// damit gespeicherte Layouts weitergelten.
static func registerAll() {
let widgets: [PlaceholderWidget] = [
.init(id: "media", displayName: "Medien", symbolName: "play.circle",
supportedSizes: [.medium, .wide], phase: "Phase 4"),
.init(id: "cpu", displayName: "CPU", symbolName: "cpu",
supportedSizes: [.small, .medium], phase: "Phase 5"),
.init(id: "gpu", displayName: "GPU", symbolName: "cpu.fill",