Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a1df07dc3 | ||
|
|
75315d92a7 | ||
|
|
95ad3a30cf | ||
|
|
8ce4831efa |
No files matched your search
@@ -32,11 +32,6 @@ class CalendarPrefs @Inject constructor(
|
||||
|
||||
suspend fun setHiddenCalendarIds(ids: Set<Long>) = prefs.set(HIDDEN_IDS, ids)
|
||||
|
||||
/** Rewrite the drawer's hidden set atomically from its current value. */
|
||||
suspend fun updateHiddenCalendarIds(transform: (Set<Long>) -> Set<Long>) {
|
||||
prefs.update(HIDDEN_IDS, transform)
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch-offs the provider does not know about yet, because writing
|
||||
* `Calendars.VISIBLE` needs `WRITE_CALENDAR` (#75). Also inherits the
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
package de.jeanlucmakiola.calendula.ui.filter
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TriStateCheckbox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.toggleableState
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -56,8 +46,7 @@ fun CalendarFilterList(
|
||||
is FilterUiState.Success -> FilterList(
|
||||
groups = s.groups,
|
||||
hidden = s.hidden,
|
||||
onToggleCalendar = viewModel::toggleCalendar,
|
||||
onToggleGroup = viewModel::toggleGroup,
|
||||
onSetVisible = viewModel::setVisible,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
@@ -72,17 +61,21 @@ fun CalendarFilterList(
|
||||
private fun FilterList(
|
||||
groups: List<CalendarAccountGroup>,
|
||||
hidden: Set<Long>,
|
||||
onToggleCalendar: (Long) -> Unit,
|
||||
onToggleGroup: (List<Long>) -> Unit,
|
||||
onSetVisible: (Long, Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
groups.forEach { group ->
|
||||
val ids = group.calendars.map { it.id }
|
||||
GroupHeader(
|
||||
title = accountGroupTitle(group),
|
||||
state = groupVisibility(ids, hidden),
|
||||
onToggle = { onToggleGroup(ids) },
|
||||
Text(
|
||||
text = accountGroupTitle(group),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(
|
||||
start = GroupedListInset,
|
||||
end = GroupedListInset,
|
||||
top = 12.dp,
|
||||
bottom = 4.dp,
|
||||
),
|
||||
)
|
||||
group.calendars.forEachIndexed { index, cal ->
|
||||
val visible = cal.id !in hidden
|
||||
@@ -94,54 +87,16 @@ private fun FilterList(
|
||||
trailing = {
|
||||
Checkbox(
|
||||
checked = visible,
|
||||
onCheckedChange = { onToggleCalendar(cal.id) },
|
||||
onCheckedChange = { onSetVisible(cal.id, it) },
|
||||
)
|
||||
},
|
||||
onClick = { onToggleCalendar(cal.id) },
|
||||
onClick = { onSetVisible(cal.id, !visible) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An account's header doubling as the group's parent checkbox (#143): checked
|
||||
* when every calendar shows, indeterminate when some do. Tapping it shows all,
|
||||
* or hides all when the group is fully shown.
|
||||
*/
|
||||
@Composable
|
||||
private fun GroupHeader(
|
||||
title: String,
|
||||
state: ToggleableState,
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
val label = stringResource(
|
||||
if (state.nextGroupVisible()) R.string.filter_group_show_all else R.string.filter_group_hide_all,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = GroupedListInset / 2)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.clickable(onClickLabel = label, role = Role.Checkbox) { onToggle() }
|
||||
.semantics { toggleableState = state }
|
||||
.padding(start = GroupedListInset / 2, end = GroupedListInset / 2 + 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 8.dp),
|
||||
)
|
||||
Box(Modifier.size(48.dp), contentAlignment = Alignment.Center) {
|
||||
TriStateCheckbox(state = state, onClick = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FilterLoading(modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
|
||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -47,19 +48,13 @@ class FilterViewModel @Inject constructor(
|
||||
initialValue = FilterUiState.Loading,
|
||||
)
|
||||
|
||||
/**
|
||||
* Flip one calendar, or a whole group from its header (#143). Both decide
|
||||
* against the stored set rather than the last drawn state, so quick repeated
|
||||
* taps still alternate.
|
||||
*/
|
||||
fun toggleCalendar(calendarId: Long) {
|
||||
/** Show or hide a single calendar; persists the new hidden set. */
|
||||
fun setVisible(calendarId: Long, visible: Boolean) {
|
||||
viewModelScope.launch {
|
||||
prefs.updateHiddenCalendarIds { if (calendarId in it) it - calendarId else it + calendarId }
|
||||
val current = prefs.hiddenCalendarIds.first()
|
||||
val next = if (visible) current - calendarId else current + calendarId
|
||||
if (next != current) prefs.setHiddenCalendarIds(next)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleGroup(calendarIds: List<Long>) {
|
||||
viewModelScope.launch { prefs.updateHiddenCalendarIds { toggledGroup(it, calendarIds) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package de.jeanlucmakiola.calendula.ui.filter
|
||||
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
|
||||
/** The drawer group header's checkbox state: on, off, or some of [calendarIds] shown. */
|
||||
internal fun groupVisibility(calendarIds: Collection<Long>, hidden: Set<Long>): ToggleableState {
|
||||
val shown = calendarIds.count { it !in hidden }
|
||||
return when (shown) {
|
||||
0 -> ToggleableState.Off
|
||||
calendarIds.size -> ToggleableState.On
|
||||
else -> ToggleableState.Indeterminate
|
||||
}
|
||||
}
|
||||
|
||||
/** What tapping the header does: a fully shown group hides, anything else shows all. */
|
||||
internal fun ToggleableState.nextGroupVisible(): Boolean = this != ToggleableState.On
|
||||
|
||||
/** [hidden] after tapping the header of the group holding [calendarIds]. */
|
||||
internal fun toggledGroup(hidden: Set<Long>, calendarIds: Collection<Long>): Set<Long> =
|
||||
if (groupVisibility(calendarIds, hidden).nextGroupVisible()) hidden - calendarIds.toSet() else hidden + calendarIds
|
||||
@@ -550,9 +550,6 @@
|
||||
|
||||
<!-- Calendar filter (M3) -->
|
||||
<string name="filter_title">Calendars</string>
|
||||
<!-- TalkBack action on a drawer account header that toggles all its calendars -->
|
||||
<string name="filter_group_show_all">Show all calendars in this group</string>
|
||||
<string name="filter_group_hide_all">Hide all calendars in this group</string>
|
||||
|
||||
<!-- Settings (M4) -->
|
||||
<string name="settings_title">Settings</string>
|
||||
|
||||
@@ -34,16 +34,6 @@ class CalendarPrefsTest {
|
||||
assertThat(prefs.hiddenCalendarIds.first()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateHiddenCalendarIds transforms the stored set`(@TempDir tempDir: Path) = runTest {
|
||||
val prefs = testPrefs(tempDir)
|
||||
prefs.setHiddenCalendarIds(setOf(9L))
|
||||
prefs.updateHiddenCalendarIds { it + listOf(1L, 2L) }
|
||||
assertThat(prefs.hiddenCalendarIds.first()).isEqualTo(setOf(1L, 2L, 9L))
|
||||
prefs.updateHiddenCalendarIds { emptySet() }
|
||||
assertThat(prefs.hiddenCalendarIds.first()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `garbage stored string is parsed defensively`(@TempDir tempDir: Path) = runTest {
|
||||
val store = testDataStore(tempDir, "test_prefs")
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package de.jeanlucmakiola.calendula.ui.filter
|
||||
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class GroupVisibilityTest {
|
||||
|
||||
private val group = listOf(1L, 2L, 3L)
|
||||
|
||||
@Test
|
||||
fun `all shown is on and hides on tap`() {
|
||||
val state = groupVisibility(group, hidden = setOf(9L))
|
||||
assertThat(state).isEqualTo(ToggleableState.On)
|
||||
assertThat(state.nextGroupVisible()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `none shown is off and shows on tap`() {
|
||||
val state = groupVisibility(group, hidden = setOf(1L, 2L, 3L))
|
||||
assertThat(state).isEqualTo(ToggleableState.Off)
|
||||
assertThat(state.nextGroupVisible()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `some shown is indeterminate and shows all on tap`() {
|
||||
val state = groupVisibility(group, hidden = setOf(2L))
|
||||
assertThat(state).isEqualTo(ToggleableState.Indeterminate)
|
||||
assertThat(state.nextGroupVisible()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty group is off`() {
|
||||
assertThat(groupVisibility(emptyList(), hidden = emptySet())).isEqualTo(ToggleableState.Off)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toggling twice from the stored set alternates`() {
|
||||
val hidden = setOf(9L)
|
||||
val once = toggledGroup(hidden, group)
|
||||
assertThat(once).isEqualTo(setOf(1L, 2L, 3L, 9L))
|
||||
assertThat(toggledGroup(once, group)).isEqualTo(setOf(9L))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toggling a partly shown group shows all`() {
|
||||
assertThat(toggledGroup(setOf(2L, 9L), group)).isEqualTo(setOf(9L))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user