Mediensteuerung hochkant, Symbole wieder sichtbar, „Auto" als Zustand
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>
This commit is contained in:
@@ -98,22 +98,46 @@ private struct FanRow: View {
|
||||
.foregroundStyle(Onyx.Color.textPrimary)
|
||||
}
|
||||
|
||||
HStack(spacing: 6) {
|
||||
HStack(spacing: 8) {
|
||||
// Automatik ist der Ruhezustand und bleibt einen Klick entfernt:
|
||||
// wer manuell geregelt hat, muss ohne Nachdenken zurückkönnen.
|
||||
// Als gefüllte Pille, wenn sie greift — ein blauer Text sieht
|
||||
// aus wie ein Link und nicht wie ein Zustand.
|
||||
Button { control.setAutomatic(index: index) } label: {
|
||||
Text("fans.auto")
|
||||
.font(Onyx.Font.caption)
|
||||
.font(Onyx.Font.metricSmall)
|
||||
.foregroundStyle(fan.isManual ? Onyx.Color.textTertiary
|
||||
: Onyx.Color.accent)
|
||||
: Onyx.Color.surface)
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 2)
|
||||
.background {
|
||||
Capsule()
|
||||
.fill(fan.isManual ? Color.clear : Onyx.Color.accent)
|
||||
.overlay {
|
||||
Capsule().strokeBorder(
|
||||
fan.isManual ? Onyx.Color.hairline : .clear,
|
||||
lineWidth: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(!fan.isManual)
|
||||
|
||||
Slider(value: Binding(
|
||||
get: { fan.targetRPM },
|
||||
set: { control.setTarget(index: index, rpm: $0) }),
|
||||
in: fan.limits.minimum...fan.limits.maximum)
|
||||
.controlSize(.mini)
|
||||
// Ein Regler über einen leeren Bereich stürzt ab. Meldet der
|
||||
// Helfer keine brauchbaren Grenzen — etwa weil der Lüfter steht
|
||||
// und der SMC nichts hergibt —, gibt es nichts zu ziehen.
|
||||
if fan.limits.maximum > fan.limits.minimum {
|
||||
Slider(value: Binding(
|
||||
get: { min(max(fan.targetRPM, fan.limits.minimum), fan.limits.maximum) },
|
||||
set: { control.setTarget(index: index, rpm: $0) }),
|
||||
in: fan.limits.minimum...fan.limits.maximum)
|
||||
.controlSize(.mini)
|
||||
} else {
|
||||
Text("fans.noRange")
|
||||
.font(Onyx.Font.metricSmall)
|
||||
.foregroundStyle(Onyx.Color.textTertiary)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,19 +243,16 @@ private final class FanStatusView: NSView, WidthReporting {
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
let color = NSColor.labelColor
|
||||
guard let image = NSImage(systemSymbolName: "fan", accessibilityDescription: nil)
|
||||
else { return }
|
||||
image.isTemplate = true
|
||||
|
||||
if presentation == .symbol {
|
||||
color.set()
|
||||
image.draw(in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
||||
width: 15, height: 15))
|
||||
MenuBarText.drawSymbol("fan", color: color,
|
||||
in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
||||
width: 15, height: 15))
|
||||
return
|
||||
}
|
||||
|
||||
color.set()
|
||||
image.draw(in: NSRect(x: 0, y: bounds.midY - 6.5, width: 13, height: 13))
|
||||
MenuBarText.drawSymbol("fan", color: color,
|
||||
in: NSRect(x: 0, y: bounds.midY - 6.5, width: 13, height: 13))
|
||||
MenuBarText.draw(rpm.map { String(Int($0.rounded())) } ?? "–",
|
||||
color: color,
|
||||
in: NSRect(x: 15, y: 0, width: bounds.width - 15, height: bounds.height))
|
||||
|
||||
@@ -3,15 +3,75 @@
|
||||
"strings": {
|
||||
"%@": {
|
||||
"comment": "A label showing the minimum RPM of a fan.",
|
||||
"isCommentAutoGenerated": true
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ U/min": {
|
||||
"comment": "A label that shows the current speed of a fan. The argument is the current speed of the fan.",
|
||||
"isCommentAutoGenerated": true
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@ U/min"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@ U/min"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ °C": {
|
||||
"comment": "A label showing the temperature of the hottest fan in the report, in degrees Celsius. The argument is the temperature in degrees Celsius.",
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@ °C"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@ °C"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@°": {
|
||||
"comment": "A temperature in °C.",
|
||||
"isCommentAutoGenerated": true
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@°"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@°"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.approval.needed": {
|
||||
"extractionState": "stale",
|
||||
@@ -64,6 +124,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.auto": {
|
||||
"comment": "A button to set a fan to automatic mode.",
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Auto"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.connecting": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
@@ -80,7 +158,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.fan %@": {
|
||||
"comment": "A label showing the index of a fan. The argument is the index of the fan.",
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Lüfter %@"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Fan %@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.fan %lld": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -162,6 +259,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.manual": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Manuell"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.mode.auto": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
@@ -195,6 +308,7 @@
|
||||
}
|
||||
},
|
||||
"fans.noData": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -210,7 +324,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.noRange": {
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Kein Regelbereich"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "No adjustable range"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"fans.notInstalled": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -563,6 +694,7 @@
|
||||
}
|
||||
},
|
||||
"mixer.menubar.name": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -596,6 +728,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.allow": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -612,6 +745,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.audio": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -628,6 +762,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.audio.why": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -644,6 +779,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.calendar": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -660,6 +796,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.calendar.why": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -676,6 +813,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.done": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -692,6 +830,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.helper": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -708,6 +847,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.helper.why": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -740,6 +880,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.location": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -756,6 +897,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.location.why": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -772,6 +914,7 @@
|
||||
}
|
||||
},
|
||||
"onboarding.openSettings": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -951,6 +1094,7 @@
|
||||
}
|
||||
},
|
||||
"settings.general": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -1100,6 +1244,7 @@
|
||||
}
|
||||
},
|
||||
"settings.onboarding.show": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -1252,7 +1397,21 @@
|
||||
},
|
||||
"settings.shelf.count %@": {
|
||||
"comment": "A label showing the number of items in the shelf. The argument is the number of items in the shelf.",
|
||||
"isCommentAutoGenerated": true
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@ Dateien abgelegt"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "%@ files parked"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.shelf.count %lld": {
|
||||
"extractionState": "stale",
|
||||
@@ -1273,7 +1432,21 @@
|
||||
},
|
||||
"settings.shelf.days %@": {
|
||||
"comment": "A label that lets the user select how many days to wait before automatically removing items from the shelf. The argument is the number of days.",
|
||||
"isCommentAutoGenerated": true
|
||||
"isCommentAutoGenerated": true,
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Nach %@ Tagen"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "After %@ days"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.shelf.days %lld": {
|
||||
"extractionState": "stale",
|
||||
@@ -1579,6 +1752,7 @@
|
||||
}
|
||||
},
|
||||
"settings.widgets.hint.order": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
@@ -1629,6 +1803,7 @@
|
||||
}
|
||||
},
|
||||
"widget.fans.name": {
|
||||
"extractionState": "stale",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
|
||||
@@ -92,17 +92,14 @@ private final class MixerStatusView: NSView {
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
guard let image = NSImage(systemSymbolName: "slider.horizontal.3",
|
||||
accessibilityDescription: nil) else { return }
|
||||
image.isTemplate = true
|
||||
// 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)
|
||||
color.set()
|
||||
image.draw(in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
||||
width: 15, height: 15))
|
||||
MenuBarText.drawSymbol("slider.horizontal.3", color: color,
|
||||
in: NSRect(x: bounds.midX - 7.5, y: bounds.midY - 7.5,
|
||||
width: 15, height: 15))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user