diff --git a/Calendarr iOS/Models/AppSettings.swift b/Calendarr iOS/Models/AppSettings.swift index a90560f..2f3e7c3 100644 --- a/Calendarr iOS/Models/AppSettings.swift +++ b/Calendarr iOS/Models/AppSettings.swift @@ -223,6 +223,7 @@ struct UserProfile: Codable { let isAdmin: Bool let hasAvatar: Bool let totpEnabled: Bool + var directoryHidden: Bool = false enum CodingKeys: String, CodingKey { case id, username, email @@ -230,6 +231,19 @@ struct UserProfile: Codable { case isAdmin = "is_admin" case hasAvatar = "has_avatar" case totpEnabled = "totp_enabled" + case directoryHidden = "directory_hidden" + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(Int.self, forKey: .id) + username = try c.decode(String.self, forKey: .username) + displayName = try c.decodeIfPresent(String.self, forKey: .displayName) + email = try c.decodeIfPresent(String.self, forKey: .email) + isAdmin = try c.decodeIfPresent(Bool.self, forKey: .isAdmin) ?? false + hasAvatar = try c.decodeIfPresent(Bool.self, forKey: .hasAvatar) ?? false + totpEnabled = try c.decodeIfPresent(Bool.self, forKey: .totpEnabled) ?? false + directoryHidden = try c.decodeIfPresent(Bool.self, forKey: .directoryHidden) ?? false } } @@ -258,9 +272,20 @@ struct GroupMember: Codable, Identifiable { let displayName: String? var role: String var color: String? + var sharesCalendar: Bool = true enum CodingKeys: String, CodingKey { case id, role, color case displayName = "display_name" + case sharesCalendar = "shares_calendar" + } + + init(from decoder: Decoder) throws { + let c = try decoder.container(keyedBy: CodingKeys.self) + id = try c.decode(Int.self, forKey: .id) + displayName = try c.decodeIfPresent(String.self, forKey: .displayName) + role = try c.decodeIfPresent(String.self, forKey: .role) ?? "member" + color = try c.decodeIfPresent(String.self, forKey: .color) + sharesCalendar = try c.decodeIfPresent(Bool.self, forKey: .sharesCalendar) ?? true } } diff --git a/Calendarr iOS/Models/Localization.swift b/Calendarr iOS/Models/Localization.swift index d488375..bcabbae 100644 --- a/Calendarr iOS/Models/Localization.swift +++ b/Calendarr iOS/Models/Localization.swift @@ -132,6 +132,8 @@ private let strings: [String: [String: String]] = [ "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.directory_hidden": "Profil verbergen", + "settings.directory_hidden.desc": "Nicht in Teilen-/Gruppen-Auswahl anzeigen. In der Admin-Benutzerverwaltung bleibst du sichtbar.", "settings.private.busy": "Als „Beschäftigt“", "settings.private.hidden": "Ausblenden", "settings.calendars": "Geteilter Kalender", @@ -461,6 +463,8 @@ private let strings: [String: [String: String]] = [ "settings.privacy": "Privacy", "settings.private_visibility": "Private events for groups", "settings.private_visibility.desc": "How your private events appear to other group members", + "settings.directory_hidden": "Hide my profile", + "settings.directory_hidden.desc": "Don't show me in share/group pickers. You stay visible in the admin user management.", "settings.private.busy": "Show as \"Busy\"", "settings.private.hidden": "Hide", "settings.calendars": "Shared calendar", diff --git a/Calendarr iOS/Services/CalendarrAPI.swift b/Calendarr iOS/Services/CalendarrAPI.swift index 6f82672..7b8d2a3 100644 --- a/Calendarr iOS/Services/CalendarrAPI.swift +++ b/Calendarr iOS/Services/CalendarrAPI.swift @@ -450,11 +450,13 @@ class CalendarrAPI { /// Update profile fields. A login-name change returns a fresh token (the old /// one becomes invalid) — the caller must store the returned token. - func updateProfile(displayName: String?, username: String?, email: String?) async throws -> String? { + func updateProfile(displayName: String?, username: String?, email: String?, + directoryHidden: Bool? = nil) async throws -> String? { var body: [String: Any] = [:] if let d = displayName { body["display_name"] = d } if let u = username { body["username"] = u } if let e = email { body["email"] = e } else { body["email"] = NSNull() } + if let h = directoryHidden { body["directory_hidden"] = h } let data = try await request("/api/profile/", method: "PUT", body: body) let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] return json?["access_token"] as? String diff --git a/Calendarr iOS/Views/CalendarFilterSheet.swift b/Calendarr iOS/Views/CalendarFilterSheet.swift index 63f8efb..bd79e95 100644 --- a/Calendarr iOS/Views/CalendarFilterSheet.swift +++ b/Calendarr iOS/Views/CalendarFilterSheet.swift @@ -217,7 +217,9 @@ struct CalendarFilterSheet: View { if let g = groupDetail { List { Section(header: Label(g.name, systemImage: GroupIcons.symbol(g.icon))) { - ForEach(g.members ?? []) { m in + // Only members who actually share a calendar into the group — + // avoids phantom empty rows for members who share nothing. + ForEach((g.members ?? []).filter { $0.sharesCalendar }) { m in groupRow(name: m.displayName ?? "—", colorHex: m.color ?? "#4285f4", key: CalendarStore.groupMemberKey(m.id)) diff --git a/Calendarr iOS/Views/SettingsView.swift b/Calendarr iOS/Views/SettingsView.swift index 97caa2b..3cdde7a 100644 --- a/Calendarr iOS/Views/SettingsView.swift +++ b/Calendarr iOS/Views/SettingsView.swift @@ -32,6 +32,7 @@ struct SettingsView: View { @State private var privateVisibility = "busy" @State private var groupVisibleId = 0 // 0 = none @State private var ownLocalCals: [LocalCalendar] = [] + @State private var directoryHidden = false @State private var profileMsg = "" var body: some View { @@ -102,6 +103,13 @@ struct SettingsView: View { .keyboardType(.emailAddress) .autocapitalization(.none) } + Toggle(isOn: $directoryHidden) { + VStack(alignment: .leading, spacing: 2) { + Text(L10n.t("settings.directory_hidden", appLang)) + Text(L10n.t("settings.directory_hidden.desc", appLang)) + .font(.caption).foregroundStyle(.secondary) + } + } Button(L10n.t("event.save", appLang)) { Task { await saveProfile() } } if !profileMsg.isEmpty { Text(profileMsg).font(.caption).foregroundStyle(.secondary) @@ -188,6 +196,7 @@ struct SettingsView: View { displayName = p.displayName ?? p.username loginName = p.username email = p.email ?? "" + directoryHidden = p.directoryHidden } if let s = try? await api.getSettings() { privateVisibility = s.privateEventVisibility @@ -202,7 +211,8 @@ struct SettingsView: View { do { _ = try await api.updateProfile(displayName: displayName.isEmpty ? nil : displayName, username: nil, - email: email.isEmpty ? "" : email) + email: email.isEmpty ? "" : email, + directoryHidden: directoryHidden) UserDefaults.standard.set(displayName, forKey: "displayName") profileMsg = L10n.t("settings.saved", appLang) } catch {