Neben ihr wären die Arme zwangsläufig gleich breit — die Silhouette zeichnet den Steg mittig um die Hardware. Ein Symbol links und ein Balken rechts hieße dann 132 Punkte Schwarz für ein Symbol. Darunter hängt die Fläche frei und wird so breit, wie ihr Inhalt es braucht. Lautstärke, Strom, Titelwechsel und Helligkeit. Gemeldet wird nur, was sich **geändert** hat: `PowerWatcher` schweigt beim ersten gelesenen Zustand, sonst begrüßte Onyx jeden Start mit „am Netz". Die Helligkeit kostete einen Spike. Von drei Wegen trägt nur `DisplayServices`, und nur für den eingebauten Bildschirm. `CoreDisplay` — der Weg, den jede Anleitung empfiehlt — gibt für alle Bildschirme 1.0 zurück, auch für den, der auf 62 % stand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
97 lines
3.5 KiB
Swift
97 lines
3.5 KiB
Swift
import Foundation
|
|
|
|
/// Ein Anspruch auf die eingeklappte Notch.
|
|
///
|
|
/// Bewusst ohne Darstellung: was eine Live Activity *zeigt*, ist eine View und
|
|
/// gehört nicht in die Entscheidung, **welche** gerade gilt. So lässt sich die
|
|
/// Regel ohne Bildschirm prüfen — wie `NotchStateMachine` und `LayoutEngine`.
|
|
public struct ActivityTicket: Equatable, Sendable, Identifiable {
|
|
|
|
public let id: String
|
|
/// Höher verdrängt niedriger.
|
|
public let rank: Int
|
|
/// Wann angemeldet.
|
|
public let raised: Date
|
|
/// Wie lange sie steht. `nil` heißt: bis sie zurückgezogen wird.
|
|
public let duration: TimeInterval?
|
|
|
|
public init(id: String, rank: Int, raised: Date, duration: TimeInterval?) {
|
|
self.id = id
|
|
self.rank = rank
|
|
self.raised = raised
|
|
self.duration = duration
|
|
}
|
|
|
|
/// Wann sie von selbst geht. `nil` bei dauerhaften.
|
|
public var expiry: Date? {
|
|
duration.map { raised.addingTimeInterval($0) }
|
|
}
|
|
|
|
public func hasExpired(at now: Date) -> Bool {
|
|
guard let expiry else { return false }
|
|
return now >= expiry
|
|
}
|
|
}
|
|
|
|
/// Wer die eingeklappte Notch gerade bekommt.
|
|
///
|
|
/// **Warum überhaupt eine Warteschlange.** Die Anlässe kommen unabhängig
|
|
/// voneinander: die Lautstärke ändert sich, während ein Titel wechselt, während
|
|
/// das Netzteil angesteckt wird. Ohne Regel gewinnt, wer zuletzt kam — und dann
|
|
/// verschluckt ein beiläufiger Lautstärkeschritt die Meldung, dass der Akku
|
|
/// fast leer ist.
|
|
///
|
|
/// **Die Regel ist Rang vor Aktualität.** Bei gleichem Rang gilt die jüngere:
|
|
/// zwei Lautstärkeschritte hintereinander sollen den zweiten zeigen, nicht den
|
|
/// ersten stehen lassen.
|
|
public struct ActivityQueue: Equatable, Sendable {
|
|
|
|
private var tickets: [ActivityTicket] = []
|
|
|
|
public init() {}
|
|
|
|
/// Alles, was gerade anliegt — ungefiltert, in Anmeldereihenfolge.
|
|
public var all: [ActivityTicket] { tickets }
|
|
|
|
/// Meldet einen Anspruch an. Ein bereits vorhandener mit derselben Kennung
|
|
/// wird **ersetzt**, nicht ergänzt.
|
|
///
|
|
/// Das ist der Normalfall und keine Ausnahme: wer die Lautstärke dreimal
|
|
/// verstellt, meldet dreimal dieselbe Activity an und will dreimal die
|
|
/// volle Standzeit — nicht drei Einträge in einer Schlange.
|
|
public mutating func raise(_ ticket: ActivityTicket) {
|
|
tickets.removeAll { $0.id == ticket.id }
|
|
tickets.append(ticket)
|
|
}
|
|
|
|
public mutating func withdraw(id: String) {
|
|
tickets.removeAll { $0.id == id }
|
|
}
|
|
|
|
/// Wirft weg, was abgelaufen ist.
|
|
public mutating func prune(now: Date) {
|
|
tickets.removeAll { $0.hasExpired(at: now) }
|
|
}
|
|
|
|
/// Welche gerade gilt.
|
|
public func current(now: Date) -> ActivityTicket? {
|
|
tickets
|
|
.filter { !$0.hasExpired(at: now) }
|
|
// `last` statt `max(by:)`: bei gleichem Rang gewinnt die zuletzt
|
|
// angemeldete, und `tickets` steht in Anmeldereihenfolge.
|
|
.reduce(into: ActivityTicket?.none) { best, candidate in
|
|
guard let current = best else { best = candidate; return }
|
|
if candidate.rank >= current.rank { best = candidate }
|
|
}
|
|
}
|
|
|
|
/// Wann das nächste Mal etwas von selbst verschwindet.
|
|
///
|
|
/// Der Presenter stellt darauf seinen Wecker. Ohne diese Auskunft müsste er
|
|
/// abtasten — und eine Notch, die zehnmal je Sekunde nachfragt, ob etwas
|
|
/// abgelaufen ist, hält den Rechner wach.
|
|
public func nextExpiry(after now: Date) -> Date? {
|
|
tickets.compactMap(\.expiry).filter { $0 > now }.min()
|
|
}
|
|
}
|