feat: iOS Einstellungen – Profil, Privatsphäre, geteilter Kalender
Neue Settings-Sektionen: Anzeigename + E-Mail ändern (Login-Name read-only), Private-Termine-Sichtbarkeit (busy/hidden) und Auswahl des für Gruppen sichtbaren Kalenders. Gezielte API-PUTs, damit nicht die ganze AppSettings überschrieben wird. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,19 @@ private let strings: [String: [String: String]] = [
|
||||
"settings.monday": "Montag",
|
||||
"settings.sunday": "Sonntag",
|
||||
"settings.dimpast": "Vergangene Termine ausgrauen",
|
||||
"settings.nav.profile": "Profil",
|
||||
"settings.saved": "Gespeichert",
|
||||
"settings.privacy": "Privatsphäre",
|
||||
"settings.private_visibility": "Private Termine für Gruppen",
|
||||
"settings.private_visibility.desc": "Wie private Termine für andere Gruppenmitglieder erscheinen",
|
||||
"settings.private.busy": "Als „Beschäftigt“",
|
||||
"settings.private.hidden": "Ausblenden",
|
||||
"settings.calendars": "Geteilter Kalender",
|
||||
"settings.group_visible": "Für Gruppen sichtbar",
|
||||
"settings.group_visible.desc": "Wähle, welcher deiner Kalender für Gruppenmitglieder sichtbar ist",
|
||||
"group.visible.none": "Keiner",
|
||||
"profile.display_name": "Anzeigename",
|
||||
"profile.login_name": "Login-Name",
|
||||
|
||||
"settings.hourheight": "Stundenhöhe",
|
||||
"settings.hourheight.desc": "Platz pro Stunde in der Wochen- & Tagesansicht",
|
||||
@@ -389,6 +402,19 @@ private let strings: [String: [String: String]] = [
|
||||
"settings.monday": "Monday",
|
||||
"settings.sunday": "Sunday",
|
||||
"settings.dimpast": "Dim past events",
|
||||
"settings.nav.profile": "Profile",
|
||||
"settings.saved": "Saved",
|
||||
"settings.privacy": "Privacy",
|
||||
"settings.private_visibility": "Private events for groups",
|
||||
"settings.private_visibility.desc": "How your private events appear to other group members",
|
||||
"settings.private.busy": "Show as \"Busy\"",
|
||||
"settings.private.hidden": "Hide",
|
||||
"settings.calendars": "Shared calendar",
|
||||
"settings.group_visible": "Visible to groups",
|
||||
"settings.group_visible.desc": "Choose which of your calendars group members can see",
|
||||
"group.visible.none": "None",
|
||||
"profile.display_name": "Display name",
|
||||
"profile.login_name": "Login name",
|
||||
|
||||
"settings.hourheight": "Hour height",
|
||||
"settings.hourheight.desc": "Space per hour in week & day view",
|
||||
|
||||
@@ -414,6 +414,17 @@ class CalendarrAPI {
|
||||
return json?["access_token"] as? String
|
||||
}
|
||||
|
||||
// MARK: – Targeted settings (avoid overwriting the whole AppSettings)
|
||||
|
||||
func updatePrivateVisibility(_ value: String) async throws {
|
||||
_ = try await request("/api/settings/", method: "PUT", body: ["private_event_visibility": value])
|
||||
}
|
||||
|
||||
func updateGroupVisibleCalendar(_ calendarId: Int?) async throws {
|
||||
_ = try await request("/api/settings/", method: "PUT",
|
||||
body: ["group_visible_calendar_id": calendarId as Any? ?? NSNull()])
|
||||
}
|
||||
|
||||
// MARK: – Sharing
|
||||
|
||||
func getUserDirectory() async throws -> [DirectoryUser] {
|
||||
|
||||
@@ -23,9 +23,21 @@ struct SettingsView: View {
|
||||
@AppStorage("weekStartDay") private var weekStartDay = "monday"
|
||||
@AppStorage("dimPastEvents") private var dimPastEvents = false
|
||||
|
||||
// Profile chapter (server-backed; loaded on appear).
|
||||
@State private var displayName = ""
|
||||
@State private var loginName = ""
|
||||
@State private var email = ""
|
||||
@State private var privateVisibility = "busy"
|
||||
@State private var groupVisibleId = 0 // 0 = none
|
||||
@State private var ownLocalCals: [LocalCalendar] = []
|
||||
@State private var profileMsg = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
profilSection
|
||||
privatsphaereSection
|
||||
geteilterKalenderSection
|
||||
liquidGlassSection
|
||||
cacheSection
|
||||
spracheSection
|
||||
@@ -40,6 +52,7 @@ struct SettingsView: View {
|
||||
}
|
||||
// Reflect the latest server values when opening the screen.
|
||||
.task { await SettingsSync.pull(api: api) }
|
||||
.task { await loadProfile() }
|
||||
// Appearance changes update widgets live; synced values are also pushed
|
||||
// to the server (debounced). `push` itself decides what actually gets
|
||||
// sent based on the sync toggle, so every change can simply call it.
|
||||
@@ -62,6 +75,101 @@ struct SettingsView: View {
|
||||
.onChange(of: settingsSync) { _, on in if on { Task { await SettingsSync.pull(api: api) } } }
|
||||
}
|
||||
|
||||
// MARK: – Profil
|
||||
|
||||
var profilSection: some View {
|
||||
Section(L10n.t("settings.nav.profile", appLang)) {
|
||||
HStack {
|
||||
Text(L10n.t("profile.display_name", appLang))
|
||||
Spacer()
|
||||
TextField(L10n.t("profile.display_name", appLang), text: $displayName)
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
HStack {
|
||||
Text(L10n.t("profile.login_name", appLang))
|
||||
Spacer()
|
||||
Text(loginName).foregroundStyle(.secondary)
|
||||
}
|
||||
HStack {
|
||||
Text("E-Mail")
|
||||
Spacer()
|
||||
TextField("E-Mail", text: $email)
|
||||
.multilineTextAlignment(.trailing)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
Button(L10n.t("event.save", appLang)) { Task { await saveProfile() } }
|
||||
if !profileMsg.isEmpty {
|
||||
Text(profileMsg).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Privatsphäre
|
||||
|
||||
var privatsphaereSection: some View {
|
||||
Section {
|
||||
Picker(L10n.t("settings.private_visibility", appLang), selection: $privateVisibility) {
|
||||
Text(L10n.t("settings.private.busy", appLang)).tag("busy")
|
||||
Text(L10n.t("settings.private.hidden", appLang)).tag("hidden")
|
||||
}
|
||||
.onChange(of: privateVisibility) { _, v in
|
||||
Task { try? await api.updatePrivateVisibility(v) }
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.t("settings.privacy", appLang))
|
||||
} footer: {
|
||||
Text(L10n.t("settings.private_visibility.desc", appLang)).font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Geteilter Kalender
|
||||
|
||||
var geteilterKalenderSection: some View {
|
||||
Section {
|
||||
Picker(L10n.t("settings.group_visible", appLang), selection: $groupVisibleId) {
|
||||
Text(L10n.t("group.visible.none", appLang)).tag(0)
|
||||
ForEach(ownLocalCals) { cal in
|
||||
Text(cal.name).tag(cal.id)
|
||||
}
|
||||
}
|
||||
.onChange(of: groupVisibleId) { _, id in
|
||||
Task { try? await api.updateGroupVisibleCalendar(id == 0 ? nil : id) }
|
||||
}
|
||||
} header: {
|
||||
Text(L10n.t("settings.calendars", appLang))
|
||||
} footer: {
|
||||
Text(L10n.t("settings.group_visible.desc", appLang)).font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadProfile() async {
|
||||
if let p = try? await api.getProfile() {
|
||||
displayName = p.displayName ?? p.username
|
||||
loginName = p.username
|
||||
email = p.email ?? ""
|
||||
}
|
||||
if let s = try? await api.getSettings() {
|
||||
privateVisibility = s.privateEventVisibility
|
||||
groupVisibleId = s.groupVisibleCalendarId ?? 0
|
||||
}
|
||||
if let cals = try? await api.getLocalCalendars() {
|
||||
ownLocalCals = cals.filter { $0.owned && !$0.group }
|
||||
}
|
||||
}
|
||||
|
||||
private func saveProfile() async {
|
||||
do {
|
||||
_ = try await api.updateProfile(displayName: displayName.isEmpty ? nil : displayName,
|
||||
username: nil,
|
||||
email: email.isEmpty ? "" : email)
|
||||
UserDefaults.standard.set(displayName, forKey: "displayName")
|
||||
profileMsg = L10n.t("settings.saved", appLang)
|
||||
} catch {
|
||||
profileMsg = error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Liquid Glass
|
||||
|
||||
var liquidGlassSection: some View {
|
||||
|
||||
Reference in New Issue
Block a user