Die Mediensteuerung stand nebeneinander: Cover links, alles andere in den verbleibenden gut hundert Punkten. Daher „Bernhard H…" statt eines Titels. Jetzt übereinander, wie es zu einer hochkanten Kachel passt. Das Cover ragte zusätzlich über die Kachel hinaus, weil `clipShape` vor dem Rahmen stand — beschnitten wurde damit auf die Eigengröße des Bildes, und die ist bei einem Podcast-Cover ein Vielfaches. Erst Rahmen, dann beschneiden. Menüleistensymbole waren kaum zu sehen. `color.set()` vor `image.draw(in:)` färbt ein Template-Bild nicht: es wird schwarz gezeichnet, auf einer dunklen Menüleiste also unsichtbar. AppKit färbt Template-Bilder nur ein, wenn sie als `image` eines Buttons gesetzt sind — in einer selbst gezeichneten Ansicht muss die Farbe in die Symbolkonfiguration. Betraf alle fünf Zeichenstellen, nicht nur die Lüfter. „fans.auto" stand als roher Schlüssel da: Xcode hatte ihn extrahiert, gefüllt wurde er nie. Beim Nachsehen waren es 35 solche Einträge über alle Kataloge hinweg — die meisten reine Formatschlüssel, die als Durchreicher trotzdem einen Wert brauchen. Der Automatik-Knopf ist jetzt eine gefüllte Pille, wenn sie greift. Blauer Text sieht aus wie ein Link und nicht wie ein Zustand. Und ein Absturz, der noch niemandem passiert ist: meldet der Helfer für einen stehenden Lüfter keine Grenzen, hätte `Slider(in: 0...0)` die App beendet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
3.3 KiB
Swift
106 lines
3.3 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
import AudioProvider
|
|
import OnyxMenuBar
|
|
import OnyxDesign
|
|
|
|
/// Der Lautstärkemixer als eigenes Element in der Menüleiste.
|
|
///
|
|
/// Bewusst getrennt vom Onyx-Element: den Ton einer einzelnen App leiser zu
|
|
/// drehen ist etwas, das man mitten in etwas anderem tut. Ein Umweg über ein
|
|
/// Menü und dann in ein Panel ist dafür zwei Schritte zu weit.
|
|
@MainActor
|
|
final class MixerMenuBarModule: MenuBarModule {
|
|
let id = "audio"
|
|
var displayName: String { String(localized: "mixer.menubar.name") }
|
|
|
|
private let mixer: AudioMixer
|
|
private var view: MixerStatusView?
|
|
private var isActive = false
|
|
|
|
init(mixer: AudioMixer) { self.mixer = mixer }
|
|
|
|
func makeStatusView(presentation: MenuBarPresentation) -> NSView {
|
|
let view = MixerStatusView()
|
|
view.update(isEnabled: mixer.isEnabled, managed: mixer.managed.count)
|
|
self.view = view
|
|
return view
|
|
}
|
|
|
|
func makePopoverView() -> AnyView {
|
|
AnyView(
|
|
ScrollView {
|
|
MixerView(mixer: mixer)
|
|
.padding(14)
|
|
}
|
|
.scrollIndicators(.automatic)
|
|
.frame(width: 280)
|
|
.frame(maxHeight: 420)
|
|
)
|
|
}
|
|
|
|
func activate() {
|
|
isActive = true
|
|
startObserving()
|
|
}
|
|
|
|
func deactivate() {
|
|
isActive = false
|
|
view = nil
|
|
}
|
|
|
|
/// Der Mixer misst ohnehin; hier wird nur zugehört. `withObservationTracking`
|
|
/// meldet sich einmal und muss danach neu eingerichtet werden.
|
|
private func startObserving() {
|
|
withObservationTracking {
|
|
_ = mixer.isEnabled
|
|
_ = mixer.managed
|
|
} onChange: {
|
|
Task { @MainActor [weak self] in
|
|
guard let self, isActive else { return }
|
|
view?.update(isEnabled: mixer.isEnabled, managed: mixer.managed.count)
|
|
startObserving()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Ein Glyph, mehr nicht.
|
|
///
|
|
/// Eine Zahl wäre hier sinnlos: „welche Lautstärke" hat bei einem Mischpult
|
|
/// keine einzelne Antwort. Der Zustand, der zählt, ist ob überhaupt etwas
|
|
/// geregelt wird — und den zeigt die Deckkraft.
|
|
private final class MixerStatusView: NSView {
|
|
private var isEnabled = false
|
|
private var managed = 0
|
|
|
|
init() {
|
|
super.init(frame: NSRect(x: 0, y: 0, width: MenuBarText.minimumWidth, height: 22))
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
override var intrinsicContentSize: NSSize {
|
|
NSSize(width: MenuBarText.minimumWidth, height: 22)
|
|
}
|
|
|
|
func update(isEnabled: Bool, managed: Int) {
|
|
self.isEnabled = isEnabled
|
|
self.managed = managed
|
|
needsDisplay = true
|
|
}
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
// Ausgeschaltet bleibt es sichtbar, aber zurückgenommen: das Element
|
|
// verschwinden zu lassen wäre die schlechtere Antwort — dann fände man
|
|
// den Weg zum Einschalten nicht mehr.
|
|
let color = (isEnabled && managed > 0)
|
|
? NSColor.labelColor
|
|
: NSColor.labelColor.withAlphaComponent(0.55)
|
|
MenuBarText.drawSymbol("slider.horizontal.3", color: color,
|
|
in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
|
width: 15, height: 15))
|
|
}
|
|
}
|