Compare commits

..
Author SHA1 Message Date
makiolaj 754745586d Run the Play job on trixie for fastlane's Ruby 3.2 requirement (#509) 2026-11-13 19:17:46 +01:00
Jean-Luc Makiola 1a1df07dc3 Release v2.24.2 (#511)
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 12s
Renovate / renovate (push) Successful in 1m53s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 17m38s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Failing after 34s
- Queued drops: moves made while an earlier one is still saving are queued instead of dropped, with messages for stale and skipped moves (#494)
- Day view title names the month (#501)
- Dependency updates: kotlinx-datetime, AppCompat, fastlane, Renovate image (#507–#510)
- Version 2.24.2 (2240299), CHANGELOG, What's New in all store locales

#494 stays open until the reporter answers; the fix covers the ways a quick move could get lost, the original report isn't reproduced.

Refs #494
Closes #501
2026-11-13 18:56:38 +01:00
makiolaj 75315d92a7 Merge main into release/2.24.2
Beta — Codeberg pre-release / detect (push) Successful in 5s
Beta — Codeberg pre-release / beta (push) Skipped
2026-11-13 16:10:53 +01:00
95ad3a30cf Release v2.24.1 (#504)
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 6s
Renovate / renovate (push) Successful in 1m20s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Successful in 19m27s
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Successful in 1m58s
- Agenda widget: Cards / Classic style option, opacity slider for all Solid/Tinted widgets (#489)
- Month widget fits events into short week rows (#491)
- Week numbers follow the locale's rules and the row's first day (#493)
- Readable dimmed events in dark widgets, pinned Light/Dark theme matches system colours (#475)
- Dropped unused WorkManager permissions: network state, wake lock, foreground service (#448)
- Weblate translations (#434), incl. the Arabic string from after the 2.24.0 squash
- Version 2.24.1 (2240199), CHANGELOG, What's New in all store locales

#489 deviates from the issue: the classic look comes back as a style option instead of replacing the new design.

Closes #489
Closes #491
Closes #493
Closes #475
Closes #448

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Co-authored-by: Sup99999 <alelmnour1o234@proton.me>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Louis Leblanc <sandraev@posteo.dk>
Co-authored-by: Dmitri Gabinski <dkgabi@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/504
2026-11-13 11:17:48 +01:00
Jean-Luc Makiola 8ce4831efa Translations update from Weblate (#434)
Release — F-Droid repo + Gitea/Codeberg release + Play / detect (push) Successful in 12s
Release — F-Droid repo + Gitea/Codeberg release + Play / release (push) Skipped
Release — F-Droid repo + Gitea/Codeberg release + Play / play (push) Skipped
Renovate / renovate (push) Successful in 2m59s
Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/434
2026-11-13 11:07:42 +01:00
8 changed files with 24 additions and 158 deletions

No files matched your search

+4 -1
View File
@@ -318,13 +318,16 @@ jobs:
# not provide node inside custom job containers, so JavaScript actions
# (checkout, download-artifact) can't run there. The Renovate job gets away
# with a container because its only step is a shell command. Ruby is installed
# the same way sshpass, jq and fdroidserver are in the job above.
# the same way sshpass, jq and fdroidserver are in the job above, from trixie:
# fastlane >= 2.241 needs Ruby 3.2+, and bookworm's apt only has 3.1.
play:
needs: [detect, release]
# workflow_dispatch is the F-Droid re-sign recovery path — it must never
# touch Play, so gate on a real release only.
if: needs.detect.outputs.is_release == 'true'
runs-on: docker
container:
image: node:22-trixie
env:
VERSION: ${{ needs.detect.outputs.version }}
VERSION_CODE: ${{ needs.detect.outputs.version_code }}
@@ -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
-3
View File
@@ -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))
}
}