Files
onyx/Onyx/AppModel.swift
Guido Schmit dd25d788c0 Phase 2 abgeschlossen: Einstellungsfenster und Lokalisierung
Einstellungen wirken sofort statt erst nach einem Neustart. In einer App ohne
Fenster ist eine verzögerte Änderung praktisch unauffindbar — man sieht ja
nicht, dass etwas passiert ist. AppModel meldet Layout- und Anzeigeänderungen
an den Koordinator, das Panel zeichnet sich neu und die Panelgröße zieht mit.

Widgets lassen sich hinzufügen, entfernen, in der Größe ändern und per
Drag-and-Drop umsortieren. Angeboten werden nur die Größen, die ein Widget
sinnvoll ausfüllt — ein Mini-Monat in 1x1 wäre unleserlich.

Lokalisierung als String Catalog in DE und EN, Quellsprache Englisch damit die
Schlüssel im Code lesbar bleiben. Der Katalog von OnyxCore musste ausdrücklich
als Ressource deklariert werden: ohne das gibt es kein Bundle.module und die
Übersetzungen wären zur Laufzeit unauffindbar. Im gebauten Bundle liegen jetzt
de.lproj und en.lproj, auch für das Paket.

Kein Menüleisten-Tab: solange es keine Module gibt, wäre das eine Oberfläche
für ein Feature, das noch nicht existiert. Er kommt mit Phase 5.

80 Tests grün.
2026-08-10 18:03:34 +02:00

99 lines
3.2 KiB
Swift

import SwiftUI
import OnyxNotch
import OnyxWidgetKit
/// Der geteilte Zustand von Onyx: was im Panel liegt und auf welchen Displays.
///
/// Änderungen wirken sofort. Eine Einstellung, die erst nach einem Neustart
/// greift, ist in einer App ohne Fenster praktisch unauffindbar man sieht ja
/// nicht, dass etwas passiert ist.
@MainActor
@Observable
final class AppModel {
private let store: LayoutStore
private let defaults: UserDefaults
/// Wird gerufen, wenn sich die Panelgröße geändert haben könnte.
var onLayoutChanged: (([WidgetPlacement]) -> Void)?
/// Wird gerufen, wenn Panels auf anderen Displays erscheinen sollen.
var onDisplayPolicyChanged: ((DisplayPolicy) -> Void)?
var layout: [WidgetPlacement] {
didSet {
guard layout != oldValue else { return }
persistLayout()
onLayoutChanged?(layout)
}
}
var displayPolicy: DisplayPolicy {
didSet {
guard displayPolicy != oldValue else { return }
defaults.set(displayPolicy.rawValue, forKey: Keys.displayPolicy)
onDisplayPolicyChanged?(displayPolicy)
}
}
var showsMenuBarIcon: Bool {
didSet { defaults.set(showsMenuBarIcon, forKey: Keys.showsMenuBarIcon) }
}
private enum Keys {
static let displayPolicy = "onyx.displayPolicy"
static let showsMenuBarIcon = "onyx.showsMenuBarIcon"
}
init(store: LayoutStore = .standard(), defaults: UserDefaults = .standard) {
self.store = store
self.defaults = defaults
let known = Set(WidgetRegistry.shared.all.map(\.id))
self.layout = store.load(knownWidgetIDs: known)
self.displayPolicy = defaults.string(forKey: Keys.displayPolicy)
.flatMap(DisplayPolicy.init(rawValue:)) ?? .builtInOnly
// Standardmäßig sichtbar: das Menüleistensymbol ist der einzige Weg
// zurück, wenn die Notch einmal nicht reagiert.
self.showsMenuBarIcon = defaults.object(forKey: Keys.showsMenuBarIcon) as? Bool ?? true
}
// MARK: - Widgets
var panelContentSize: CGSize { WidgetGrid.panelSize(for: layout) }
func isInPanel(_ widgetID: String) -> Bool {
layout.contains { $0.widgetID == widgetID }
}
func add(_ widget: any OnyxWidget) {
guard !isInPanel(widget.id) else { return }
let size = widget.supportedSizes.first ?? .small
layout.append(WidgetPlacement(widgetID: widget.id, size: size))
}
func remove(_ widgetID: String) {
layout.removeAll { $0.widgetID == widgetID }
}
func setSize(_ size: WidgetSize, for widgetID: String) {
guard let index = layout.firstIndex(where: { $0.widgetID == widgetID }) else { return }
layout[index].size = size
}
func move(fromOffsets source: IndexSet, toOffset destination: Int) {
layout.move(fromOffsets: source, toOffset: destination)
}
private func persistLayout() {
do {
try store.save(layout)
} catch {
// Ein fehlgeschlagener Schreibvorgang darf die App nicht anhalten;
// beim nächsten Start greift dann das Standardlayout.
NSLog("Onyx: Layout konnte nicht gespeichert werden — \(error.localizedDescription)")
}
}
}