Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c04b2ecee2 |
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.20.1] — 2026-09-07
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **An empty calendar now explains itself.** "No calendars configured" offered
|
||||||
|
an "Open system calendar settings" button that opened nothing — every screen
|
||||||
|
wired it to the same reload that had just produced the empty state. It now
|
||||||
|
opens the system's account chooser, filtered to the providers that sync
|
||||||
|
calendars ([#239]).
|
||||||
|
- **Calendars that are all switched off no longer look like an empty month.**
|
||||||
|
When every calendar on the device is hidden at system level there is nothing
|
||||||
|
to draw, but the app said nothing at all: it only ever checked whether the
|
||||||
|
device had *no* calendars, so a device that had them and hid them fell
|
||||||
|
through to a blank grid with no hint. That case now says so, and points at
|
||||||
|
Settings → Calendars — the one screen that can switch a calendar back on. The
|
||||||
|
navigation drawer's calendar list drew the same conflation and is fixed with
|
||||||
|
it ([#239]).
|
||||||
|
|
||||||
## [2.20.0] — 2026-09-07
|
## [2.20.0] — 2026-09-07
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -1654,3 +1671,4 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#150]: https://codeberg.org/jlmakiola/calendula/issues/150
|
[#150]: https://codeberg.org/jlmakiola/calendula/issues/150
|
||||||
[#146]: https://codeberg.org/jlmakiola/calendula/issues/146
|
[#146]: https://codeberg.org/jlmakiola/calendula/issues/146
|
||||||
[#267]: https://codeberg.org/jlmakiola/calendula/issues/267
|
[#267]: https://codeberg.org/jlmakiola/calendula/issues/267
|
||||||
|
[#239]: https://codeberg.org/jlmakiola/calendula/issues/239
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ android {
|
|||||||
// which builds this version and then creates the matching vX.Y.Z tag +
|
// which builds this version and then creates the matching vX.Y.Z tag +
|
||||||
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
||||||
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
||||||
versionCode = 22000
|
versionCode = 22001
|
||||||
versionName = "2.20.0"
|
versionName = "2.20.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,3 +46,19 @@ fun hasSystemHiddenCalendars(
|
|||||||
calendars: List<CalendarSource>,
|
calendars: List<CalendarSource>,
|
||||||
pendingDisabledIds: Set<Long>,
|
pendingDisabledIds: Set<Long>,
|
||||||
): Boolean = calendars.any { !it.isVisibleInSystem && it.id !in pendingDisabledIds }
|
): Boolean = calendars.any { !it.isVisibleInSystem && it.id !in pendingDisabledIds }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The failure a calendar list implies, or null when there is something to show.
|
||||||
|
* Distinguishes "the device has no calendars" from "every one of them is
|
||||||
|
* switched off" (#239) — both leave the grid empty, but only the second is
|
||||||
|
* fixed from Settings → Calendars.
|
||||||
|
*
|
||||||
|
* The filter sheet's own hidden set is deliberately not folded in: emptying it
|
||||||
|
* is a deliberate act taken moments earlier, in a sheet that is still one tap
|
||||||
|
* away.
|
||||||
|
*/
|
||||||
|
fun calendarListFailure(calendars: List<CalendarSource>): FailureReason? = when {
|
||||||
|
calendars.isEmpty() -> FailureReason.NoCalendarsConfigured
|
||||||
|
calendars.none { it.isVisibleInSystem } -> FailureReason.AllCalendarsHidden
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ enum class RecurringWriteScope {
|
|||||||
enum class FailureReason {
|
enum class FailureReason {
|
||||||
PermissionRevoked,
|
PermissionRevoked,
|
||||||
NoCalendarsConfigured,
|
NoCalendarsConfigured,
|
||||||
|
AllCalendarsHidden,
|
||||||
ProviderUnavailable,
|
ProviderUnavailable,
|
||||||
EventNotFound,
|
EventNotFound,
|
||||||
Unknown,
|
Unknown,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.EventMoveHost
|
import de.jeanlucmakiola.calendula.ui.common.EventMoveHost
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventMoveScope
|
import de.jeanlucmakiola.calendula.ui.common.EventMoveScope
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
import de.jeanlucmakiola.calendula.ui.common.LocalEventMove
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalManageCalendars
|
||||||
import de.jeanlucmakiola.calendula.ui.common.RescheduleViewModel
|
import de.jeanlucmakiola.calendula.ui.common.RescheduleViewModel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.drillToDay
|
import de.jeanlucmakiola.calendula.ui.common.drillToDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||||
@@ -344,7 +345,12 @@ fun CalendarHost(
|
|||||||
// navigation, so it fades through rather than sliding — paging *within* a
|
// navigation, so it fades through rather than sliding — paging *within* a
|
||||||
// view keeps the directional slide. AnimatedContent keyed on the view type.
|
// view keeps the directional slide. AnimatedContent keyed on the view type.
|
||||||
val viewSwitch = fadeThrough()
|
val viewSwitch = fadeThrough()
|
||||||
CompositionLocalProvider(LocalEventMove provides moveScope) {
|
CompositionLocalProvider(
|
||||||
|
LocalEventMove provides moveScope,
|
||||||
|
// The failure state's way out when every calendar is switched off
|
||||||
|
// (#239); nothing else in the calendar surfaces navigates here.
|
||||||
|
LocalManageCalendars provides { showCalendars = true },
|
||||||
|
) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = view,
|
targetState = view,
|
||||||
transitionSpec = { viewSwitch },
|
transitionSpec = { viewSwitch },
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import de.jeanlucmakiola.calendula.data.prefs.firstDayOfWeek
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -156,9 +157,7 @@ class AgendaViewModel @Inject constructor(
|
|||||||
calendars: List<CalendarSource>,
|
calendars: List<CalendarSource>,
|
||||||
instances: List<EventInstance>,
|
instances: List<EventInstance>,
|
||||||
): AgendaUiState {
|
): AgendaUiState {
|
||||||
if (calendars.isEmpty()) {
|
calendarListFailure(calendars)?.let { return AgendaUiState.Failure(it) }
|
||||||
return AgendaUiState.Failure(FailureReason.NoCalendarsConfigured)
|
|
||||||
}
|
|
||||||
val anchor = params.anchor
|
val anchor = params.anchor
|
||||||
val rangeEnd = anchor.plus(
|
val rangeEnd = anchor.plus(
|
||||||
params.range.dayCount(anchor, params.weekStart) - 1,
|
params.range.dayCount(anchor, params.weekStart) - 1,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.common
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.provider.CalendarContract
|
||||||
|
import android.provider.Settings
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -10,8 +14,10 @@ import androidx.compose.material3.FilledTonalButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -24,18 +30,29 @@ import de.jeanlucmakiola.calendula.domain.FailureReason
|
|||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CalendarFailure(reason: FailureReason, onRetry: () -> Unit) {
|
fun CalendarFailure(reason: FailureReason, onRetry: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val manageCalendars = LocalManageCalendars.current
|
||||||
val titleRes = when (reason) {
|
val titleRes = when (reason) {
|
||||||
FailureReason.PermissionRevoked -> R.string.state_failure_permission
|
FailureReason.PermissionRevoked -> R.string.state_failure_permission
|
||||||
FailureReason.NoCalendarsConfigured -> R.string.state_failure_no_calendars
|
FailureReason.NoCalendarsConfigured -> R.string.state_failure_no_calendars
|
||||||
|
FailureReason.AllCalendarsHidden -> R.string.state_failure_all_hidden
|
||||||
FailureReason.ProviderUnavailable -> R.string.state_failure_provider
|
FailureReason.ProviderUnavailable -> R.string.state_failure_provider
|
||||||
FailureReason.Unknown,
|
FailureReason.Unknown,
|
||||||
FailureReason.EventNotFound -> R.string.state_failure_unknown
|
FailureReason.EventNotFound -> R.string.state_failure_unknown
|
||||||
}
|
}
|
||||||
val actionRes = when (reason) {
|
val actionRes = when (reason) {
|
||||||
FailureReason.NoCalendarsConfigured -> R.string.state_failure_no_calendars_action
|
FailureReason.NoCalendarsConfigured -> R.string.state_failure_no_calendars_action
|
||||||
|
FailureReason.AllCalendarsHidden -> R.string.state_failure_all_hidden_action
|
||||||
FailureReason.PermissionRevoked -> R.string.state_failure_permission_action
|
FailureReason.PermissionRevoked -> R.string.state_failure_permission_action
|
||||||
else -> R.string.state_retry
|
else -> R.string.state_retry
|
||||||
}
|
}
|
||||||
|
val onAction: () -> Unit = when (reason) {
|
||||||
|
FailureReason.NoCalendarsConfigured -> {
|
||||||
|
{ context.startCalendarSetup() }
|
||||||
|
}
|
||||||
|
FailureReason.AllCalendarsHidden -> manageCalendars ?: onRetry
|
||||||
|
else -> onRetry
|
||||||
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -49,8 +66,30 @@ fun CalendarFailure(reason: FailureReason, onRetry: () -> Unit) {
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
FilledTonalButton(onClick = onRetry) {
|
FilledTonalButton(onClick = onAction) {
|
||||||
Text(stringResource(actionRes))
|
Text(stringResource(actionRes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens Settings → Calendars, the only screen that can switch a calendar back
|
||||||
|
* on. Null outside the calendar host — screens without it never raise the
|
||||||
|
* reason that needs it, and fall back to their own retry.
|
||||||
|
*/
|
||||||
|
val LocalManageCalendars = compositionLocalOf<(() -> Unit)?> { null }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the system screen for putting a calendar on the device: the account
|
||||||
|
* chooser filtered to calendar providers, falling back to account settings
|
||||||
|
* where no chooser resolves. Both are system components, so nothing here needs
|
||||||
|
* a `<queries>` entry.
|
||||||
|
*/
|
||||||
|
private fun Context.startCalendarSetup() {
|
||||||
|
val intents = listOf(
|
||||||
|
Intent(Settings.ACTION_ADD_ACCOUNT)
|
||||||
|
.putExtra(Settings.EXTRA_AUTHORITIES, arrayOf(CalendarContract.AUTHORITY)),
|
||||||
|
Intent(Settings.ACTION_SYNC_SETTINGS),
|
||||||
|
)
|
||||||
|
intents.firstOrNull { runCatching { startActivity(it) }.isSuccess }
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import de.jeanlucmakiola.calendula.data.di.IoDispatcher
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
|
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
|
||||||
import de.jeanlucmakiola.calendula.ui.week.layoutDay
|
import de.jeanlucmakiola.calendula.ui.week.layoutDay
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
@@ -88,9 +89,7 @@ class DayViewModel @Inject constructor(
|
|||||||
calendars: List<CalendarSource>,
|
calendars: List<CalendarSource>,
|
||||||
instances: List<EventInstance>,
|
instances: List<EventInstance>,
|
||||||
): DayUiState {
|
): DayUiState {
|
||||||
if (calendars.isEmpty()) {
|
calendarListFailure(calendars)?.let { return DayUiState.Failure(it) }
|
||||||
return DayUiState.Failure(FailureReason.NoCalendarsConfigured)
|
|
||||||
}
|
|
||||||
val days = listOf(day)
|
val days = listOf(day)
|
||||||
val allDay = instances.filter { it.isAllDay }
|
val allDay = instances.filter { it.isAllDay }
|
||||||
val timed = instances.filterNot { it.isAllDay }
|
val timed = instances.filterNot { it.isAllDay }
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ private fun FilterLoading(modifier: Modifier = Modifier) {
|
|||||||
private fun FilterMessage(reason: FailureReason, modifier: Modifier = Modifier) {
|
private fun FilterMessage(reason: FailureReason, modifier: Modifier = Modifier) {
|
||||||
val msg = when (reason) {
|
val msg = when (reason) {
|
||||||
FailureReason.NoCalendarsConfigured -> R.string.state_failure_no_calendars
|
FailureReason.NoCalendarsConfigured -> R.string.state_failure_no_calendars
|
||||||
|
FailureReason.AllCalendarsHidden -> R.string.state_failure_all_hidden
|
||||||
FailureReason.PermissionRevoked -> R.string.state_failure_permission
|
FailureReason.PermissionRevoked -> R.string.state_failure_permission
|
||||||
else -> R.string.state_failure_provider
|
else -> R.string.state_failure_provider
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import de.jeanlucmakiola.calendula.data.di.IoDispatcher
|
|||||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs
|
import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs
|
||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.common.groupByAccount
|
import de.jeanlucmakiola.calendula.ui.common.groupByAccount
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -35,8 +36,9 @@ class FilterViewModel @Inject constructor(
|
|||||||
// Calendars switched off device-wide don't belong in the drawer's
|
// Calendars switched off device-wide don't belong in the drawer's
|
||||||
// hide/show list; they live only in Settings → Calendars.
|
// hide/show list; they live only in Settings → Calendars.
|
||||||
val enabled = calendars.filter { it.isVisibleInSystem }
|
val enabled = calendars.filter { it.isVisibleInSystem }
|
||||||
if (enabled.isEmpty()) {
|
val failure = calendarListFailure(calendars)
|
||||||
FilterUiState.Failure(FailureReason.NoCalendarsConfigured)
|
if (failure != null) {
|
||||||
|
FilterUiState.Failure(failure)
|
||||||
} else {
|
} else {
|
||||||
FilterUiState.Success(groupCalendarsForFilter(enabled, hidden))
|
FilterUiState.Success(groupCalendarsForFilter(enabled, hidden))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.prefs.firstDayOfWeek
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
import de.jeanlucmakiola.calendula.domain.isDeclined
|
||||||
import de.jeanlucmakiola.calendula.ui.week.coversDay
|
import de.jeanlucmakiola.calendula.ui.week.coversDay
|
||||||
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
|
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
|
||||||
@@ -208,9 +209,7 @@ class MonthViewModel @Inject constructor(
|
|||||||
calendars: List<CalendarSource>,
|
calendars: List<CalendarSource>,
|
||||||
instances: List<EventInstance>,
|
instances: List<EventInstance>,
|
||||||
): ContinuousMonthUiState {
|
): ContinuousMonthUiState {
|
||||||
if (calendars.isEmpty()) {
|
calendarListFailure(calendars)?.let { return ContinuousMonthUiState.Failure(it) }
|
||||||
return ContinuousMonthUiState.Failure(FailureReason.NoCalendarsConfigured)
|
|
||||||
}
|
|
||||||
// Bucketed once for the whole window: a row then takes the handful of
|
// Bucketed once for the whole window: a row then takes the handful of
|
||||||
// events on its seven days instead of re-scanning a year of them.
|
// events on its seven days instead of re-scanning a year of them.
|
||||||
val byDay = DayIndex(instances, zone)
|
val byDay = DayIndex(instances, zone)
|
||||||
@@ -314,9 +313,7 @@ class MonthViewModel @Inject constructor(
|
|||||||
calendars: List<CalendarSource>,
|
calendars: List<CalendarSource>,
|
||||||
instances: List<EventInstance>,
|
instances: List<EventInstance>,
|
||||||
): MonthUiState {
|
): MonthUiState {
|
||||||
if (calendars.isEmpty()) {
|
calendarListFailure(calendars)?.let { return MonthUiState.Failure(it) }
|
||||||
return MonthUiState.Failure(FailureReason.NoCalendarsConfigured)
|
|
||||||
}
|
|
||||||
val weeks = layoutMonthWeeks(ym, weekStart, instances, zone)
|
val weeks = layoutMonthWeeks(ym, weekStart, instances, zone)
|
||||||
return MonthUiState.Success(
|
return MonthUiState.Success(
|
||||||
month = ym,
|
month = ym,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
|||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
import de.jeanlucmakiola.calendula.domain.FailureReason
|
import de.jeanlucmakiola.calendula.domain.FailureReason
|
||||||
|
import de.jeanlucmakiola.calendula.domain.calendarListFailure
|
||||||
import de.jeanlucmakiola.calendula.domain.isDeclined
|
import de.jeanlucmakiola.calendula.domain.isDeclined
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -126,9 +127,7 @@ class WeekViewModel @Inject constructor(
|
|||||||
calendars: List<CalendarSource>,
|
calendars: List<CalendarSource>,
|
||||||
instances: List<EventInstance>,
|
instances: List<EventInstance>,
|
||||||
): WeekUiState {
|
): WeekUiState {
|
||||||
if (calendars.isEmpty()) {
|
calendarListFailure(calendars)?.let { return WeekUiState.Failure(it) }
|
||||||
return WeekUiState.Failure(FailureReason.NoCalendarsConfigured)
|
|
||||||
}
|
|
||||||
val days = (0 until 7).map { start.plus(it, DateTimeUnit.DAY) }
|
val days = (0 until 7).map { start.plus(it, DateTimeUnit.DAY) }
|
||||||
val allDay = instances.filter { it.isAllDay }
|
val allDay = instances.filter { it.isAllDay }
|
||||||
val timed = instances.filterNot { it.isAllDay }
|
val timed = instances.filterNot { it.isAllDay }
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
<string name="state_failure_permission_action">Grant access</string>
|
<string name="state_failure_permission_action">Grant access</string>
|
||||||
<string name="state_failure_no_calendars">No calendars configured.</string>
|
<string name="state_failure_no_calendars">No calendars configured.</string>
|
||||||
<string name="state_failure_no_calendars_action">Open system calendar settings</string>
|
<string name="state_failure_no_calendars_action">Open system calendar settings</string>
|
||||||
|
<string name="state_failure_all_hidden">All your calendars are switched off.</string>
|
||||||
|
<string name="state_failure_all_hidden_action">Manage calendars</string>
|
||||||
<string name="state_failure_provider">Could not read the calendar.</string>
|
<string name="state_failure_provider">Could not read the calendar.</string>
|
||||||
|
|
||||||
<!-- Long-press a field to copy it (#195) -->
|
<!-- Long-press a field to copy it (#195) -->
|
||||||
|
|||||||
@@ -121,4 +121,24 @@ class CalendarVisibilityPlanTest {
|
|||||||
),
|
),
|
||||||
).isTrue()
|
).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no calendars on the device is its own failure`() {
|
||||||
|
assertThat(calendarListFailure(emptyList()))
|
||||||
|
.isEqualTo(FailureReason.NoCalendarsConfigured)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `calendars that are all switched off read as hidden, not missing`() {
|
||||||
|
// The remedy differs: one needs a calendar made, the other needs one
|
||||||
|
// switched back on (#239).
|
||||||
|
assertThat(calendarListFailure(listOf(cal(1L, visible = false), cal(2L, visible = false))))
|
||||||
|
.isEqualTo(FailureReason.AllCalendarsHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `one visible calendar is enough to show something`() {
|
||||||
|
assertThat(calendarListFailure(listOf(cal(1L, visible = false), cal(2L, visible = true))))
|
||||||
|
.isNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,12 @@ class ModelsTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `FailureReason enum has all five variants`() {
|
fun `FailureReason enum has all six variants`() {
|
||||||
assertThat(FailureReason.values().toSet()).isEqualTo(
|
assertThat(FailureReason.values().toSet()).isEqualTo(
|
||||||
setOf(
|
setOf(
|
||||||
FailureReason.PermissionRevoked,
|
FailureReason.PermissionRevoked,
|
||||||
FailureReason.NoCalendarsConfigured,
|
FailureReason.NoCalendarsConfigured,
|
||||||
|
FailureReason.AllCalendarsHidden,
|
||||||
FailureReason.ProviderUnavailable,
|
FailureReason.ProviderUnavailable,
|
||||||
FailureReason.EventNotFound,
|
FailureReason.EventNotFound,
|
||||||
FailureReason.Unknown,
|
FailureReason.Unknown,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
New
|
||||||
|
• Long-press an event's title, location or description to copy.
|
||||||
|
• Settings → About lists the open-source projects we ship.
|
||||||
|
|
||||||
|
Changed
|
||||||
|
• Week & day: each hour sits in its own cell, not a line.
|
||||||
|
• Unanswered invitations are drawn as outlines.
|
||||||
|
|
||||||
|
Fixed
|
||||||
|
• "No calendars configured" now really opens the system calendar setup.
|
||||||
|
• When every calendar is switched off, the app says so instead of a blank month.
|
||||||
Reference in New Issue
Block a user