diff --git a/Calendarr iOS/Models/CalendarStore.swift b/Calendarr iOS/Models/CalendarStore.swift index eaf2581..6f924e2 100644 --- a/Calendarr iOS/Models/CalendarStore.swift +++ b/Calendarr iOS/Models/CalendarStore.swift @@ -70,6 +70,15 @@ class CalendarStore { /// show/hide list. Re-activation happens in AccountsView. var banishedCalendarKeys: Set = CalendarStore.loadBanishedKeys() + /// Group-overlay visibility: which members' calendars (`gm:`) and the + /// group calendar (`gc`) are hidden in the combined view — like hiding + /// individual people in Outlook. In-memory; resets when leaving/switching a + /// group (the per-calendar hide/banish sets are for the personal view only). + var hiddenGroupKeys: Set = [] + + static func groupMemberKey(_ ownerId: Int) -> String { "gm:\(ownerId)" } + static let groupCalendarKey = "gc" + // Cache bookkeeping private var cachedStart: Date? = nil private var cachedEnd: Date? = nil @@ -114,6 +123,19 @@ class CalendarStore { publishWidgetSnapshot() } + /// Toggle / replace group-overlay visibility (members or the group calendar). + func setGroupKeyHidden(_ key: String, hidden: Bool) { + if hidden { hiddenGroupKeys.insert(key) } else { hiddenGroupKeys.remove(key) } + let (s, e) = rangeForCurrentView() + refreshFromCache(start: s, end: e) + } + + func setHiddenGroupKeys(_ keys: Set) { + hiddenGroupKeys = keys + let (s, e) = rangeForCurrentView() + refreshFromCache(start: s, end: e) + } + static func calendarKey(source: String, calendarId: String) -> String { // The events API returns `calendar_id` inconsistently: a raw numeric for // CalDAV, but "-" for local / ical / google / homeassistant @@ -219,10 +241,18 @@ class CalendarStore { /// `start` / `end` are kept in the signature for call-site clarity. func refreshFromCache(start: Date, end: Date) { _ = (start, end) - // In group overlay mode show everything (the per-calendar hide/banish - // toggles are for the personal view only). + // In group overlay mode the per-calendar hide/banish toggles don't apply; + // instead honour the per-member / group-calendar toggles (hiddenGroupKeys). if activeGroup != nil { - events = allCachedEvents + if hiddenGroupKeys.isEmpty { + events = allCachedEvents + } else { + events = allCachedEvents.filter { ev in + if ev.isGroupEvent { return !hiddenGroupKeys.contains(Self.groupCalendarKey) } + if let o = ev.owner { return !hiddenGroupKeys.contains(Self.groupMemberKey(o.id ?? -1)) } + return true + } + } return } events = allCachedEvents.filter { ev in diff --git a/Calendarr iOS/Models/Localization.swift b/Calendarr iOS/Models/Localization.swift index 1be1469..67a2a2b 100644 --- a/Calendarr iOS/Models/Localization.swift +++ b/Calendarr iOS/Models/Localization.swift @@ -164,6 +164,7 @@ private let strings: [String: [String: String]] = [ "group.name": "Name", "group.icon": "Icon", "group.members": "Mitglieder", + "group.calendar": "Gruppenkalender", "group.member_colors": "Farben der Mitglieder", "group.delete": "Gruppe löschen", @@ -465,6 +466,7 @@ private let strings: [String: [String: String]] = [ "group.name": "Name", "group.icon": "Icon", "group.members": "Members", + "group.calendar": "Group calendar", "group.member_colors": "Member colours", "group.delete": "Delete group", diff --git a/Calendarr iOS/Views/Calendar/CalendarHostView.swift b/Calendarr iOS/Views/Calendar/CalendarHostView.swift index 18ce9f1..4fb127d 100644 --- a/Calendarr iOS/Views/Calendar/CalendarHostView.swift +++ b/Calendarr iOS/Views/Calendar/CalendarHostView.swift @@ -247,6 +247,7 @@ struct CalendarHostView: View { private func switchGroup(_ g: CalGroup?) { store.activeGroup = g + store.hiddenGroupKeys = [] // member visibility is per-group; start fresh // The cache holds the previous mode's events — drop it and reload the // visible range + prefetch a wide window so the whole grid is covered. Task { await forceReload() } diff --git a/Calendarr iOS/Views/CalendarFilterSheet.swift b/Calendarr iOS/Views/CalendarFilterSheet.swift index 57e9fc1..23d9ca6 100644 --- a/Calendarr iOS/Views/CalendarFilterSheet.swift +++ b/Calendarr iOS/Views/CalendarFilterSheet.swift @@ -20,12 +20,18 @@ struct CalendarFilterSheet: View { @State private var banished: Set = [] /// All non-banished keys discovered during load — used by bulk show/hide. @State private var allKeys: Set = [] + /// Group-mode: the active group's full detail (members + colours) and the + /// per-member / group-calendar hidden keys. + @State private var groupDetail: CalGroup? = nil + @State private var hiddenGroup: Set = [] var body: some View { NavigationStack { Group { if isLoading { ProgressView(L10n.t("filter.loading", appLang)) + } else if store.activeGroup != nil { + groupFilterList } else if allKeys.isEmpty { Text(L10n.t("filter.empty", appLang)) .foregroundStyle(.secondary) @@ -167,8 +173,61 @@ struct CalendarFilterSheet: View { } } + // MARK: – Group overlay filter (hide individual members / the group calendar) + + @ViewBuilder + private var groupFilterList: some View { + if let g = groupDetail { + List { + Section(header: Text("\(g.icon ?? "👥") \(g.name)")) { + ForEach(g.members ?? []) { m in + groupRow(name: m.displayName ?? "—", + colorHex: m.color ?? "#4285f4", + key: CalendarStore.groupMemberKey(m.id)) + } + groupRow(name: L10n.t("group.calendar", appLang), + colorHex: g.groupCalendarColor ?? "#4285f4", + key: CalendarStore.groupCalendarKey) + } + } + } else { + Text(L10n.t("filter.empty", appLang)).foregroundStyle(.secondary) + } + } + + @ViewBuilder + private func groupRow(name: String, colorHex: String, key: String) -> some View { + let isVisible = !hiddenGroup.contains(key) + Button { + if isVisible { hiddenGroup.insert(key) } else { hiddenGroup.remove(key) } + store.setGroupKeyHidden(key, hidden: isVisible) + } label: { + HStack(spacing: 12) { + Circle() + .fill(Color(hex: colorHex)) + .frame(width: 14, height: 14) + .opacity(isVisible ? 1.0 : 0.35) + Text(name) + .foregroundStyle(isVisible ? .primary : .secondary) + .strikethrough(!isVisible, color: .secondary) + Spacer() + Image(systemName: isVisible ? "eye" : "eye.slash") + .foregroundStyle(isVisible ? Color.accentColor : .secondary) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + private func load() async { isLoading = true + // Group overlay: list members (+ the group calendar) to hide individually. + if let g = store.activeGroup { + hiddenGroup = store.hiddenGroupKeys + groupDetail = try? await api.getGroup(id: g.id) + isLoading = false + return + } hidden = store.hiddenCalendarKeys banished = store.banishedCalendarKeys async let c = (try? await api.getCalDAVAccounts()) ?? []