feat: hide individual member calendars in the group view (iOS)
The calendar filter, when a group overlay is active, now lists the group's members (+ the shared group calendar) and lets you hide each one individually (Outlook-style). Filtering is client-side via CalendarStore.hiddenGroupKeys (per-member gm:<id> / group-calendar gc keys), reset when switching groups. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -70,6 +70,15 @@ class CalendarStore {
|
|||||||
/// show/hide list. Re-activation happens in AccountsView.
|
/// show/hide list. Re-activation happens in AccountsView.
|
||||||
var banishedCalendarKeys: Set<String> = CalendarStore.loadBanishedKeys()
|
var banishedCalendarKeys: Set<String> = CalendarStore.loadBanishedKeys()
|
||||||
|
|
||||||
|
/// Group-overlay visibility: which members' calendars (`gm:<userId>`) 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<String> = []
|
||||||
|
|
||||||
|
static func groupMemberKey(_ ownerId: Int) -> String { "gm:\(ownerId)" }
|
||||||
|
static let groupCalendarKey = "gc"
|
||||||
|
|
||||||
// Cache bookkeeping
|
// Cache bookkeeping
|
||||||
private var cachedStart: Date? = nil
|
private var cachedStart: Date? = nil
|
||||||
private var cachedEnd: Date? = nil
|
private var cachedEnd: Date? = nil
|
||||||
@@ -114,6 +123,19 @@ class CalendarStore {
|
|||||||
publishWidgetSnapshot()
|
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<String>) {
|
||||||
|
hiddenGroupKeys = keys
|
||||||
|
let (s, e) = rangeForCurrentView()
|
||||||
|
refreshFromCache(start: s, end: e)
|
||||||
|
}
|
||||||
|
|
||||||
static func calendarKey(source: String, calendarId: String) -> String {
|
static func calendarKey(source: String, calendarId: String) -> String {
|
||||||
// The events API returns `calendar_id` inconsistently: a raw numeric for
|
// The events API returns `calendar_id` inconsistently: a raw numeric for
|
||||||
// CalDAV, but "<source>-<id>" for local / ical / google / homeassistant
|
// CalDAV, but "<source>-<id>" for local / ical / google / homeassistant
|
||||||
@@ -219,10 +241,18 @@ class CalendarStore {
|
|||||||
/// `start` / `end` are kept in the signature for call-site clarity.
|
/// `start` / `end` are kept in the signature for call-site clarity.
|
||||||
func refreshFromCache(start: Date, end: Date) {
|
func refreshFromCache(start: Date, end: Date) {
|
||||||
_ = (start, end)
|
_ = (start, end)
|
||||||
// In group overlay mode show everything (the per-calendar hide/banish
|
// In group overlay mode the per-calendar hide/banish toggles don't apply;
|
||||||
// toggles are for the personal view only).
|
// instead honour the per-member / group-calendar toggles (hiddenGroupKeys).
|
||||||
if activeGroup != nil {
|
if activeGroup != nil {
|
||||||
|
if hiddenGroupKeys.isEmpty {
|
||||||
events = allCachedEvents
|
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
|
return
|
||||||
}
|
}
|
||||||
events = allCachedEvents.filter { ev in
|
events = allCachedEvents.filter { ev in
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ private let strings: [String: [String: String]] = [
|
|||||||
"group.name": "Name",
|
"group.name": "Name",
|
||||||
"group.icon": "Icon",
|
"group.icon": "Icon",
|
||||||
"group.members": "Mitglieder",
|
"group.members": "Mitglieder",
|
||||||
|
"group.calendar": "Gruppenkalender",
|
||||||
"group.member_colors": "Farben der Mitglieder",
|
"group.member_colors": "Farben der Mitglieder",
|
||||||
"group.delete": "Gruppe löschen",
|
"group.delete": "Gruppe löschen",
|
||||||
|
|
||||||
@@ -465,6 +466,7 @@ private let strings: [String: [String: String]] = [
|
|||||||
"group.name": "Name",
|
"group.name": "Name",
|
||||||
"group.icon": "Icon",
|
"group.icon": "Icon",
|
||||||
"group.members": "Members",
|
"group.members": "Members",
|
||||||
|
"group.calendar": "Group calendar",
|
||||||
"group.member_colors": "Member colours",
|
"group.member_colors": "Member colours",
|
||||||
"group.delete": "Delete group",
|
"group.delete": "Delete group",
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ struct CalendarHostView: View {
|
|||||||
|
|
||||||
private func switchGroup(_ g: CalGroup?) {
|
private func switchGroup(_ g: CalGroup?) {
|
||||||
store.activeGroup = g
|
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
|
// 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.
|
// visible range + prefetch a wide window so the whole grid is covered.
|
||||||
Task { await forceReload() }
|
Task { await forceReload() }
|
||||||
|
|||||||
@@ -20,12 +20,18 @@ struct CalendarFilterSheet: View {
|
|||||||
@State private var banished: Set<String> = []
|
@State private var banished: Set<String> = []
|
||||||
/// All non-banished keys discovered during load — used by bulk show/hide.
|
/// All non-banished keys discovered during load — used by bulk show/hide.
|
||||||
@State private var allKeys: Set<String> = []
|
@State private var allKeys: Set<String> = []
|
||||||
|
/// 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<String> = []
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Group {
|
Group {
|
||||||
if isLoading {
|
if isLoading {
|
||||||
ProgressView(L10n.t("filter.loading", appLang))
|
ProgressView(L10n.t("filter.loading", appLang))
|
||||||
|
} else if store.activeGroup != nil {
|
||||||
|
groupFilterList
|
||||||
} else if allKeys.isEmpty {
|
} else if allKeys.isEmpty {
|
||||||
Text(L10n.t("filter.empty", appLang))
|
Text(L10n.t("filter.empty", appLang))
|
||||||
.foregroundStyle(.secondary)
|
.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 {
|
private func load() async {
|
||||||
isLoading = true
|
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
|
hidden = store.hiddenCalendarKeys
|
||||||
banished = store.banishedCalendarKeys
|
banished = store.banishedCalendarKeys
|
||||||
async let c = (try? await api.getCalDAVAccounts()) ?? []
|
async let c = (try? await api.getCalDAVAccounts()) ?? []
|
||||||
|
|||||||
Reference in New Issue
Block a user