Berechtigungsdialog und Einstellungsfenster reparieren
Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der gar nicht lief. Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt. Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow. Kalenderberechtigung, zwei Ursachen übereinander: Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht. ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll. Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist, an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird. Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute" — eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`. Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
0B27E91E0F9BF29005B0ED64 /* SettingsWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E43384BA603C866D148D9D8 /* SettingsWindowController.swift */; };
|
||||||
378A5A417112CCB31EF3F774 /* OnyxWidgetKit in Frameworks */ = {isa = PBXBuildFile; productRef = E66577A40689DF225BCED184 /* OnyxWidgetKit */; };
|
378A5A417112CCB31EF3F774 /* OnyxWidgetKit in Frameworks */ = {isa = PBXBuildFile; productRef = E66577A40689DF225BCED184 /* OnyxWidgetKit */; };
|
||||||
39E29ADC71D4860543AD25EB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BD2DF1B775C733C8F75635 /* SettingsView.swift */; };
|
39E29ADC71D4860543AD25EB /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45BD2DF1B775C733C8F75635 /* SettingsView.swift */; };
|
||||||
4E729C7A6498B4679C8D2C42 /* OnyxCore in Frameworks */ = {isa = PBXBuildFile; productRef = C64A111F449D0F9D36ED46FB /* OnyxCore */; };
|
4E729C7A6498B4679C8D2C42 /* OnyxCore in Frameworks */ = {isa = PBXBuildFile; productRef = C64A111F449D0F9D36ED46FB /* OnyxCore */; };
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
50A018673EE6BD9C50F1EFA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
50A018673EE6BD9C50F1EFA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
821B742C211265914B6CC44B /* OnyxKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = OnyxKit; path = Packages/OnyxKit; sourceTree = SOURCE_ROOT; };
|
821B742C211265914B6CC44B /* OnyxKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = OnyxKit; path = Packages/OnyxKit; sourceTree = SOURCE_ROOT; };
|
||||||
87597F09E971C14CF1E475DB /* Onyx.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Onyx.entitlements; sourceTree = "<group>"; };
|
87597F09E971C14CF1E475DB /* Onyx.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Onyx.entitlements; sourceTree = "<group>"; };
|
||||||
|
8E43384BA603C866D148D9D8 /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
||||||
B83C4E3CB821F903F3977E88 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = "<group>"; };
|
B83C4E3CB821F903F3977E88 /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = "<group>"; };
|
||||||
CB5D52C0D22AE1F5E0601AF9 /* PlaceholderWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderWidgets.swift; sourceTree = "<group>"; };
|
CB5D52C0D22AE1F5E0601AF9 /* PlaceholderWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderWidgets.swift; sourceTree = "<group>"; };
|
||||||
F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnyxApp.swift; sourceTree = "<group>"; };
|
F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnyxApp.swift; sourceTree = "<group>"; };
|
||||||
@@ -92,6 +94,7 @@
|
|||||||
F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */,
|
F89CAC2A7DA89B8707D45D65 /* OnyxApp.swift */,
|
||||||
CB5D52C0D22AE1F5E0601AF9 /* PlaceholderWidgets.swift */,
|
CB5D52C0D22AE1F5E0601AF9 /* PlaceholderWidgets.swift */,
|
||||||
45BD2DF1B775C733C8F75635 /* SettingsView.swift */,
|
45BD2DF1B775C733C8F75635 /* SettingsView.swift */,
|
||||||
|
8E43384BA603C866D148D9D8 /* SettingsWindowController.swift */,
|
||||||
);
|
);
|
||||||
path = Onyx;
|
path = Onyx;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -182,6 +185,7 @@
|
|||||||
9C2E419014B7E04630FDF151 /* OnyxApp.swift in Sources */,
|
9C2E419014B7E04630FDF151 /* OnyxApp.swift in Sources */,
|
||||||
5EA873EC28C7EE21AC85A263 /* PlaceholderWidgets.swift in Sources */,
|
5EA873EC28C7EE21AC85A263 /* PlaceholderWidgets.swift in Sources */,
|
||||||
39E29ADC71D4860543AD25EB /* SettingsView.swift in Sources */,
|
39E29ADC71D4860543AD25EB /* SettingsView.swift in Sources */,
|
||||||
|
0B27E91E0F9BF29005B0ED64 /* SettingsWindowController.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -244,6 +248,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
ENABLE_DEBUG_DYLIB = NO;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -306,6 +311,7 @@
|
|||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
DEVELOPMENT_TEAM = PP34X97WS3;
|
DEVELOPMENT_TEAM = PP34X97WS3;
|
||||||
|
ENABLE_DEBUG_DYLIB = NO;
|
||||||
ENABLE_HARDENED_RUNTIME = YES;
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
|
|||||||
@@ -29,6 +29,11 @@
|
|||||||
|
|
||||||
<!-- Berechtigungstexte. Sie erscheinen wörtlich im Systemdialog, deshalb
|
<!-- Berechtigungstexte. Sie erscheinen wörtlich im Systemdialog, deshalb
|
||||||
steht hier, wofür Onyx den Zugriff braucht — nicht, dass es ihn braucht. -->
|
steht hier, wofür Onyx den Zugriff braucht — nicht, dass es ihn braucht. -->
|
||||||
|
<!-- Der Alt-Schlüssel bleibt gesetzt: Teile von EventKit prüfen ihn
|
||||||
|
weiterhin, und ein fehlender Verwendungszweck äußert sich nicht als
|
||||||
|
Fehler, sondern als ausbleibender Dialog. -->
|
||||||
|
<key>NSCalendarsUsageDescription</key>
|
||||||
|
<string>Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben.</string>
|
||||||
<key>NSCalendarsFullAccessUsageDescription</key>
|
<key>NSCalendarsFullAccessUsageDescription</key>
|
||||||
<string>Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben.</string>
|
<string>Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
|||||||
@@ -2,132 +2,450 @@
|
|||||||
"sourceLanguage": "en",
|
"sourceLanguage": "en",
|
||||||
"strings": {
|
"strings": {
|
||||||
"menu.openPanel": {
|
"menu.openPanel": {
|
||||||
|
"extractionState": "stale",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Panel öffnen" } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Open Panel" } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Panel öffnen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu.settings" : {
|
"en": {
|
||||||
"localizations" : {
|
"stringUnit": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Einstellungen …" } },
|
"state": "translated",
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Settings…" } }
|
"value": "Open Panel"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"menu.quit": {
|
"menu.quit": {
|
||||||
|
"extractionState": "stale",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Onyx beenden" } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Quit Onyx" } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Onyx beenden"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"en": {
|
||||||
"settings.tab.widgets" : {
|
"stringUnit": {
|
||||||
"localizations" : {
|
"state": "translated",
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Widgets" } },
|
"value": "Quit Onyx"
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Widgets" } }
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.tab.display" : {
|
"menu.settings": {
|
||||||
|
"extractionState": "stale",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Anzeige" } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Display" } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Einstellungen …"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.widgets.hint" : {
|
"en": {
|
||||||
"localizations" : {
|
"stringUnit": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Die Reihenfolge bestimmt die Anordnung im Panel. Jedes Widget rückt an die erste Stelle, an die es passt." } },
|
"state": "translated",
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "The order determines the arrangement in the panel. Each widget takes the first spot it fits into." } }
|
"value": "Settings…"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"settings.widgets.inPanel" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Im Panel" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "In the panel" } }
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"settings.widgets.available" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Verfügbar" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Available" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.widgets.empty" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Widgets im Panel. Das Panel bleibt leer, bis du eins hinzufügst." } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "No widgets in the panel. It stays empty until you add one." } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.widgets.add" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzufügen" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Add" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.widgets.remove" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Aus dem Panel entfernen" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Remove from panel" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"settings.size.small" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Klein (1×1)" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Small (1×1)" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.size.medium" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Mittel (2×1)" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Medium (2×1)" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.size.large" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Groß (2×2)" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Large (2×2)" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.size.wide" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Breit (4×1)" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Wide (4×1)" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"settings.display.policy" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Panel anzeigen auf" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show panel on" } }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.display.builtInOnly" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Nur dem internen Display" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Built-in display only" } }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.display.all": {
|
"settings.display.all": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Allen Displays" } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "All displays" } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Allen Displays"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "All displays"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.display.builtInOnly": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Nur dem internen Display"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Built-in display only"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.display.hint": {
|
"settings.display.hint": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Auf Displays ohne echte Notch erscheint oben mittig ein Balken, der sich genauso verhält. Ist der Deckel geschlossen und kein internes Display vorhanden, erscheint das Panel trotzdem auf einem externen — sonst wäre Onyx nicht erreichbar." } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "On displays without a real notch, a bar appears at the top centre and behaves the same. With the lid closed and no built-in display present, the panel still appears on an external one — otherwise Onyx would be unreachable." } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Auf Displays ohne echte Notch erscheint oben mittig ein Balken, der sich genauso verhält. Ist der Deckel geschlossen und kein internes Display vorhanden, erscheint das Panel trotzdem auf einem externen — sonst wäre Onyx nicht erreichbar."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "On displays without a real notch, a bar appears at the top centre and behaves the same. With the lid closed and no built-in display present, the panel still appears on an external one — otherwise Onyx would be unreachable."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.display.menuBarIcon": {
|
"settings.display.menuBarIcon": {
|
||||||
|
"extractionState": "stale",
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Symbol in der Menüleiste anzeigen" } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Show icon in the menu bar" } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Symbol in der Menüleiste anzeigen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Show icon in the menu bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings.display.menuBarIcon.hint": {
|
"settings.display.menuBarIcon.hint": {
|
||||||
"localizations": {
|
"localizations": {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Über das Symbol erreichst du Panel, Einstellungen und Beenden. Ohne es kommst du nur über die Notch an Onyx heran." } },
|
"de": {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "The icon gives you the panel, settings and quit. Without it, the notch is the only way to reach Onyx." } }
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Über das Symbol erreichst du Panel, Einstellungen und Beenden. Ohne es kommst du nur über die Notch an Onyx heran."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "The icon gives you the panel, settings and quit. Without it, the notch is the only way to reach Onyx."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.display.policy": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Panel anzeigen auf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Show panel on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.size.large": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Groß (2×2)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Large (2×2)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.size.medium": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Mittel (2×1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Medium (2×1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.size.small": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Klein (1×1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Small (1×1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.size.wide": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Breit (4×1)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Wide (4×1)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.tab.display": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Anzeige"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Display"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.tab.widgets": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Widgets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Widgets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.widgets.add": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Hinzufügen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Add"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.widgets.available": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Verfügbar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.widgets.empty": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Keine Widgets im Panel. Das Panel bleibt leer, bis du eins hinzufügst."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "No widgets in the panel. It stays empty until you add one."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.widgets.hint": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Die Reihenfolge bestimmt die Anordnung im Panel. Jedes Widget rückt an die erste Stelle, an die es passt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "The order determines the arrangement in the panel. Each widget takes the first spot it fits into."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.widgets.inPanel": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Im Panel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "In the panel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.widgets.remove": {
|
||||||
|
"extractionState": "stale",
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Aus dem Panel entfernen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Remove from panel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.tab.permissions": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Berechtigungen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Permissions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.calendar": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Kalender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.granted": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Erteilt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Granted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.request": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Anfragen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.calendar.why": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Onyx shows your events in the calendar widget. It only ever reads, never writes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.openSystem": {
|
||||||
|
"localizations": {
|
||||||
|
"de": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Systemeinstellungen öffnen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"stringUnit": {
|
||||||
|
"state": "translated",
|
||||||
|
"value": "Open System Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,15 +15,10 @@ struct OnyxApp: App {
|
|||||||
// Onyx hat kein Hauptfenster — alles lebt in der Notch und in der
|
// Onyx hat kein Hauptfenster — alles lebt in der Notch und in der
|
||||||
// Menüleiste. Die Einstellungen sind die einzige Ausnahme; im Panel
|
// Menüleiste. Die Einstellungen sind die einzige Ausnahme; im Panel
|
||||||
// wäre dafür zu wenig Platz.
|
// wäre dafür zu wenig Platz.
|
||||||
Settings {
|
// Eine Szene ist Pflicht, benutzt wird sie nicht: das
|
||||||
if let model = delegate.model {
|
// Einstellungsfenster führt SettingsWindowController selbst.
|
||||||
SettingsView(model: model)
|
// Siehe die Begründung dort.
|
||||||
} else {
|
Settings { EmptyView() }
|
||||||
// Nur sichtbar, wenn die Einzelinstanz-Prüfung uns abgewiesen
|
|
||||||
// hat und die App gerade wieder verschwindet.
|
|
||||||
ProgressView().frame(width: 520, height: 420)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +30,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
private var menuBar: MenuBarController?
|
private var menuBar: MenuBarController?
|
||||||
private var onyxItem: NSStatusItem?
|
private var onyxItem: NSStatusItem?
|
||||||
private var calendarModel: CalendarModel?
|
private var calendarModel: CalendarModel?
|
||||||
|
private let settingsWindow = SettingsWindowController()
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||||
NSApp.setActivationPolicy(.accessory)
|
NSApp.setActivationPolicy(.accessory)
|
||||||
@@ -78,6 +74,38 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
installOnyxStatusItem()
|
installOnyxStatusItem()
|
||||||
observeMenuBarPreference()
|
observeMenuBarPreference()
|
||||||
|
requestPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Berechtigungen beim Start anfragen, nicht erst wenn ein Widget sichtbar
|
||||||
|
/// wird.
|
||||||
|
///
|
||||||
|
/// Das Panel ist beim Start zu. Eine an die View gebundene Anfrage kommt
|
||||||
|
/// deshalb erst, wenn jemand die Notch berührt — und bis dahin taucht Onyx
|
||||||
|
/// in den Systemeinstellungen gar nicht auf, es gibt also nichts, das man
|
||||||
|
/// dort freischalten könnte. Genau dieser Zustand ist beim Testen
|
||||||
|
/// aufgetreten.
|
||||||
|
private func requestPermissions() {
|
||||||
|
guard let calendarModel else { return }
|
||||||
|
// Immer einmal laden: mit Berechtigung kommen Termine, ohne sie zeigt
|
||||||
|
// das Widget den Berechtigungshinweis statt einer leeren Liste.
|
||||||
|
calendarModel.refresh()
|
||||||
|
guard !calendarModel.hasAccess else { return }
|
||||||
|
|
||||||
|
// Nicht im Hintergrund anfragen.
|
||||||
|
//
|
||||||
|
// Onyx ist eine App ohne Fenster. TCC bereitet den Dialog zwar vor
|
||||||
|
// (promptType 1), entscheidet sich dann aber dagegen (promptPolicy 0),
|
||||||
|
// weil keine App im Vordergrund ist, an die er gehören könnte. Die
|
||||||
|
// Anfrage kommt kommentarlos mit `false` zurück, der Status bleibt
|
||||||
|
// `notDetermined` — und in den Systemeinstellungen taucht Onyx nie auf.
|
||||||
|
// `NSApp.activate` davor hilft nicht: Aktivierung wirkt asynchron und
|
||||||
|
// ist beim Aufruf noch nicht durch.
|
||||||
|
//
|
||||||
|
// Deshalb der ehrliche Weg: beim ersten Start das Einstellungsfenster
|
||||||
|
// öffnen. Dort ist ein echtes Fenster im Vordergrund, der Dialog
|
||||||
|
// erscheint zuverlässig — und der Nutzer weiß, warum gefragt wird.
|
||||||
|
showSettings(tab: .permissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationWillTerminate(_ notification: Notification) {
|
func applicationWillTerminate(_ notification: Notification) {
|
||||||
@@ -140,9 +168,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
@objc private func togglePanel() { coordinator?.togglePanelUnderPointer() }
|
@objc private func togglePanel() { coordinator?.togglePanelUnderPointer() }
|
||||||
|
|
||||||
@objc private func openSettings() {
|
@objc private func openSettings() { showSettings(tab: .widgets) }
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
|
||||||
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
|
private func showSettings(tab: SettingsTab) {
|
||||||
|
guard let model, let calendarModel else { return }
|
||||||
|
settingsWindow.show(model: model, calendarModel: calendarModel, tab: tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func quit() { NSApp.terminate(nil) }
|
@objc private func quit() { NSApp.terminate(nil) }
|
||||||
|
|||||||
@@ -2,21 +2,90 @@ import SwiftUI
|
|||||||
import OnyxDesign
|
import OnyxDesign
|
||||||
import OnyxNotch
|
import OnyxNotch
|
||||||
import OnyxWidgetKit
|
import OnyxWidgetKit
|
||||||
|
import CalendarProvider
|
||||||
|
|
||||||
|
enum SettingsTab: Hashable {
|
||||||
|
case widgets, display, permissions
|
||||||
|
}
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
@Bindable var model: AppModel
|
@Bindable var model: AppModel
|
||||||
|
let calendarModel: CalendarModel
|
||||||
|
@Binding var selectedTab: SettingsTab
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView(selection: $selectedTab) {
|
||||||
WidgetSettings(model: model)
|
WidgetSettings(model: model)
|
||||||
.tabItem { Label("settings.tab.widgets", systemImage: "square.grid.2x2") }
|
.tabItem { Label("settings.tab.widgets", systemImage: "square.grid.2x2") }
|
||||||
|
.tag(SettingsTab.widgets)
|
||||||
DisplaySettings(model: model)
|
DisplaySettings(model: model)
|
||||||
.tabItem { Label("settings.tab.display", systemImage: "macbook") }
|
.tabItem { Label("settings.tab.display", systemImage: "macbook") }
|
||||||
|
.tag(SettingsTab.display)
|
||||||
|
PermissionSettings(calendarModel: calendarModel)
|
||||||
|
.tabItem { Label("settings.tab.permissions", systemImage: "hand.raised") }
|
||||||
|
.tag(SettingsTab.permissions)
|
||||||
}
|
}
|
||||||
.frame(width: 520, height: 420)
|
.frame(width: 520, height: 420)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Berechtigungen
|
||||||
|
|
||||||
|
/// Zeigt den Zustand, statt ihn nur spürbar zu machen.
|
||||||
|
///
|
||||||
|
/// Eine verweigerte Berechtigung äußert sich sonst als leeres Widget, und der
|
||||||
|
/// Weg zurück ist nicht offensichtlich: hat macOS die Anfrage einmal
|
||||||
|
/// abgelehnt bekommen, fragt es nie wieder — es bleibt nur der Gang in die
|
||||||
|
/// Systemeinstellungen.
|
||||||
|
private struct PermissionSettings: View {
|
||||||
|
let calendarModel: CalendarModel
|
||||||
|
@State private var granted = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Label("settings.permissions.calendar", systemImage: "calendar")
|
||||||
|
Spacer()
|
||||||
|
if granted {
|
||||||
|
Label("settings.permissions.granted", systemImage: "checkmark.circle.fill")
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
.labelStyle(.titleAndIcon)
|
||||||
|
} else {
|
||||||
|
Button("settings.permissions.request") {
|
||||||
|
Task {
|
||||||
|
let ok = await calendarModel.requestAccessNow()
|
||||||
|
granted = ok
|
||||||
|
// macOS fragt nach einer Ablehnung nicht erneut.
|
||||||
|
// Dann hilft nur noch der Weg über die
|
||||||
|
// Systemeinstellungen — also dorthin führen.
|
||||||
|
if !ok { openPrivacySettings() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("settings.permissions.calendar.why")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button("settings.permissions.openSystem") { openPrivacySettings() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.padding()
|
||||||
|
.onAppear { granted = calendarModel.hasAccess }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openPrivacySettings() {
|
||||||
|
guard let url = URL(string:
|
||||||
|
"x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") else { return }
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Widgets
|
// MARK: - Widgets
|
||||||
|
|
||||||
private struct WidgetSettings: View {
|
private struct WidgetSettings: View {
|
||||||
|
|||||||
56
Onyx/SettingsWindowController.swift
Normal file
56
Onyx/SettingsWindowController.swift
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
|
import CalendarProvider
|
||||||
|
|
||||||
|
/// Führt das Einstellungsfenster selbst.
|
||||||
|
///
|
||||||
|
/// SwiftUIs `Settings`-Szene wird über `NSApp.sendAction(showSettingsWindow:)`
|
||||||
|
/// geöffnet — ein privater Selektor, dessen Name sich zwischen macOS-Versionen
|
||||||
|
/// schon geändert hat (`showPreferencesWindow:` davor) und der bei
|
||||||
|
/// `.accessory`-Apps ohne Menüleiste unzuverlässig ankommt. Genau das ist hier
|
||||||
|
/// passiert: der Menüeintrag tat nichts.
|
||||||
|
///
|
||||||
|
/// Ein eigenes `NSWindow` hat diese Abhängigkeit nicht. Es kostet ein paar
|
||||||
|
/// Zeilen mehr und funktioniert dafür.
|
||||||
|
@MainActor
|
||||||
|
final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||||
|
|
||||||
|
private var window: NSWindow?
|
||||||
|
/// Muss außerhalb der View liegen, damit ein erneutes Öffnen den Reiter setzen kann.
|
||||||
|
private var selectedTab: SettingsTab = .widgets
|
||||||
|
|
||||||
|
func show(model: AppModel, calendarModel: CalendarModel, tab: SettingsTab = .widgets) {
|
||||||
|
selectedTab = tab
|
||||||
|
|
||||||
|
if window == nil {
|
||||||
|
let hosting = NSHostingController(
|
||||||
|
rootView: SettingsView(model: model,
|
||||||
|
calendarModel: calendarModel,
|
||||||
|
selectedTab: Binding(
|
||||||
|
get: { [weak self] in self?.selectedTab ?? .widgets },
|
||||||
|
set: { [weak self] in self?.selectedTab = $0 })))
|
||||||
|
|
||||||
|
let window = NSWindow(contentViewController: hosting)
|
||||||
|
window.title = "Onyx"
|
||||||
|
window.styleMask = [.titled, .closable, .miniaturizable]
|
||||||
|
window.isReleasedWhenClosed = false
|
||||||
|
window.delegate = self
|
||||||
|
window.center()
|
||||||
|
// Über die Position freuen sich Nutzer mit mehreren Displays:
|
||||||
|
// beim zweiten Öffnen steht es wieder da, wo sie es hingeschoben haben.
|
||||||
|
window.setFrameAutosaveName("onyx.settings")
|
||||||
|
self.window = window
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ohne `activate` bleibt das Fenster einer .accessory-App hinter der
|
||||||
|
// aktiven App liegen und wirkt, als wäre nichts passiert.
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
window?.makeKeyAndOrderFront(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowWillClose(_ notification: Notification) {
|
||||||
|
// Zurück in den Hintergrund: Onyx hat sonst weiter den Fokus, obwohl
|
||||||
|
// kein Fenster mehr offen ist.
|
||||||
|
NSApp.hide(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,6 +83,10 @@ public enum CalendarSourceState: Equatable, Sendable {
|
|||||||
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
|
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
|
||||||
public protocol CalendarSource: Sendable {
|
public protocol CalendarSource: Sendable {
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
|
/// Ob bereits gelesen werden darf — **ohne** dabei nachzufragen.
|
||||||
|
/// Die Einstellungen brauchen das, um den Zustand anzuzeigen, ohne
|
||||||
|
/// nebenbei einen Systemdialog auszulösen.
|
||||||
|
var isAuthorized: Bool { get }
|
||||||
func requestAccess() async -> Bool
|
func requestAccess() async -> Bool
|
||||||
func calendars() async -> [OnyxCalendar]
|
func calendars() async -> [OnyxCalendar]
|
||||||
func load(_ interval: DateInterval) async -> CalendarSourceState
|
func load(_ interval: DateInterval) async -> CalendarSourceState
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import OSLog
|
||||||
|
import EventKit
|
||||||
import OnyxDesign
|
import OnyxDesign
|
||||||
import OnyxWidgetKit
|
import OnyxWidgetKit
|
||||||
|
|
||||||
|
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Calendar")
|
||||||
|
|
||||||
/// Hält die Termine für das Widget und lädt sie nach.
|
/// Hält die Termine für das Widget und lädt sie nach.
|
||||||
@MainActor
|
@MainActor
|
||||||
@Observable
|
@Observable
|
||||||
@@ -19,6 +23,24 @@ public final class CalendarModel {
|
|||||||
self.calendar = calendar
|
self.calendar = calendar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ob gelesen werden darf. Fragt nicht nach — nur zum Anzeigen.
|
||||||
|
public var hasAccess: Bool { source.isAuthorized }
|
||||||
|
|
||||||
|
/// Fragt die Berechtigung an und lädt bei Erfolg gleich.
|
||||||
|
///
|
||||||
|
/// Wird beim Start gerufen, nicht erst wenn das Widget sichtbar wird. Das
|
||||||
|
/// Panel ist beim Start zu, also erscheint eine View-gebundene Anfrage erst,
|
||||||
|
/// wenn jemand die Notch berührt — und bis dahin steht in den
|
||||||
|
/// Systemeinstellungen kein Onyx, das man freischalten könnte.
|
||||||
|
@discardableResult
|
||||||
|
public func requestAccessNow() async -> Bool {
|
||||||
|
log.notice("Kalenderzugriff: Status vor der Anfrage = \(EKEventStore.authorizationStatus(for: .event).rawValue, privacy: .public)")
|
||||||
|
let granted = await source.requestAccess()
|
||||||
|
log.notice("Kalenderzugriff: erteilt = \(granted, privacy: .public), Status danach = \(EKEventStore.authorizationStatus(for: .event).rawValue, privacy: .public)")
|
||||||
|
if granted { refresh() } else { state = .denied }
|
||||||
|
return granted
|
||||||
|
}
|
||||||
|
|
||||||
public var window: EventWindow? {
|
public var window: EventWindow? {
|
||||||
if case .ready(let window) = state { return window }
|
if case .ready(let window) = state { return window }
|
||||||
return nil
|
return nil
|
||||||
@@ -33,7 +55,12 @@ public final class CalendarModel {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
|
|
||||||
guard await source.requestAccess() else {
|
// Hier wird NICHT nachgefragt. Ein Berechtigungsdialog aus einer
|
||||||
|
// App ohne Fenster heraus wird von TCC verworfen, ohne dass etwas
|
||||||
|
// passiert — die Anfrage muss aus dem Einstellungsfenster kommen.
|
||||||
|
// Ohne Berechtigung ist der ehrliche Zustand `denied` und nicht
|
||||||
|
// eine leere Terminliste, die "nichts mehr heute" behaupten würde.
|
||||||
|
guard source.isAuthorized else {
|
||||||
state = .denied
|
state = .denied
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import OSLog
|
||||||
import EventKit
|
import EventKit
|
||||||
import AppKit
|
import AppKit
|
||||||
|
|
||||||
|
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "EventKit")
|
||||||
|
|
||||||
/// Termine aus dem Apple-Kalender. Ausschließlich lesend.
|
/// Termine aus dem Apple-Kalender. Ausschließlich lesend.
|
||||||
public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
||||||
|
|
||||||
@@ -13,6 +16,10 @@ public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
|||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
|
public var isAuthorized: Bool {
|
||||||
|
EKEventStore.authorizationStatus(for: .event) == .fullAccess
|
||||||
|
}
|
||||||
|
|
||||||
public func requestAccess() async -> Bool {
|
public func requestAccess() async -> Bool {
|
||||||
switch EKEventStore.authorizationStatus(for: .event) {
|
switch EKEventStore.authorizationStatus(for: .event) {
|
||||||
case .fullAccess:
|
case .fullAccess:
|
||||||
@@ -22,7 +29,15 @@ public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
|||||||
case .notDetermined, .writeOnly:
|
case .notDetermined, .writeOnly:
|
||||||
// `writeOnly` ist für uns wertlos — Onyx zeigt nur an. Der Nutzer
|
// `writeOnly` ist für uns wertlos — Onyx zeigt nur an. Der Nutzer
|
||||||
// muss den Vollzugriff geben, sonst bleibt das Widget leer.
|
// muss den Vollzugriff geben, sonst bleibt das Widget leer.
|
||||||
return (try? await store.requestFullAccessToEvents()) ?? false
|
do {
|
||||||
|
return try await store.requestFullAccessToEvents()
|
||||||
|
} catch {
|
||||||
|
// Nicht verschlucken: wenn EventKit hier wirft, erscheint gar
|
||||||
|
// kein Dialog und der Zustand bleibt `notDetermined` — von außen
|
||||||
|
// sieht das aus, als hätte die App nie gefragt.
|
||||||
|
log.error("requestFullAccessToEvents warf: \(error.localizedDescription, privacy: .public) — \(String(describing: error), privacy: .public)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
@unknown default:
|
@unknown default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,159 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
"widget.calendar.name" : {
|
"" : {
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Kalender" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Calendar" } }
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"widget.calendar.noUpcoming" : {
|
"%lld" : {
|
||||||
"comment" : "Nur zeigen, wenn der Zeitraum wirklich abgedeckt ist",
|
"comment" : "A day of the month. The argument is the day of the month.",
|
||||||
"localizations" : {
|
"isCommentAutoGenerated" : true
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Nichts mehr für heute" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Nothing left today" } }
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"widget.calendar.allDay" : {
|
"widget.calendar.allDay" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Ganztägig" } },
|
"de" : {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "All day" } }
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Ganztägig"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "All day"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"widget.calendar.day.unknown" : {
|
"widget.calendar.day.unknown" : {
|
||||||
"comment" : "Erklärt einen ausgegrauten Tag: keine Information, nicht keine Termine",
|
"comment" : "Erklärt einen ausgegrauten Tag: keine Information, nicht keine Termine",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Für diesen Tag liegen keine Daten vor" } },
|
"de" : {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "No data available for this day" } }
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Für diesen Tag liegen keine Daten vor"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "No data available for this day"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"widget.calendar.denied" : {
|
"widget.calendar.denied" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Kein Kalenderzugriff.\nIn den Systemeinstellungen erlauben." } },
|
"de" : {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "No calendar access.\nAllow it in System Settings." } }
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Kein Kalenderzugriff.\nIn den Systemeinstellungen erlauben."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"widget.calendar.neverWritten" : {
|
"en" : {
|
||||||
"localizations" : {
|
"stringUnit" : {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Calendarr einmal öffnen,\ndamit Termine bereitstehen." } },
|
"state" : "translated",
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Open Calendarr once\nso events become available." } }
|
"value" : "No calendar access.\nAllow it in System Settings."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"widget.calendar.loggedOut" : {
|
|
||||||
"localizations" : {
|
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "In Calendarr anmelden" } },
|
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Sign in to Calendarr" } }
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"widget.calendar.incompatible" : {
|
"widget.calendar.incompatible" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Onyx ist zu alt für diese\nCalendarr-Version. Bitte aktualisieren." } },
|
"de" : {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Onyx is too old for this\nCalendarr version. Please update." } }
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Onyx ist zu alt für diese\nCalendarr-Version. Bitte aktualisieren."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Onyx is too old for this\nCalendarr version. Please update."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widget.calendar.loggedOut" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "In Calendarr anmelden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Sign in to Calendarr"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widget.calendar.name" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Kalender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widget.calendar.neverWritten" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Calendarr einmal öffnen,\ndamit Termine bereitstehen."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Open Calendarr once\nso events become available."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"widget.calendar.noUpcoming" : {
|
||||||
|
"comment" : "Nur zeigen, wenn der Zeitraum wirklich abgedeckt ist",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nichts mehr für heute"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Nothing left today"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"widget.calendar.unreadable" : {
|
"widget.calendar.unreadable" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Termine nicht lesbar.\nKonfigurationsfehler." } },
|
"de" : {
|
||||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Events unreadable.\nConfiguration error." } }
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Termine nicht lesbar.\nKonfigurationsfehler."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Events unreadable.\nConfiguration error."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.1"
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,12 @@ settings:
|
|||||||
# der mit Team-Zertifikat signierte Host sonst nicht laden darf.
|
# der mit Team-Zertifikat signierte Host sonst nicht laden darf.
|
||||||
OTHER_CODE_SIGN_FLAGS: "--deep"
|
OTHER_CODE_SIGN_FLAGS: "--deep"
|
||||||
SWIFT_STRICT_CONCURRENCY: complete
|
SWIFT_STRICT_CONCURRENCY: complete
|
||||||
|
# Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus
|
||||||
|
# (für SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur noch ein Rumpf —
|
||||||
|
# und TCC ordnet Berechtigungsanfragen dem Rumpf zu, der weder Info.plist
|
||||||
|
# noch Entitlements trägt. Ergebnis: requestFullAccessToEvents() liefert
|
||||||
|
# kommentarlos false, ohne dass je ein Dialog erscheint.
|
||||||
|
ENABLE_DEBUG_DYLIB: NO
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
OnyxKit:
|
OnyxKit:
|
||||||
|
|||||||
Reference in New Issue
Block a user