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:
Scarriffle
2026-05-31 19:35:22 +02:00
parent e7e4998fb9
commit 023f90be3b
3 changed files with 145 additions and 0 deletions

View File

@@ -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",

View File

@@ -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] {

View File

@@ -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 {