Kacheln und Fenster sprechen dieselbe Sprache
Alle Kacheln, Popovers und Fenster auf die Designschicht umgestellt: rund hundert gesetzte Schriftgrößen und 159 gesetzte Abstände und Radien sind jetzt Token. Übrig bleibt `spacing: 0` — das ist eine Aussage, keine geratene Zahl. Die Einstellungen bekommen eine Seitenleiste statt sieben Reitern, deren deutsche Beschriftungen zusammen 880 Punkte brauchten. Panel-Token stehen nicht mehr in Fenstern: dort gilt Systemsemantik, damit der Hellmodus nicht schwarz auf schwarz endet. Neu ist ein Schaufenster, das alle Bauteile nebeneinander zeigt — ohne das lässt sich eine Änderung an einem Token nur prüfen, indem man die App bedient, bis jede Kachel einmal zu sehen war. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9b600de9a9
commit
edb327cabe
@@ -44,6 +44,9 @@ public final class SystemAudio {
|
||||
public private(set) var inputIsAdjustable = false
|
||||
|
||||
private var listeners: [PropertyListener] = []
|
||||
/// Zuhörer, die am gewählten Gerät hängen und beim Wechsel neu gesetzt
|
||||
/// werden müssen.
|
||||
private var deviceListeners: [PropertyListener] = []
|
||||
|
||||
/// Die Pegel von Ausgabe und Eingabe.
|
||||
///
|
||||
@@ -59,6 +62,9 @@ public final class SystemAudio {
|
||||
// MARK: - Lesen
|
||||
|
||||
public func refresh() {
|
||||
let previousOutput = outputDeviceID
|
||||
let previousInput = inputDeviceID
|
||||
|
||||
outputDevices = Self.devices().filter(\.isOutput)
|
||||
inputDevices = Self.devices().filter(\.isInput)
|
||||
|
||||
@@ -79,6 +85,13 @@ public final class SystemAudio {
|
||||
alertVolume = alertDevice == outputDeviceID
|
||||
? nil // Dasselbe Gerät, derselbe Regler — keine zweite Zeile dafür.
|
||||
: Self.volume(alertDevice, scope: kAudioDevicePropertyScopeOutput)
|
||||
|
||||
// Nur bei einem echten Wechsel neu anmelden. Ein Abmelden mitten im
|
||||
// Rückruf, der gerade läuft, ist eine Einladung zum Absturz.
|
||||
if outputDeviceID != previousOutput || inputDeviceID != previousInput
|
||||
|| deviceListeners.isEmpty {
|
||||
observeCurrentDevices()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setzen
|
||||
@@ -135,6 +148,48 @@ public final class SystemAudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Zuhörer an Lautstärke und Stummschaltung der **gerade gewählten** Geräte.
|
||||
///
|
||||
/// **Das fehlte.** Beobachtet wurden nur Gerätewechsel; wer die Lautstärke
|
||||
/// per Tastatur änderte, bekam davon nichts mit — der Regler im Mixer stand
|
||||
/// dann auf einem Wert, den es nicht mehr gab, bis irgendetwas anderes ein
|
||||
/// `refresh()` auslöste.
|
||||
///
|
||||
/// Die Zuhörer hängen am Gerät, nicht am Systemobjekt, und müssen deshalb
|
||||
/// bei jedem Gerätewechsel neu gesetzt werden.
|
||||
private func observeCurrentDevices() {
|
||||
deviceListeners = []
|
||||
for (device, scope) in [(outputDeviceID, kAudioDevicePropertyScopeOutput),
|
||||
(inputDeviceID, kAudioDevicePropertyScopeInput)] {
|
||||
guard device != AudioObjectID(kAudioObjectUnknown) else { continue }
|
||||
for selector in [kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyMute] {
|
||||
// Manche Geräte führen keine Hauptlautstärke, sondern nur
|
||||
// Kanäle. Dort scheitert das Anmelden — das ist kein Fehler,
|
||||
// dort gibt es schlicht nichts zu hören.
|
||||
if let listener = PropertyListener(object: device,
|
||||
selector: selector,
|
||||
scope: scope,
|
||||
handler: { [weak self] in
|
||||
Task { @MainActor in self?.readLevels() }
|
||||
}) {
|
||||
deviceListeners.append(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Nur die Pegel und Schalter neu lesen — ohne Geräteliste.
|
||||
///
|
||||
/// Getrennt von `refresh()`, weil dieser Weg aus einem Rückruf kommt, der
|
||||
/// **während** einer Änderung feuert: dort auch noch die Geräte neu
|
||||
/// aufzuzählen wäre Arbeit für nichts.
|
||||
private func readLevels() {
|
||||
outputVolume = Self.volume(outputDeviceID, scope: kAudioDevicePropertyScopeOutput) ?? 0
|
||||
outputMuted = Self.muted(outputDeviceID, scope: kAudioDevicePropertyScopeOutput)
|
||||
inputVolume = Self.volume(inputDeviceID, scope: kAudioDevicePropertyScopeInput) ?? 0
|
||||
inputMuted = Self.muted(inputDeviceID, scope: kAudioDevicePropertyScopeInput)
|
||||
}
|
||||
|
||||
private static func devices() -> [AudioDevice] {
|
||||
var address = AudioObjectPropertyAddress(
|
||||
mSelector: kAudioHardwarePropertyDevices,
|
||||
@@ -325,14 +380,23 @@ public final class SystemAudio {
|
||||
/// geschrieben.
|
||||
private final class PropertyListener: @unchecked Sendable {
|
||||
|
||||
private let object = AudioObjectID(kAudioObjectSystemObject)
|
||||
private let object: AudioObjectID
|
||||
private var address: AudioObjectPropertyAddress
|
||||
private let block: AudioObjectPropertyListenerBlock
|
||||
|
||||
init?(selector: AudioObjectPropertySelector, handler: @escaping @Sendable () -> Void) {
|
||||
/// - Parameters:
|
||||
/// - object: Standardmäßig das Systemobjekt. Für Lautstärke und
|
||||
/// Stummschaltung muss es das **Gerät** sein — die sind keine
|
||||
/// Eigenschaften der Hardware im Allgemeinen, sondern dieser Kiste.
|
||||
/// - scope: Ein- oder Ausgabe. Global genügt nur bei Geräteeigenschaften.
|
||||
init?(object: AudioObjectID = AudioObjectID(kAudioObjectSystemObject),
|
||||
selector: AudioObjectPropertySelector,
|
||||
scope: AudioObjectPropertyScope = kAudioObjectPropertyScopeGlobal,
|
||||
handler: @escaping @Sendable () -> Void) {
|
||||
self.object = object
|
||||
address = AudioObjectPropertyAddress(
|
||||
mSelector: selector,
|
||||
mScope: kAudioObjectPropertyScopeGlobal,
|
||||
mScope: scope,
|
||||
mElement: kAudioObjectPropertyElementMain)
|
||||
block = { _, _ in handler() }
|
||||
guard AudioObjectAddPropertyListenerBlock(object, &address, .main, block) == noErr
|
||||
|
||||
Reference in New Issue
Block a user