Phase 8: Ablage, AirDrop und Calendarr
Calendarr liefert Termine über den Gruppencontainer. Streng lesend: keine eigene Anmeldung, kein Netz, kein Schreiben zurück. Die Abdeckung ist der Kern und kein Detail. Der Schnappschuss reicht 49 Tage nach vorne, und das steht nirgends in der Datei — es ist zugesagtes Verhalten des Schreibers. Außerhalb liegt keine Information vor, nicht keine Termine; das Monatsraster graut solche Tage aus, statt „frei" zu behaupten. Verlockend wäre, die Abdeckung aus dem frühesten Termin abzuleiten. Das ist falsch, und ein Test hält es fest: mehrtägige Termine ragen ins Fenster hinein und werden mitgeschrieben, obwohl ihr Anfang nicht abgedeckt ist. Bei „beide Quellen" ist die Abdeckung der Schnitt, nicht die Vereinigung. Ein Tag gilt nur als bekannt, wenn ihn jede Quelle abdeckt. Umgekehrt darf ein Ausfall einer Quelle die andere nicht mitreißen: Calendarr nie geöffnet zu haben knipst den Apple-Kalender nicht aus. Die Ablage kopiert, statt zu verweisen. Anhänge aus Mail oder WhatsApp liegen in temporären Verzeichnissen — ein Verweis wäre in Stunden tot, und die Ablage wäre leer, wenn man sie braucht. Aus demselben Grund nimmt die Fläche UTType.item statt public.file-url an: solche Anhänge kommen als Dateiversprechen, und loadFileRepresentation löst beides ein. Die übergebene Datei existiert nur innerhalb des Rückrufs, also wird sie dort sofort gesichert statt später auf dem MainActor. Gelöscht wird in den Papierkorb. Automatisches Aufräumen ist standardmäßig aus — eine Ablage, die ungefragt löscht, ist ein Papierkorb mit Zeitschaltuhr. Nebenbei: „mixer.on" und „network.vpn.active" standen als rohe Schlüssel in der Oberfläche. Label(_:systemImage:) kennt kein Bundle und sucht immer im Hauptbundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import MediaProvider
|
||||
import MetricsProvider
|
||||
import NetworkProvider
|
||||
import AudioProvider
|
||||
import ShelfProvider
|
||||
|
||||
@main
|
||||
struct OnyxApp: App {
|
||||
@@ -39,6 +40,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var mediaModel: MediaModel?
|
||||
private var metricsModel: MetricsModel?
|
||||
private var networkModel: NetworkModel?
|
||||
private var shelfStore: ShelfStore?
|
||||
private let settingsWindow = SettingsWindowController()
|
||||
private let fanControl = FanControl()
|
||||
private let audioMixer = AudioMixer()
|
||||
@@ -150,7 +152,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
/// Echte Widgets zuerst, danach die Platzhalter für alles, was noch fehlt.
|
||||
private func registerWidgets() {
|
||||
let calendarModel = CalendarModel(source: EventKitSource())
|
||||
let calendarModel = CalendarModel()
|
||||
self.calendarModel = calendarModel
|
||||
WidgetRegistry.shared.register(CalendarWidget(model: calendarModel))
|
||||
|
||||
@@ -174,6 +176,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
WidgetRegistry.shared.register(AudioMixerWidget(mixer: audioMixer))
|
||||
|
||||
let shelfStore = ShelfStore()
|
||||
self.shelfStore = shelfStore
|
||||
WidgetRegistry.shared.register(ShelfWidget(store: shelfStore))
|
||||
WidgetRegistry.shared.register(AirDropWidget())
|
||||
|
||||
PlaceholderWidgets.registerAll()
|
||||
}
|
||||
|
||||
@@ -272,7 +279,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
guard let model, let calendarModel, let weatherModel else { return }
|
||||
settingsWindow.show(model: model, calendarModel: calendarModel,
|
||||
weatherModel: weatherModel, metricsModel: metricsModel,
|
||||
fanControl: fanControl, tab: tab)
|
||||
shelfStore: shelfStore, fanControl: fanControl, tab: tab)
|
||||
}
|
||||
|
||||
@objc private func quit() { NSApp.terminate(nil) }
|
||||
|
||||
@@ -46,10 +46,6 @@ enum PlaceholderWidgets {
|
||||
let widgets: [PlaceholderWidget] = [
|
||||
.init(id: "fans", displayName: "Lüfter", symbolName: "fan",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 6"),
|
||||
.init(id: "shelf", displayName: "Ablage", symbolName: "tray.full",
|
||||
supportedSizes: [.small, .medium], phase: "Phase 8"),
|
||||
.init(id: "airdrop", displayName: "AirDrop", symbolName: "shareplay",
|
||||
supportedSizes: [.small], phase: "Phase 8"),
|
||||
]
|
||||
widgets.forEach { WidgetRegistry.shared.register($0) }
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ import OnyxDesign
|
||||
import OnyxNotch
|
||||
import OnyxWidgetKit
|
||||
import CalendarProvider
|
||||
import ShelfProvider
|
||||
import OnyxMenuBar
|
||||
import MetricsProvider
|
||||
import OnyxHelperProtocol
|
||||
import WeatherProvider
|
||||
|
||||
enum SettingsTab: Hashable {
|
||||
case widgets, display, menubar, fans, permissions
|
||||
case widgets, display, sources, menubar, fans, permissions
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@@ -17,17 +18,21 @@ struct SettingsView: View {
|
||||
let calendarModel: CalendarModel
|
||||
let weatherModel: WeatherModel
|
||||
let metricsModel: MetricsModel?
|
||||
let shelfStore: ShelfStore?
|
||||
let fanControl: FanControl
|
||||
@Binding var selectedTab: SettingsTab
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
WidgetSettings(model: model)
|
||||
WidgetSettings(model: model, shelfStore: shelfStore)
|
||||
.tabItem { Label("settings.tab.widgets", systemImage: "square.grid.2x2") }
|
||||
.tag(SettingsTab.widgets)
|
||||
DisplaySettings(model: model)
|
||||
.tabItem { Label("settings.tab.display", systemImage: "macbook") }
|
||||
.tag(SettingsTab.display)
|
||||
CalendarSourceSettings(model: calendarModel)
|
||||
.tabItem { Label("settings.tab.sources", systemImage: "calendar.badge.clock") }
|
||||
.tag(SettingsTab.sources)
|
||||
MenuBarSettings(model: model, metricsModel: metricsModel)
|
||||
.tabItem { Label("settings.tab.menubar", systemImage: "menubar.rectangle") }
|
||||
.tag(SettingsTab.menubar)
|
||||
@@ -279,6 +284,7 @@ extension MenuBarPresentation {
|
||||
|
||||
private struct WidgetSettings: View {
|
||||
@Bindable var model: AppModel
|
||||
let shelfStore: ShelfStore?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -317,6 +323,12 @@ private struct WidgetSettings: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let shelfStore {
|
||||
Section("settings.shelf.title") {
|
||||
ShelfSettingsRows(store: shelfStore)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.inset)
|
||||
}
|
||||
@@ -324,6 +336,39 @@ private struct WidgetSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Einstellungen der Ablage.
|
||||
///
|
||||
/// Automatisches Aufräumen ist ausdrücklich abschaltbar und standardmäßig aus:
|
||||
/// eine Ablage, die ungefragt löscht, ist kein Zwischenlager, sondern ein
|
||||
/// Papierkorb mit Zeitschaltuhr.
|
||||
private struct ShelfSettingsRows: View {
|
||||
@Bindable var store: ShelfStore
|
||||
|
||||
private static let choices: [Int?] = [nil, 1, 7, 30, 90]
|
||||
|
||||
var body: some View {
|
||||
Picker(selection: $store.autoCleanupDays) {
|
||||
ForEach(Self.choices, id: \.self) { days in
|
||||
if let days {
|
||||
Text("settings.shelf.days \(days)").tag(Optional(days))
|
||||
} else {
|
||||
Text("settings.shelf.never").tag(Int?.none)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("settings.shelf.cleanup")
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("settings.shelf.count \(store.items.count)")
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Button("settings.shelf.reveal") { store.revealInFinder() }
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct PlacementRow: View {
|
||||
@Bindable var model: AppModel
|
||||
let placement: WidgetPlacement
|
||||
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
import CalendarProvider
|
||||
import WeatherProvider
|
||||
import MetricsProvider
|
||||
import ShelfProvider
|
||||
|
||||
/// Führt das Einstellungsfenster selbst.
|
||||
///
|
||||
@@ -26,7 +27,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
var keepsDockIcon = false
|
||||
|
||||
func show(model: AppModel, calendarModel: CalendarModel, weatherModel: WeatherModel,
|
||||
metricsModel: MetricsModel?, fanControl: FanControl,
|
||||
metricsModel: MetricsModel?, shelfStore: ShelfStore?, fanControl: FanControl,
|
||||
tab: SettingsTab = .widgets) {
|
||||
selectedTab = tab
|
||||
keepsDockIcon = model.showsDockIcon
|
||||
@@ -45,6 +46,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
calendarModel: calendarModel,
|
||||
weatherModel: weatherModel,
|
||||
metricsModel: metricsModel,
|
||||
shelfStore: shelfStore,
|
||||
fanControl: fanControl,
|
||||
selectedTab: Binding(
|
||||
get: { [weak self] in self?.selectedTab ?? .widgets },
|
||||
|
||||
Reference in New Issue
Block a user