Berechtigungen zeigen ihren Zustand live, Dock-Symbol wird wählbar
Die Einstellungen zeigten weiter "Anfragen", obwohl die Ortung längst lief: authorizationStatus ist eine Abfrage, keine Beobachtung, und ich hatte sie einmalig beim Öffnen gelesen. Jetzt meldet locationManagerDidChangeAuthorization die Änderung, WeatherModel führt daraus einen beobachtbaren Wert, und die Einstellungen aktualisieren sich, während das Fenster offen steht. Nach der Zustimmung wird sofort geladen statt beim nächsten Anlauf. Der Knopf "Systemeinstellungen öffnen" ganz unten zeigte fest auf den Kalenderbereich — wer ihn beim Standort drückte, landete am falschen Ort. Es gibt ihn nicht mehr; stattdessen führt jede Berechtigung in ihren eigenen Bereich, und der Knopf erscheint nur im Zustand "abgelehnt". Bei "noch nie gefragt" steht dort Anfragen, bei "erteilt" gar nichts: drei Zustände, drei Antworten. Genau daran ist die erste Fassung gescheitert. Dock-Symbol ist jetzt eine Einstellung, standardmäßig aus. Onyx läuft als LSUIElement — das war Absicht und keine Auslassung: es hat kein Hauptfenster und wäre im Dock ein Symbol, das beim Anklicken nichts öffnet, und es stünde im Programmumschalter zwischen den Programmen, mit denen man arbeitet. Wer es trotzdem will, schaltet es ein; die Wahl wird beim Start wieder angewendet.
This commit is contained in:
@@ -42,96 +42,108 @@ struct SettingsView: View {
|
||||
private struct PermissionSettings: View {
|
||||
let calendarModel: CalendarModel
|
||||
let weatherModel: WeatherModel
|
||||
@State private var granted = false
|
||||
@State private var locationGranted = false
|
||||
@State private var calendarGranted = false
|
||||
|
||||
private var locationState: PermissionRow.State {
|
||||
switch weatherModel.locationAuthorization {
|
||||
case .granted: .granted
|
||||
case .denied: .denied
|
||||
case .undetermined: .undetermined
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
PermissionRow(
|
||||
title: "settings.permissions.calendar",
|
||||
symbol: "calendar",
|
||||
state: calendarGranted ? .granted
|
||||
: (calendarModel.authorization == .denied ? .denied : .undetermined),
|
||||
explanation: "settings.permissions.calendar.why",
|
||||
pane: .calendar,
|
||||
request: { calendarGranted = await calendarModel.requestAccessNow() })
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Label("settings.permissions.location", systemImage: "location")
|
||||
Spacer()
|
||||
if locationGranted {
|
||||
Label("settings.permissions.granted", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.labelStyle(.titleAndIcon)
|
||||
} else if weatherModel.locationIsDenied {
|
||||
Button("settings.permissions.openSystem") { openLocationSettings() }
|
||||
} else {
|
||||
Button("settings.permissions.request") {
|
||||
// Muss aus diesem Fenster kommen: macOS zeigt den
|
||||
// Dialog nur, wenn eine App im Vordergrund ist.
|
||||
weatherModel.requestLocationAuthorization()
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("settings.permissions.location.why")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("settings.permissions.openSystem") { openPrivacySettings() }
|
||||
PermissionRow(
|
||||
title: "settings.permissions.location",
|
||||
symbol: "location",
|
||||
// Direkt aus dem beobachtbaren Wert: der Zustand ändert
|
||||
// sich, während dieses Fenster offen steht.
|
||||
state: locationState,
|
||||
explanation: "settings.permissions.location.why",
|
||||
pane: .location,
|
||||
request: { weatherModel.requestLocationAuthorization() })
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.padding()
|
||||
.task {
|
||||
granted = calendarModel.hasAccess
|
||||
guard !granted, !didAutoRequest,
|
||||
calendarModel.authorization == .undetermined else { return }
|
||||
didAutoRequest = true
|
||||
|
||||
// Kurz warten, bis das Fenster wirklich vorne steht. TCC zeigt den
|
||||
// Dialog nur, wenn eine App im Vordergrund ist, an die er gehören
|
||||
// kann — und Aktivierung wirkt asynchron. Ohne diese Pause fragt
|
||||
// Onyx, während es formal noch im Hintergrund liegt, und die
|
||||
// Anfrage verpufft kommentarlos.
|
||||
calendarGranted = calendarModel.hasAccess
|
||||
guard !calendarGranted, calendarModel.authorization == .undetermined else { return }
|
||||
// Kurz warten, bis das Fenster wirklich vorne steht: TCC zeigt den
|
||||
// Dialog nur, wenn eine App im Vordergrund ist, und Aktivierung
|
||||
// wirkt asynchron.
|
||||
try? await Task.sleep(for: .milliseconds(400))
|
||||
granted = await calendarModel.requestAccessNow()
|
||||
calendarGranted = await calendarModel.requestAccessNow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine Berechtigung mit ihrem Zustand und dem Weg, der von dort weiterführt.
|
||||
///
|
||||
/// Die drei Zustände brauchen drei verschiedene Antworten, und genau daran ist
|
||||
/// die erste Fassung gescheitert: „noch nie gefragt" lässt sich mit einer
|
||||
/// Anfrage lösen, „abgelehnt" nur über die Systemeinstellungen, und „erteilt"
|
||||
/// braucht gar nichts.
|
||||
private struct PermissionRow: View {
|
||||
enum State { case granted, denied, undetermined }
|
||||
enum Pane {
|
||||
case calendar, location
|
||||
|
||||
/// Jede Berechtigung führt in ihren eigenen Bereich. Ein gemeinsamer
|
||||
/// Knopf, der immer im Kalender landet, ist schlimmer als keiner.
|
||||
var url: URL? {
|
||||
switch self {
|
||||
case .calendar:
|
||||
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars")
|
||||
case .location:
|
||||
URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_LocationServices")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var didAutoRequest = false
|
||||
let title: LocalizedStringKey
|
||||
let symbol: String
|
||||
let state: State
|
||||
let explanation: LocalizedStringKey
|
||||
let pane: Pane
|
||||
let request: () async -> Void
|
||||
|
||||
private func openLocationSettings() {
|
||||
guard let url = URL(string:
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_LocationServices") else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
private func openPrivacySettings() {
|
||||
guard let url = URL(string:
|
||||
"x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") else { return }
|
||||
NSWorkspace.shared.open(url)
|
||||
var body: some View {
|
||||
HStack {
|
||||
Label(title, systemImage: symbol)
|
||||
Spacer()
|
||||
switch state {
|
||||
case .granted:
|
||||
Label("settings.permissions.granted", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.labelStyle(.titleAndIcon)
|
||||
case .undetermined:
|
||||
Button("settings.permissions.request") { Task { await request() } }
|
||||
case .denied:
|
||||
// macOS fragt nach einer Ablehnung nicht mehr — ein
|
||||
// Anfragen-Knopf wäre hier eine Lüge.
|
||||
Button("settings.permissions.openSystem") {
|
||||
if let url = pane.url { NSWorkspace.shared.open(url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(explanation)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +255,14 @@ private struct DisplaySettings: View {
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
Divider()
|
||||
|
||||
Toggle("settings.display.dockIcon", isOn: $model.showsDockIcon)
|
||||
Text("settings.display.dockIcon.hint")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.padding()
|
||||
|
||||
Reference in New Issue
Block a user