feat: hide individual member calendars in the group view (Android)
The calendar filter, in group mode, lists the group's members (+ the shared group calendar) with toggles to hide each individually. Filtering is client-side via CalendarViewModel.hiddenGroupKeys (gm:<id> / gc), reset on group switch; members + colours loaded from the group detail. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,20 @@ fun CalendarFilterSheet(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by vm.state.collectAsState()
|
val state by vm.state.collectAsState()
|
||||||
|
val groupMode = state.activeGroup != null
|
||||||
|
|
||||||
|
// In group mode the filter lists members (+ the group calendar) so they can
|
||||||
|
// be hidden individually, Outlook-style; otherwise the normal calendars.
|
||||||
|
val groupEntries: List<CalendarFilterEntry> = if (groupMode) {
|
||||||
|
buildList {
|
||||||
|
state.activeGroupMembers.forEach { m ->
|
||||||
|
add(CalendarFilterEntry(groupMemberKey(m.id), m.displayName, m.color ?: "#4285f4"))
|
||||||
|
}
|
||||||
|
add(CalendarFilterEntry(GROUP_CALENDAR_KEY, tr("groups.calendar"), state.activeGroup?.groupCalendarColor ?: "#4285f4"))
|
||||||
|
}
|
||||||
|
} else emptyList()
|
||||||
|
val rows = if (groupMode) groupEntries else events
|
||||||
|
val hiddenSet = if (groupMode) state.hiddenGroupKeys else state.hiddenKeys
|
||||||
|
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||||
Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
|
Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||||
@@ -46,21 +60,24 @@ fun CalendarFilterSheet(
|
|||||||
) {
|
) {
|
||||||
Text(tr("filter.title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
Text(tr("filter.title"), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||||
Row {
|
Row {
|
||||||
TextButton(onClick = { vm.setHiddenCalendars(emptySet()) }) { Text(tr("filter.show_all")) }
|
|
||||||
TextButton(onClick = {
|
TextButton(onClick = {
|
||||||
vm.setHiddenCalendars(events.map { it.key }.toSet())
|
if (groupMode) vm.setHiddenGroupKeys(emptySet()) else vm.setHiddenCalendars(emptySet())
|
||||||
|
}) { Text(tr("filter.show_all")) }
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (groupMode) vm.setHiddenGroupKeys(rows.map { it.key }.toSet())
|
||||||
|
else vm.setHiddenCalendars(rows.map { it.key }.toSet())
|
||||||
}) { Text(tr("filter.hide_all")) }
|
}) { Text(tr("filter.hide_all")) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (events.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
tr("filter.empty"),
|
tr("filter.empty"),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(vertical = 24.dp),
|
modifier = Modifier.padding(vertical = 24.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
events.forEach { entry ->
|
rows.forEach { entry ->
|
||||||
val visible = entry.key !in state.hiddenKeys
|
val visible = entry.key !in hiddenSet
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -73,7 +90,10 @@ fun CalendarFilterSheet(
|
|||||||
)
|
)
|
||||||
Switch(
|
Switch(
|
||||||
checked = visible,
|
checked = visible,
|
||||||
onCheckedChange = { vm.setCalendarHidden(entry.key, hidden = !it) },
|
onCheckedChange = {
|
||||||
|
if (groupMode) vm.setGroupKeyHidden(entry.key, hidden = !it)
|
||||||
|
else vm.setCalendarHidden(entry.key, hidden = !it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.scarriffle.calendarr.data.SettingsStore
|
|||||||
import com.scarriffle.calendarr.domain.model.CalEvent
|
import com.scarriffle.calendarr.domain.model.CalEvent
|
||||||
import com.scarriffle.calendarr.domain.model.CalViewType
|
import com.scarriffle.calendarr.domain.model.CalViewType
|
||||||
import com.scarriffle.calendarr.domain.model.Group
|
import com.scarriffle.calendarr.domain.model.Group
|
||||||
|
import com.scarriffle.calendarr.domain.model.GroupMember
|
||||||
import com.scarriffle.calendarr.domain.model.WritableCalendar
|
import com.scarriffle.calendarr.domain.model.WritableCalendar
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -42,8 +43,15 @@ data class CalendarUiState(
|
|||||||
// Group overlay: when non-null the calendar shows the group's combined view.
|
// Group overlay: when non-null the calendar shows the group's combined view.
|
||||||
val groups: List<Group> = emptyList(),
|
val groups: List<Group> = emptyList(),
|
||||||
val activeGroup: Group? = null,
|
val activeGroup: Group? = null,
|
||||||
|
// Group overlay: full member list (for the filter) + per-member / group-cal
|
||||||
|
// hidden keys ("gm:<userId>" / "gc"). In-memory; reset when switching group.
|
||||||
|
val activeGroupMembers: List<GroupMember> = emptyList(),
|
||||||
|
val hiddenGroupKeys: Set<String> = emptySet(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun groupMemberKey(ownerId: Int): String = "gm:$ownerId"
|
||||||
|
const val GROUP_CALENDAR_KEY = "gc"
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class CalendarViewModel @Inject constructor(
|
class CalendarViewModel @Inject constructor(
|
||||||
private val repository: CalendarRepository,
|
private val repository: CalendarRepository,
|
||||||
@@ -252,9 +260,18 @@ class CalendarViewModel @Inject constructor(
|
|||||||
|
|
||||||
private fun refreshFromCache() {
|
private fun refreshFromCache() {
|
||||||
val st = _state.value
|
val st = _state.value
|
||||||
// In group mode the server already scopes + filters; show everything.
|
// In group mode: server scopes/filters by privacy; locally honour the
|
||||||
|
// per-member / group-calendar hide toggles (hiddenGroupKeys).
|
||||||
val visible = if (st.activeGroup != null) {
|
val visible = if (st.activeGroup != null) {
|
||||||
allCachedEvents
|
val hg = st.hiddenGroupKeys
|
||||||
|
if (hg.isEmpty()) allCachedEvents
|
||||||
|
else allCachedEvents.filter { ev ->
|
||||||
|
when {
|
||||||
|
ev.isGroupEvent -> GROUP_CALENDAR_KEY !in hg
|
||||||
|
ev.owner != null -> groupMemberKey(ev.owner.id ?: -1) !in hg
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val hidden = st.hiddenKeys
|
val hidden = st.hiddenKeys
|
||||||
val banished = st.banishedKeys
|
val banished = st.banishedKeys
|
||||||
@@ -329,9 +346,33 @@ class CalendarViewModel @Inject constructor(
|
|||||||
/** Flip between personal and a group's combined overlay; reloads the wide window. */
|
/** Flip between personal and a group's combined overlay; reloads the wide window. */
|
||||||
fun switchGroup(group: Group?) {
|
fun switchGroup(group: Group?) {
|
||||||
if (_state.value.activeGroup?.id == group?.id) return
|
if (_state.value.activeGroup?.id == group?.id) return
|
||||||
_state.update { it.copy(activeGroup = group) }
|
_state.update {
|
||||||
|
it.copy(activeGroup = group, hiddenGroupKeys = emptySet(), activeGroupMembers = emptyList())
|
||||||
|
}
|
||||||
invalidateCache()
|
invalidateCache()
|
||||||
initialLoad()
|
initialLoad()
|
||||||
|
// Load the full member list (with server colours) for the filter sheet.
|
||||||
|
if (group != null) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
runCatching { repository.getGroup(group.id) }
|
||||||
|
.onSuccess { g -> _state.update { it.copy(activeGroupMembers = g.members) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Toggle a single member's calendar / the group calendar in the overlay. */
|
||||||
|
fun setGroupKeyHidden(key: String, hidden: Boolean) {
|
||||||
|
_state.update {
|
||||||
|
val next = it.hiddenGroupKeys.toMutableSet().apply { if (hidden) add(key) else remove(key) }
|
||||||
|
it.copy(hiddenGroupKeys = next)
|
||||||
|
}
|
||||||
|
refreshFromCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Replace the group-overlay hidden set (bulk show/hide all). */
|
||||||
|
fun setHiddenGroupKeys(keys: Set<String>) {
|
||||||
|
_state.update { it.copy(hiddenGroupKeys = keys) }
|
||||||
|
refreshFromCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Writable calendars ----
|
// ---- Writable calendars ----
|
||||||
|
|||||||
Reference in New Issue
Block a user