diff --git a/Calendarr iOS/Models/Localization.swift b/Calendarr iOS/Models/Localization.swift index c2524cb..166befd 100644 --- a/Calendarr iOS/Models/Localization.swift +++ b/Calendarr iOS/Models/Localization.swift @@ -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", diff --git a/Calendarr iOS/Services/CalendarrAPI.swift b/Calendarr iOS/Services/CalendarrAPI.swift index bb6a9ab..a50230c 100644 --- a/Calendarr iOS/Services/CalendarrAPI.swift +++ b/Calendarr iOS/Services/CalendarrAPI.swift @@ -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] { diff --git a/Calendarr iOS/Views/SettingsView.swift b/Calendarr iOS/Views/SettingsView.swift index 01edd50..0c9e6c6 100644 --- a/Calendarr iOS/Views/SettingsView.swift +++ b/Calendarr iOS/Views/SettingsView.swift @@ -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 {