Compare commits
24 Commits
release/v2
...
69460bc35a
| Author | SHA1 | Date | |
|---|---|---|---|
| 69460bc35a | |||
|
|
bf10deaf5c | ||
|
|
b9e861af62 | ||
|
|
2eadda937b | ||
|
|
71b90fa184 | ||
|
|
4e9cf88bd4 | ||
|
|
b999202629 | ||
|
|
6f0e13a4a2 | ||
|
|
9ef7b33800 | ||
|
|
d04b431f2c | ||
|
|
dbeeadff80 | ||
|
|
f4a169ef5f | ||
|
|
604099b3f2 | ||
|
|
a3e6541512 | ||
| fb2a40114a | |||
| e1f79df67a | |||
| e258a4a7f2 | |||
| 3f27a47d1e | |||
| feb87a1e73 | |||
| 28e464a616 | |||
| 93df6d6d62 | |||
| 49ddb9437b | |||
| e1cf00999d | |||
| 2d0d707a7d |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -5,6 +5,27 @@ 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.12.0] — 2026-06-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- See past events your way. Two new settings change how events that have already
|
||||||
|
ended are shown. **Past events** (Settings → Appearance → Agenda) lets the
|
||||||
|
agenda — both in the app and the home-screen "Upcoming" widget — keep them as
|
||||||
|
usual, dim them, or hide them from the list entirely. **Dim completed events**
|
||||||
|
(Settings → Appearance) separately fades finished events in the month and week
|
||||||
|
views. Both are off by default, an event only counts as finished once it has
|
||||||
|
actually ended (events still in progress are never dimmed), and the lists
|
||||||
|
update on their own as the day goes on. Thanks to @ptab for the suggestion
|
||||||
|
([#12]).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Readable titles on overlapping events. In the week and day views, events that
|
||||||
|
overlap split a day into slim columns where the title used to be clipped to a
|
||||||
|
character or two. The title now wraps across as many lines as the block can
|
||||||
|
fit, and the time is hidden on those narrow blocks so the name can fill the
|
||||||
|
space — so you can tell events apart without opening each one. Thanks to @ptab
|
||||||
|
for the suggestion ([#13]).
|
||||||
|
|
||||||
## [2.11.2] — 2026-06-28
|
## [2.11.2] — 2026-06-28
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -706,3 +727,5 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#8]: https://codeberg.org/jlmakiola/calendula/issues/8
|
[#8]: https://codeberg.org/jlmakiola/calendula/issues/8
|
||||||
[#9]: https://codeberg.org/jlmakiola/calendula/issues/9
|
[#9]: https://codeberg.org/jlmakiola/calendula/issues/9
|
||||||
[#10]: https://codeberg.org/jlmakiola/calendula/issues/10
|
[#10]: https://codeberg.org/jlmakiola/calendula/issues/10
|
||||||
|
[#12]: https://codeberg.org/jlmakiola/calendula/issues/12
|
||||||
|
[#13]: https://codeberg.org/jlmakiola/calendula/issues/13
|
||||||
|
|||||||
@@ -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 = 21102
|
versionCode = 21200
|
||||||
versionName = "2.11.2"
|
versionName = "2.12.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,19 @@ sealed interface WeekStartPref {
|
|||||||
*/
|
*/
|
||||||
enum class TimeFormatPref { AUTO, TWELVE_HOUR, TWENTY_FOUR_HOUR }
|
enum class TimeFormatPref { AUTO, TWELVE_HOUR, TWENTY_FOUR_HOUR }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the Agenda screen treats events that have already finished today: [SHOW]
|
||||||
|
* leaves them as-is (the historical behaviour), [DIM] fades them, [HIDE] drops
|
||||||
|
* them from the list entirely.
|
||||||
|
*/
|
||||||
|
enum class PastEventDisplay { SHOW, DIM, HIDE }
|
||||||
|
|
||||||
|
/** Parse a stored [PastEventDisplay.name]; unknown/null falls back to [default]. */
|
||||||
|
fun parsePastEventDisplay(
|
||||||
|
stored: String?,
|
||||||
|
default: PastEventDisplay = PastEventDisplay.SHOW,
|
||||||
|
): PastEventDisplay = PastEventDisplay.entries.firstOrNull { it.name == stored } ?: default
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve to a concrete 24-hour flag. AUTO defers to [systemIs24Hour] (the
|
* Resolve to a concrete 24-hour flag. AUTO defers to [systemIs24Hour] (the
|
||||||
* device's `DateFormat.is24HourFormat` value).
|
* device's `DateFormat.is24HourFormat` value).
|
||||||
@@ -116,6 +129,31 @@ class SettingsPrefs @Inject constructor(
|
|||||||
store.edit { it[SHOW_HOUR_LINES_KEY] = enabled }
|
store.edit { it[SHOW_HOUR_LINES_KEY] = enabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the Agenda screen treats events that already ended today. Defaults to
|
||||||
|
* [PastEventDisplay.SHOW] — the historical behaviour; users opt into dimming
|
||||||
|
* or hiding.
|
||||||
|
*/
|
||||||
|
val pastEventDisplay: Flow<PastEventDisplay> = store.data.map { prefs ->
|
||||||
|
prefs[PAST_EVENT_DISPLAY_KEY].toEnum(PastEventDisplay.SHOW)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setPastEventDisplay(mode: PastEventDisplay) {
|
||||||
|
store.edit { it[PAST_EVENT_DISPLAY_KEY] = mode.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the month/week grids fade events that have already finished.
|
||||||
|
* Defaults to OFF — independent of the Agenda's [pastEventDisplay].
|
||||||
|
*/
|
||||||
|
val dimCompletedEvents: Flow<Boolean> = store.data.map { prefs ->
|
||||||
|
prefs[DIM_COMPLETED_EVENTS_KEY] ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setDimCompletedEvents(enabled: Boolean) {
|
||||||
|
store.edit { it[DIM_COMPLETED_EVENTS_KEY] = enabled }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How far ahead the in-app Agenda screen shows events (v2.11). Defaults to
|
* How far ahead the in-app Agenda screen shows events (v2.11). Defaults to
|
||||||
* [AgendaRange.Month] — a month of upcoming events. Independent of the
|
* [AgendaRange.Month] — a month of upcoming events. Independent of the
|
||||||
@@ -404,6 +442,8 @@ class SettingsPrefs @Inject constructor(
|
|||||||
internal val AGENDA_SHOW_RANGE_BAR_KEY = booleanPreferencesKey("agenda_show_range_bar")
|
internal val AGENDA_SHOW_RANGE_BAR_KEY = booleanPreferencesKey("agenda_show_range_bar")
|
||||||
internal val TIME_FORMAT_KEY = stringPreferencesKey("time_format")
|
internal val TIME_FORMAT_KEY = stringPreferencesKey("time_format")
|
||||||
internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines")
|
internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines")
|
||||||
|
internal val PAST_EVENT_DISPLAY_KEY = stringPreferencesKey("agenda_past_event_display")
|
||||||
|
internal val DIM_COMPLETED_EVENTS_KEY = booleanPreferencesKey("dim_completed_events")
|
||||||
internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")
|
internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")
|
||||||
internal val FORM_FIELDS_KEY = stringPreferencesKey("event_form_default_fields")
|
internal val FORM_FIELDS_KEY = stringPreferencesKey("event_form_default_fields")
|
||||||
internal val AUTOFOCUS_EVENT_TITLE_KEY = booleanPreferencesKey("autofocus_event_title")
|
internal val AUTOFOCUS_EVENT_TITLE_KEY = booleanPreferencesKey("autofocus_event_title")
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ data class EventInstance(
|
|||||||
val location: String?,
|
val location: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this event has finished relative to [now] — its end is at or before
|
||||||
|
* the current instant. An in-progress event (already started but not yet ended)
|
||||||
|
* is *not* considered ended. All-day events end at the exclusive next-midnight,
|
||||||
|
* so they only count as ended once their day is fully over.
|
||||||
|
*/
|
||||||
|
fun EventInstance.hasEnded(now: Instant): Boolean = end <= now
|
||||||
|
|
||||||
data class EventDetail(
|
data class EventDetail(
|
||||||
val instance: EventInstance,
|
val instance: EventInstance,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -53,7 +54,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||||
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
||||||
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarAnimateItem
|
import de.jeanlucmakiola.calendula.ui.common.calendarAnimateItem
|
||||||
@@ -61,12 +64,14 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
import de.jeanlucmakiola.calendula.ui.common.GroupedRow
|
||||||
import de.jeanlucmakiola.calendula.ui.common.Position
|
import de.jeanlucmakiola.calendula.ui.common.Position
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.next
|
import de.jeanlucmakiola.calendula.ui.common.next
|
||||||
import de.jeanlucmakiola.calendula.ui.common.pastelize
|
import de.jeanlucmakiola.calendula.ui.common.pastelize
|
||||||
import de.jeanlucmakiola.calendula.ui.common.positionOf
|
import de.jeanlucmakiola.calendula.ui.common.positionOf
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||||
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
import de.jeanlucmakiola.calendula.ui.common.currentLocale
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
||||||
@@ -96,6 +101,7 @@ fun AgendaScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val anchor by viewModel.anchor.collectAsStateWithLifecycle()
|
val anchor by viewModel.anchor.collectAsStateWithLifecycle()
|
||||||
|
val pastDisplay by viewModel.pastEventDisplay.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
@@ -178,6 +184,7 @@ fun AgendaScreen(
|
|||||||
}
|
}
|
||||||
AgendaContent(
|
AgendaContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
pastDisplay = pastDisplay,
|
||||||
onRetry = viewModel::goToToday,
|
onRetry = viewModel::goToToday,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -275,6 +282,7 @@ private fun AgendaRangeBanner(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AgendaContent(
|
private fun AgendaContent(
|
||||||
state: AgendaUiState,
|
state: AgendaUiState,
|
||||||
|
pastDisplay: PastEventDisplay,
|
||||||
onRetry: () -> Unit,
|
onRetry: () -> Unit,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -284,19 +292,42 @@ private fun AgendaContent(
|
|||||||
is AgendaUiState.Failure -> Box(modifier) {
|
is AgendaUiState.Failure -> Box(modifier) {
|
||||||
CalendarFailure(reason = state.reason, onRetry = onRetry)
|
CalendarFailure(reason = state.reason, onRetry = onRetry)
|
||||||
}
|
}
|
||||||
is AgendaUiState.Success ->
|
is AgendaUiState.Success -> {
|
||||||
if (state.days.isEmpty()) {
|
val now by rememberCurrentMinute()
|
||||||
|
// Hiding drops finished events — and any day they leave empty; dimming
|
||||||
|
// keeps them but fades the row. Recomputed each minute so events fall
|
||||||
|
// away (or fade) as they end while the screen stays open.
|
||||||
|
val days = if (pastDisplay == PastEventDisplay.HIDE) {
|
||||||
|
state.days.mapNotNull { day ->
|
||||||
|
val remaining = day.events.filterNot { it.hasEnded(now) }
|
||||||
|
if (remaining.isEmpty()) null else day.copy(events = remaining)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.days
|
||||||
|
}
|
||||||
|
if (days.isEmpty()) {
|
||||||
AgendaEmpty(modifier)
|
AgendaEmpty(modifier)
|
||||||
} else {
|
} else {
|
||||||
AgendaList(state = state, onEventClick = onEventClick, modifier = modifier)
|
AgendaList(
|
||||||
|
days = days,
|
||||||
|
today = state.today,
|
||||||
|
dimPast = pastDisplay == PastEventDisplay.DIM,
|
||||||
|
now = now,
|
||||||
|
onEventClick = onEventClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun AgendaList(
|
private fun AgendaList(
|
||||||
state: AgendaUiState.Success,
|
days: List<AgendaDay>,
|
||||||
|
today: LocalDate,
|
||||||
|
dimPast: Boolean,
|
||||||
|
now: Instant,
|
||||||
onEventClick: (EventInstance) -> Unit,
|
onEventClick: (EventInstance) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -305,9 +336,9 @@ private fun AgendaList(
|
|||||||
// Bottom inset clears the FAB stack so the last row stays tappable.
|
// Bottom inset clears the FAB stack so the last row stays tappable.
|
||||||
contentPadding = PaddingValues(top = 8.dp, bottom = 96.dp),
|
contentPadding = PaddingValues(top = 8.dp, bottom = 96.dp),
|
||||||
) {
|
) {
|
||||||
state.days.forEach { day ->
|
days.forEach { day ->
|
||||||
stickyHeader(key = "header-${day.date}") {
|
stickyHeader(key = "header-${day.date}") {
|
||||||
AgendaDayHeader(date = day.date, today = state.today)
|
AgendaDayHeader(date = day.date, today = today)
|
||||||
}
|
}
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = day.events,
|
items = day.events,
|
||||||
@@ -316,6 +347,7 @@ private fun AgendaList(
|
|||||||
AgendaEventRow(
|
AgendaEventRow(
|
||||||
event = event,
|
event = event,
|
||||||
position = positionOf(index, day.events.size),
|
position = positionOf(index, day.events.size),
|
||||||
|
dimmed = dimPast && event.hasEnded(now),
|
||||||
modifier = calendarAnimateItem(),
|
modifier = calendarAnimateItem(),
|
||||||
onClick = { onEventClick(event) },
|
onClick = { onEventClick(event) },
|
||||||
)
|
)
|
||||||
@@ -348,13 +380,14 @@ private fun AgendaDayHeader(date: LocalDate, today: LocalDate) {
|
|||||||
private fun AgendaEventRow(
|
private fun AgendaEventRow(
|
||||||
event: EventInstance,
|
event: EventInstance,
|
||||||
position: Position,
|
position: Position,
|
||||||
|
dimmed: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val dark = isSystemInDarkTheme()
|
val dark = isSystemInDarkTheme()
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
modifier = modifier,
|
modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier,
|
||||||
title = title,
|
title = title,
|
||||||
summary = agendaTimeSummary(event),
|
summary = agendaTimeSummary(event),
|
||||||
position = position,
|
position = position,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
||||||
import de.jeanlucmakiola.calendula.data.di.IoDispatcher
|
import de.jeanlucmakiola.calendula.data.di.IoDispatcher
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
||||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||||
@@ -53,6 +54,18 @@ class AgendaViewModel @Inject constructor(
|
|||||||
private val weekStartDay = settingsPrefs.weekStart
|
private val weekStartDay = settingsPrefs.weekStart
|
||||||
.map { it.resolveFirstDay(Locale.getDefault()) }
|
.map { it.resolveFirstDay(Locale.getDefault()) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How to treat events that already ended today (show / dim / hide). A display
|
||||||
|
* concern only, so it rides alongside the data state rather than re-querying;
|
||||||
|
* the screen combines it with a per-minute "now" to fade or drop past rows.
|
||||||
|
*/
|
||||||
|
val pastEventDisplay: StateFlow<PastEventDisplay> = settingsPrefs.pastEventDisplay
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000L),
|
||||||
|
initialValue = PastEventDisplay.SHOW,
|
||||||
|
)
|
||||||
|
|
||||||
private val zone = TimeZone.currentSystemDefault()
|
private val zone = TimeZone.currentSystemDefault()
|
||||||
|
|
||||||
private val todayDate: LocalDate
|
private val todayDate: LocalDate
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ private val LineThickness = 2.dp
|
|||||||
* the middle of a minute. Drives the "now" line in the day/week grids.
|
* the middle of a minute. Drives the "now" line in the day/week grids.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberCurrentMinute(): State<Instant> {
|
fun rememberCurrentMinute(): State<Instant> {
|
||||||
val instant = remember { mutableStateOf(Clock.System.now()) }
|
val instant = remember { mutableStateOf(Clock.System.now()) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cut-off instant before which a calendar event counts as "completed" and is
|
||||||
|
* drawn dimmed in the month/week grids — i.e. the current wall-clock minute when
|
||||||
|
* the "dim completed events" setting is on, or `null` when it is off (nothing
|
||||||
|
* dims). Provided per grid screen so only the event chips that read it recompose
|
||||||
|
* as the minute ticks, mirroring [LocalShowHourLines] / [LocalUse24HourFormat].
|
||||||
|
*/
|
||||||
|
val LocalDimCutoff = compositionLocalOf<Instant?> { null }
|
||||||
|
|
||||||
|
/** Opacity applied to a completed/past event chip when it is dimmed. */
|
||||||
|
const val EventDimAlpha = 0.4f
|
||||||
@@ -36,6 +36,8 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -44,6 +46,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -60,10 +63,14 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
@@ -97,6 +104,14 @@ fun MonthScreen(
|
|||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val month by viewModel.month.collectAsStateWithLifecycle()
|
val month by viewModel.month.collectAsStateWithLifecycle()
|
||||||
val weekStart by viewModel.weekStart.collectAsStateWithLifecycle()
|
val weekStart by viewModel.weekStart.collectAsStateWithLifecycle()
|
||||||
|
val dimCompleted by viewModel.dimCompletedEvents.collectAsStateWithLifecycle()
|
||||||
|
// The instant before which an event counts as completed, or null when dimming
|
||||||
|
// is off. derivedStateOf keeps the per-minute "now" from recomposing the
|
||||||
|
// screen while the setting is off (it stays null regardless of the tick).
|
||||||
|
val nowState = rememberCurrentMinute()
|
||||||
|
val dimCutoff by remember(dimCompleted) {
|
||||||
|
derivedStateOf { if (dimCompleted) nowState.value else null }
|
||||||
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
@@ -193,14 +208,16 @@ fun MonthScreen(
|
|||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
WeekdayHeader(weekStart = weekStart)
|
WeekdayHeader(weekStart = weekStart)
|
||||||
MonthContent(
|
CompositionLocalProvider(LocalDimCutoff provides dimCutoff) {
|
||||||
state = state,
|
MonthContent(
|
||||||
slideDir = slideDir,
|
state = state,
|
||||||
onSwipeNext = goNext,
|
slideDir = slideDir,
|
||||||
onSwipePrev = goPrev,
|
onSwipeNext = goNext,
|
||||||
onRetry = jumpToToday,
|
onSwipePrev = goPrev,
|
||||||
onOpenDay = onOpenDay,
|
onRetry = jumpToToday,
|
||||||
)
|
onOpenDay = onOpenDay,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,6 +575,8 @@ private fun MonthBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
|
val dimCutoff = LocalDimCutoff.current
|
||||||
|
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
||||||
val shape = RoundedCornerShape(
|
val shape = RoundedCornerShape(
|
||||||
topStart = if (continuesLeft) 0.dp else 4.dp,
|
topStart = if (continuesLeft) 0.dp else 4.dp,
|
||||||
bottomStart = if (continuesLeft) 0.dp else 4.dp,
|
bottomStart = if (continuesLeft) 0.dp else 4.dp,
|
||||||
@@ -565,7 +584,7 @@ private fun MonthBar(
|
|||||||
bottomEnd = if (continuesRight) 0.dp else 4.dp,
|
bottomEnd = if (continuesRight) 0.dp else 4.dp,
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
.background(pastelize(event.color, dark), shape)
|
.background(pastelize(event.color, dark), shape)
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.semantics { contentDescription = title },
|
.semantics { contentDescription = title },
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ class MonthViewModel @Inject constructor(
|
|||||||
initialValue = DayOfWeek.MONDAY,
|
initialValue = DayOfWeek.MONDAY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** Whether to fade events that have already finished (display concern only). */
|
||||||
|
val dimCompletedEvents: StateFlow<Boolean> = settingsPrefs.dimCompletedEvents
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000L),
|
||||||
|
initialValue = false,
|
||||||
|
)
|
||||||
|
|
||||||
private val todayDate: LocalDate
|
private val todayDate: LocalDate
|
||||||
get() = Clock.System.now().toLocalDateTime(zone).date
|
get() = Clock.System.now().toLocalDateTime(zone).date
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
|
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
|
||||||
@@ -475,6 +476,7 @@ private fun AppearanceScreen(
|
|||||||
var showDefaultView by remember { mutableStateOf(false) }
|
var showDefaultView by remember { mutableStateOf(false) }
|
||||||
var showAgendaScreenRange by remember { mutableStateOf(false) }
|
var showAgendaScreenRange by remember { mutableStateOf(false) }
|
||||||
var showAgendaWidgetRange by remember { mutableStateOf(false) }
|
var showAgendaWidgetRange by remember { mutableStateOf(false) }
|
||||||
|
var showPastEvents by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
CollapsingScaffold(
|
CollapsingScaffold(
|
||||||
title = stringResource(R.string.settings_section_appearance),
|
title = stringResource(R.string.settings_section_appearance),
|
||||||
@@ -533,7 +535,7 @@ private fun AppearanceScreen(
|
|||||||
GroupedRow(
|
GroupedRow(
|
||||||
title = stringResource(R.string.settings_hour_lines),
|
title = stringResource(R.string.settings_hour_lines),
|
||||||
summary = stringResource(R.string.settings_hour_lines_summary),
|
summary = stringResource(R.string.settings_hour_lines_summary),
|
||||||
position = Position.Bottom,
|
position = Position.Middle,
|
||||||
trailing = {
|
trailing = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = state.showHourLines,
|
checked = state.showHourLines,
|
||||||
@@ -542,16 +544,36 @@ private fun AppearanceScreen(
|
|||||||
},
|
},
|
||||||
onClick = { viewModel.setShowHourLines(!state.showHourLines) },
|
onClick = { viewModel.setShowHourLines(!state.showHourLines) },
|
||||||
)
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_dim_completed),
|
||||||
|
summary = stringResource(R.string.settings_dim_completed_summary),
|
||||||
|
position = Position.Bottom,
|
||||||
|
trailing = {
|
||||||
|
Switch(
|
||||||
|
checked = state.dimCompletedEvents,
|
||||||
|
onCheckedChange = viewModel::setDimCompletedEvents,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { viewModel.setDimCompletedEvents(!state.dimCompletedEvents) },
|
||||||
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
// Agenda
|
// Agenda — the only group labelled, so its agenda-specific rows are easy
|
||||||
|
// to pick out among the otherwise unheadered Appearance settings.
|
||||||
|
SectionHeader(stringResource(R.string.settings_agenda_header))
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
title = stringResource(R.string.settings_agenda_range),
|
title = stringResource(R.string.settings_agenda_range),
|
||||||
summary = agendaRangeLabel(state.agendaScreenRange),
|
summary = agendaRangeLabel(state.agendaScreenRange),
|
||||||
position = Position.Top,
|
position = Position.Top,
|
||||||
onClick = { showAgendaScreenRange = true },
|
onClick = { showAgendaScreenRange = true },
|
||||||
)
|
)
|
||||||
|
GroupedRow(
|
||||||
|
title = stringResource(R.string.settings_past_events),
|
||||||
|
summary = pastEventDisplayLabel(state.pastEventDisplay),
|
||||||
|
position = Position.Middle,
|
||||||
|
onClick = { showPastEvents = true },
|
||||||
|
)
|
||||||
GroupedRow(
|
GroupedRow(
|
||||||
title = stringResource(R.string.settings_agenda_widget_range),
|
title = stringResource(R.string.settings_agenda_widget_range),
|
||||||
summary = agendaRangeLabel(state.agendaWidgetRange),
|
summary = agendaRangeLabel(state.agendaWidgetRange),
|
||||||
@@ -620,6 +642,16 @@ private fun AppearanceScreen(
|
|||||||
onDismiss = { showTimeFormat = false },
|
onDismiss = { showTimeFormat = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (showPastEvents) {
|
||||||
|
OptionPicker(
|
||||||
|
title = stringResource(R.string.settings_past_events),
|
||||||
|
options = PastEventDisplay.entries,
|
||||||
|
selected = state.pastEventDisplay,
|
||||||
|
label = { pastEventDisplayLabel(it) },
|
||||||
|
onSelect = viewModel::setPastEventDisplay,
|
||||||
|
onDismiss = { showPastEvents = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
if (showDefaultView) {
|
if (showDefaultView) {
|
||||||
OptionPicker(
|
OptionPicker(
|
||||||
title = stringResource(R.string.settings_default_view),
|
title = stringResource(R.string.settings_default_view),
|
||||||
@@ -1170,6 +1202,26 @@ private fun timeFormatLabel(pref: TimeFormatPref): String = stringResource(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** A small primary-coloured group label, matching the Calendars settings screen. */
|
||||||
|
@Composable
|
||||||
|
private fun SectionHeader(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun pastEventDisplayLabel(mode: PastEventDisplay): String = stringResource(
|
||||||
|
when (mode) {
|
||||||
|
PastEventDisplay.SHOW -> R.string.settings_past_events_show
|
||||||
|
PastEventDisplay.DIM -> R.string.settings_past_events_dim
|
||||||
|
PastEventDisplay.HIDE -> R.string.settings_past_events_hide
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun languageLabel(tag: String?): String =
|
private fun languageLabel(tag: String?): String =
|
||||||
if (tag == null) stringResource(R.string.settings_language_auto) else AppLanguage.displayName(tag)
|
if (tag == null) stringResource(R.string.settings_language_auto) else AppLanguage.displayName(tag)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package de.jeanlucmakiola.calendula.ui.settings
|
package de.jeanlucmakiola.calendula.ui.settings
|
||||||
|
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
||||||
@@ -24,6 +25,10 @@ data class SettingsUiState(
|
|||||||
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
|
val timeFormat: TimeFormatPref = TimeFormatPref.AUTO,
|
||||||
/** Whether the week/day timeline draws an hour separator line (v2.11). */
|
/** Whether the week/day timeline draws an hour separator line (v2.11). */
|
||||||
val showHourLines: Boolean = false,
|
val showHourLines: Boolean = false,
|
||||||
|
/** How the Agenda screen treats events that already ended today. */
|
||||||
|
val pastEventDisplay: PastEventDisplay = PastEventDisplay.SHOW,
|
||||||
|
/** Whether the month/week grids fade events that have already finished. */
|
||||||
|
val dimCompletedEvents: Boolean = false,
|
||||||
/** How far ahead the in-app Agenda screen shows events (v2.11). */
|
/** How far ahead the in-app Agenda screen shows events (v2.11). */
|
||||||
val agendaScreenRange: AgendaRange = AgendaRange.Month,
|
val agendaScreenRange: AgendaRange = AgendaRange.Month,
|
||||||
/** How far ahead the agenda widget shows events (v2.11). */
|
/** How far ahead the agenda widget shows events (v2.11). */
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
|
||||||
@@ -20,6 +21,7 @@ import de.jeanlucmakiola.calendula.domain.EventFormField
|
|||||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
import de.jeanlucmakiola.calendula.ui.agenda.storageValue
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY
|
||||||
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY
|
import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY
|
||||||
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
|
||||||
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
import de.jeanlucmakiola.calendula.widget.month.MonthWidget
|
||||||
@@ -99,6 +101,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
combine(
|
combine(
|
||||||
prefs.agendaShowRangeBar,
|
prefs.agendaShowRangeBar,
|
||||||
prefs.autofocusEventTitle,
|
prefs.autofocusEventTitle,
|
||||||
|
prefs.pastEventDisplay,
|
||||||
|
prefs.dimCompletedEvents,
|
||||||
::MiscSettings,
|
::MiscSettings,
|
||||||
),
|
),
|
||||||
) { base, defaults, overrides, views, misc ->
|
) { base, defaults, overrides, views, misc ->
|
||||||
@@ -110,6 +114,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
showHourLines = views.showHourLines,
|
showHourLines = views.showHourLines,
|
||||||
agendaShowRangeBar = misc.showRangeBar,
|
agendaShowRangeBar = misc.showRangeBar,
|
||||||
autofocusEventTitle = misc.autofocusEventTitle,
|
autofocusEventTitle = misc.autofocusEventTitle,
|
||||||
|
pastEventDisplay = misc.pastEventDisplay,
|
||||||
|
dimCompletedEvents = misc.dimCompletedEvents,
|
||||||
allowColorOnUnsupportedCalendars = defaults.allowColor,
|
allowColorOnUnsupportedCalendars = defaults.allowColor,
|
||||||
defaultReminderMinutes = defaults.defaultReminder,
|
defaultReminderMinutes = defaults.defaultReminder,
|
||||||
defaultAllDayReminderMinutes = defaults.allDayReminder,
|
defaultAllDayReminderMinutes = defaults.allDayReminder,
|
||||||
@@ -156,6 +162,8 @@ class SettingsViewModel @Inject constructor(
|
|||||||
private data class MiscSettings(
|
private data class MiscSettings(
|
||||||
val showRangeBar: Boolean,
|
val showRangeBar: Boolean,
|
||||||
val autofocusEventTitle: Boolean,
|
val autofocusEventTitle: Boolean,
|
||||||
|
val pastEventDisplay: PastEventDisplay,
|
||||||
|
val dimCompletedEvents: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun setThemeMode(mode: ThemeMode) {
|
fun setThemeMode(mode: ThemeMode) {
|
||||||
@@ -211,6 +219,26 @@ class SettingsViewModel @Inject constructor(
|
|||||||
viewModelScope.launch { prefs.setShowHourLines(enabled) }
|
viewModelScope.launch { prefs.setShowHourLines(enabled) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setPastEventDisplay(mode: PastEventDisplay) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
prefs.setPastEventDisplay(mode)
|
||||||
|
// The agenda widget honours this setting too, so push it into each
|
||||||
|
// instance's Glance state and recompose — same reliable-update path as
|
||||||
|
// setAgendaWidgetRange (updateAll alone won't re-run the data preamble).
|
||||||
|
widgetRefreshMutex.withLock {
|
||||||
|
val manager = GlanceAppWidgetManager(appContext)
|
||||||
|
manager.getGlanceIds(AgendaWidget::class.java).forEach { id ->
|
||||||
|
updateAppWidgetState(appContext, id) { it[AGENDA_PAST_DISPLAY_KEY] = mode.name }
|
||||||
|
}
|
||||||
|
AgendaWidget().updateAll(appContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDimCompletedEvents(enabled: Boolean) {
|
||||||
|
viewModelScope.launch { prefs.setDimCompletedEvents(enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
fun setDefaultView(view: CalendarView) {
|
fun setDefaultView(view: CalendarView) {
|
||||||
viewModelScope.launch { prefs.setDefaultView(view) }
|
viewModelScope.launch { prefs.setDefaultView(view) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -55,6 +57,7 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -73,11 +76,15 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill
|
||||||
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition
|
||||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarFadeSpec
|
||||||
@@ -129,6 +136,14 @@ fun WeekScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
val weekStart by viewModel.weekStartDate.collectAsStateWithLifecycle()
|
val weekStart by viewModel.weekStartDate.collectAsStateWithLifecycle()
|
||||||
|
val dimCompleted by viewModel.dimCompletedEvents.collectAsStateWithLifecycle()
|
||||||
|
// The instant before which an event counts as completed, or null when dimming
|
||||||
|
// is off. derivedStateOf keeps the per-minute "now" from recomposing the
|
||||||
|
// screen while the setting is off (it stays null regardless of the tick).
|
||||||
|
val nowState = rememberCurrentMinute()
|
||||||
|
val dimCutoff by remember(dimCompleted) {
|
||||||
|
derivedStateOf { if (dimCompleted) nowState.value else null }
|
||||||
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
@@ -221,19 +236,21 @@ fun WeekScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
WeekContent(
|
CompositionLocalProvider(LocalDimCutoff provides dimCutoff) {
|
||||||
state = state,
|
WeekContent(
|
||||||
slideDir = slideDir,
|
state = state,
|
||||||
topSectionColor = topSectionColor,
|
slideDir = slideDir,
|
||||||
onSwipeNext = goNext,
|
topSectionColor = topSectionColor,
|
||||||
onSwipePrev = goPrev,
|
onSwipeNext = goNext,
|
||||||
onRetry = jumpToToday,
|
onSwipePrev = goPrev,
|
||||||
onEventClick = onEventClick,
|
onRetry = jumpToToday,
|
||||||
onCreateAt = { d, minutes -> onCreateEvent(d, minutes) },
|
onEventClick = onEventClick,
|
||||||
modifier = Modifier
|
onCreateAt = { d, minutes -> onCreateEvent(d, minutes) },
|
||||||
.padding(innerPadding)
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.padding(innerPadding)
|
||||||
)
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -550,8 +567,10 @@ private fun AllDayBar(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||||
|
val dimCutoff = LocalDimCutoff.current
|
||||||
|
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
.background(pastelize(event.color, dark), RoundedCornerShape(4.dp))
|
.background(pastelize(event.color, dark), RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 6.dp, vertical = 2.dp)
|
.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
@@ -692,6 +711,7 @@ private fun DayColumnCard(
|
|||||||
EventBlock(
|
EventBlock(
|
||||||
block = block,
|
block = block,
|
||||||
dark = dark,
|
dark = dark,
|
||||||
|
height = height,
|
||||||
onClick = { onEventClick(block.event) },
|
onClick = { onEventClick(block.event) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.offset(x = laneWidth * block.lane, y = top)
|
.offset(x = laneWidth * block.lane, y = top)
|
||||||
@@ -712,6 +732,7 @@ private fun DayColumnCard(
|
|||||||
private fun EventBlock(
|
private fun EventBlock(
|
||||||
block: TimedBlock,
|
block: TimedBlock,
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
|
height: Dp,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -720,9 +741,26 @@ private fun EventBlock(
|
|||||||
val locale = currentLocale()
|
val locale = currentLocale()
|
||||||
val timeLabel = "${minToHm(block.startMin, use24Hour, locale)}–" +
|
val timeLabel = "${minToHm(block.startMin, use24Hour, locale)}–" +
|
||||||
minToHm(block.endMin, use24Hour, locale)
|
minToHm(block.endMin, use24Hour, locale)
|
||||||
val showTime = block.endMin - block.startMin >= 45
|
// Only full-width (non-overlapping) blocks that are tall enough show the time.
|
||||||
|
// On narrow overlapping columns we drop it so the title can wrap to fill the
|
||||||
|
// whole block, mirroring Google Calendar.
|
||||||
|
val showTime = block.endMin - block.startMin >= 45 && block.laneCount == 1
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val titleLineHeight = with(density) {
|
||||||
|
MaterialTheme.typography.labelMedium.lineHeight.toDp()
|
||||||
|
}
|
||||||
|
val timeLineHeight = with(density) {
|
||||||
|
MaterialTheme.typography.labelSmall.lineHeight.toDp()
|
||||||
|
}
|
||||||
|
// Wrap the title across as many lines as the block can fit (minus the 2.dp
|
||||||
|
// top/bottom padding and the reserved time line) instead of clipping it to a
|
||||||
|
// single character on slim, overlapping blocks.
|
||||||
|
val contentHeight = height - 4.dp - if (showTime) timeLineHeight else 0.dp
|
||||||
|
val titleMaxLines = (contentHeight / titleLineHeight).toInt().coerceAtLeast(1)
|
||||||
|
val dimCutoff = LocalDimCutoff.current
|
||||||
|
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
.background(pastelize(block.event.color, dark), RoundedCornerShape(4.dp))
|
.background(pastelize(block.event.color, dark), RoundedCornerShape(4.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||||
@@ -732,18 +770,15 @@ private fun EventBlock(
|
|||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
maxLines = if (showTime) 1 else 2,
|
maxLines = titleMaxLines,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.85f),
|
color = Color.Black.copy(alpha = 0.85f),
|
||||||
)
|
)
|
||||||
if (showTime) {
|
if (showTime) {
|
||||||
// Narrow columns can't fit "13:00–14:00" on one line, so let it
|
|
||||||
// wrap to a second line (after the dash) instead of clipping the
|
|
||||||
// end time.
|
|
||||||
Text(
|
Text(
|
||||||
text = timeLabel,
|
text = timeLabel,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
maxLines = 2,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
color = Color.Black.copy(alpha = 0.6f),
|
color = Color.Black.copy(alpha = 0.6f),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ class WeekViewModel @Inject constructor(
|
|||||||
private val zone = TimeZone.currentSystemDefault()
|
private val zone = TimeZone.currentSystemDefault()
|
||||||
private val locale: Locale = Locale.getDefault()
|
private val locale: Locale = Locale.getDefault()
|
||||||
|
|
||||||
|
/** Whether to fade events that have already finished (display concern only). */
|
||||||
|
val dimCompletedEvents: StateFlow<Boolean> = settingsPrefs.dimCompletedEvents
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5_000L),
|
||||||
|
initialValue = false,
|
||||||
|
)
|
||||||
|
|
||||||
private val todayDate: LocalDate
|
private val todayDate: LocalDate
|
||||||
get() = Clock.System.now().toLocalDateTime(zone).date
|
get() = Clock.System.now().toLocalDateTime(zone).date
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
@@ -24,6 +25,7 @@ import kotlinx.datetime.toInstant
|
|||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How far either side of today the month widget pre-loads. The displayed month
|
* How far either side of today the month widget pre-loads. The displayed month
|
||||||
@@ -52,6 +54,8 @@ sealed interface AgendaWidgetData {
|
|||||||
* range is sliced from this in the composition (read reactively from
|
* range is sliced from this in the composition (read reactively from
|
||||||
* Glance state), so a range change is pure recomposition rather than a
|
* Glance state), so a range change is pure recomposition rather than a
|
||||||
* flaky widget session restart — the same approach the month widget uses.
|
* flaky widget session restart — the same approach the month widget uses.
|
||||||
|
* Kept unfiltered: hiding/dimming finished events also happens reactively
|
||||||
|
* in the composition, against [now], for the same reliable-update reason.
|
||||||
*/
|
*/
|
||||||
val days: List<AgendaDay>,
|
val days: List<AgendaDay>,
|
||||||
/** Resolved clock convention for event time labels (the time-format pref). */
|
/** Resolved clock convention for event time labels (the time-format pref). */
|
||||||
@@ -60,6 +64,10 @@ sealed interface AgendaWidgetData {
|
|||||||
val weekStart: DayOfWeek,
|
val weekStart: DayOfWeek,
|
||||||
/** Saved range pref — the fallback when an instance has no Glance state yet. */
|
/** Saved range pref — the fallback when an instance has no Glance state yet. */
|
||||||
val savedRange: AgendaRange,
|
val savedRange: AgendaRange,
|
||||||
|
/** Saved past-event display pref — the fallback before Glance state is set. */
|
||||||
|
val savedPastDisplay: PastEventDisplay,
|
||||||
|
/** Snapshot instant the data was read at, for "has this event ended?" tests. */
|
||||||
|
val now: Instant,
|
||||||
) : AgendaWidgetData
|
) : AgendaWidgetData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +116,7 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData {
|
|||||||
val ep = widgetEntryPoint()
|
val ep = widgetEntryPoint()
|
||||||
val prefs = ep.settingsPrefs()
|
val prefs = ep.settingsPrefs()
|
||||||
val savedRange = prefs.agendaWidgetRange.first()
|
val savedRange = prefs.agendaWidgetRange.first()
|
||||||
|
val savedPastDisplay = prefs.pastEventDisplay.first()
|
||||||
val weekStart = prefs.weekStart.first().resolveFirstDay(Locale.getDefault())
|
val weekStart = prefs.weekStart.first().resolveFirstDay(Locale.getDefault())
|
||||||
// Load the widest selectable window once; the displayed range is sliced in
|
// Load the widest selectable window once; the displayed range is sliced in
|
||||||
// the composition from Glance state, so changing the range is a plain
|
// the composition from Glance state, so changing the range is a plain
|
||||||
@@ -122,6 +131,8 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData {
|
|||||||
is24Hour = is24Hour,
|
is24Hour = is24Hour,
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
savedRange = savedRange,
|
savedRange = savedRange,
|
||||||
|
savedPastDisplay = savedPastDisplay,
|
||||||
|
now = Clock.System.now(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,15 @@ import androidx.glance.text.Text
|
|||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import de.jeanlucmakiola.calendula.MainActivity
|
import de.jeanlucmakiola.calendula.MainActivity
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
|
||||||
|
import de.jeanlucmakiola.calendula.data.prefs.parsePastEventDisplay
|
||||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.dayCount
|
import de.jeanlucmakiola.calendula.ui.agenda.dayCount
|
||||||
import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange
|
import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange
|
||||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
|
||||||
import de.jeanlucmakiola.calendula.ui.common.pastelize
|
import de.jeanlucmakiola.calendula.ui.common.pastelize
|
||||||
import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
|
import de.jeanlucmakiola.calendula.widget.AgendaWidgetData
|
||||||
@@ -79,6 +83,14 @@ import java.util.Locale
|
|||||||
*/
|
*/
|
||||||
internal val AGENDA_RANGE_KEY = stringPreferencesKey("agenda_range")
|
internal val AGENDA_RANGE_KEY = stringPreferencesKey("agenda_range")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-instance Glance state key holding the past-event display mode (as
|
||||||
|
* [PastEventDisplay.name]). Read reactively in the composition for the same
|
||||||
|
* reason as [AGENDA_RANGE_KEY] — so toggling the setting reflects on the live
|
||||||
|
* widget without depending on the `provideGlance` preamble re-running.
|
||||||
|
*/
|
||||||
|
internal val AGENDA_PAST_DISPLAY_KEY = stringPreferencesKey("agenda_past_display")
|
||||||
|
|
||||||
class AgendaWidget : GlanceAppWidget() {
|
class AgendaWidget : GlanceAppWidget() {
|
||||||
|
|
||||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||||
@@ -125,11 +137,26 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
|
|||||||
// to the saved pref for a freshly placed widget), then the wide
|
// to the saved pref for a freshly placed widget), then the wide
|
||||||
// pre-loaded window is sliced to it — pure recomposition.
|
// pre-loaded window is sliced to it — pure recomposition.
|
||||||
val range = parseAgendaRange(currentState(AGENDA_RANGE_KEY), data.savedRange)
|
val range = parseAgendaRange(currentState(AGENDA_RANGE_KEY), data.savedRange)
|
||||||
|
val pastDisplay =
|
||||||
|
parsePastEventDisplay(currentState(AGENDA_PAST_DISPLAY_KEY), data.savedPastDisplay)
|
||||||
val rangeEnd = data.today.plus(
|
val rangeEnd = data.today.plus(
|
||||||
range.dayCount(data.today, data.weekStart) - 1,
|
range.dayCount(data.today, data.weekStart) - 1,
|
||||||
DateTimeUnit.DAY,
|
DateTimeUnit.DAY,
|
||||||
)
|
)
|
||||||
val visibleDays = data.days.filter { it.date <= rangeEnd }
|
// Slice to the chosen range, then drop finished events (and any day
|
||||||
|
// they leave empty) when the mode is Hide — so "Upcoming" really is.
|
||||||
|
val visibleDays = data.days
|
||||||
|
.filter { it.date <= rangeEnd }
|
||||||
|
.let { days ->
|
||||||
|
if (pastDisplay == PastEventDisplay.HIDE) {
|
||||||
|
days.mapNotNull { day ->
|
||||||
|
val remaining = day.events.filterNot { it.hasEnded(data.now) }
|
||||||
|
if (remaining.isEmpty()) null else day.copy(events = remaining)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
days
|
||||||
|
}
|
||||||
|
}
|
||||||
if (visibleDays.isEmpty()) {
|
if (visibleDays.isEmpty()) {
|
||||||
WidgetMessage(R.string.agenda_empty_title)
|
WidgetMessage(R.string.agenda_empty_title)
|
||||||
} else {
|
} else {
|
||||||
@@ -143,7 +170,13 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) {
|
|||||||
items(rows.size) { index ->
|
items(rows.size) { index ->
|
||||||
when (val row = rows[index]) {
|
when (val row = rows[index]) {
|
||||||
is AgendaRow.Header -> DayHeaderRow(row.date, row.today)
|
is AgendaRow.Header -> DayHeaderRow(row.date, row.today)
|
||||||
is AgendaRow.Event -> EventRow(row.event, dark, data.is24Hour)
|
is AgendaRow.Event -> EventRow(
|
||||||
|
event = row.event,
|
||||||
|
dark = dark,
|
||||||
|
is24Hour = data.is24Hour,
|
||||||
|
dimmed = pastDisplay == PastEventDisplay.DIM &&
|
||||||
|
row.event.hasEnded(data.now),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,9 +257,15 @@ private fun DayHeaderRow(date: LocalDate, today: LocalDate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean) {
|
private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean, dimmed: Boolean) {
|
||||||
val context = androidx.glance.LocalContext.current
|
val context = androidx.glance.LocalContext.current
|
||||||
val title = event.title.ifBlank { context.getString(R.string.event_untitled) }
|
val title = event.title.ifBlank { context.getString(R.string.event_untitled) }
|
||||||
|
// Glance has no generic alpha modifier, so dim by fading the colour stripe and
|
||||||
|
// dropping both text lines to the lower-emphasis on-surface-variant tone.
|
||||||
|
val stripeColor = pastelize(event.color, dark).let {
|
||||||
|
if (dimmed) it.copy(alpha = EventDimAlpha) else it
|
||||||
|
}
|
||||||
|
val titleColor = if (dimmed) GlanceTheme.colors.onSurfaceVariant else GlanceTheme.colors.onSurface
|
||||||
Row(
|
Row(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -249,14 +288,14 @@ private fun EventRow(event: EventInstance, dark: Boolean, is24Hour: Boolean) {
|
|||||||
.width(5.dp)
|
.width(5.dp)
|
||||||
.height(36.dp)
|
.height(36.dp)
|
||||||
.cornerRadius(3.dp)
|
.cornerRadius(3.dp)
|
||||||
.background(pastelize(event.color, dark)),
|
.background(stripeColor),
|
||||||
) {}
|
) {}
|
||||||
Spacer(GlanceModifier.width(10.dp))
|
Spacer(GlanceModifier.width(10.dp))
|
||||||
Column(modifier = GlanceModifier.defaultWeight()) {
|
Column(modifier = GlanceModifier.defaultWeight()) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
style = TextStyle(color = GlanceTheme.colors.onSurface, fontSize = 14.sp),
|
style = TextStyle(color = titleColor, fontSize = 14.sp),
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = eventTimeSummary(context, event, is24Hour),
|
text = eventTimeSummary(context, event, is24Hour),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Calendula</string>
|
<string name="app_name">Calendula</string>
|
||||||
<string name="app_tagline">Ein moderner Kalender.</string>
|
<string name="app_tagline">Ein moderner Kalender.</string>
|
||||||
|
|
||||||
<string name="state_loading">Lädt…</string>
|
<string name="state_loading">Lädt…</string>
|
||||||
<string name="state_retry">Erneut versuchen</string>
|
<string name="state_retry">Erneut versuchen</string>
|
||||||
<string name="state_failure_unknown">Etwas ist schiefgelaufen.</string>
|
<string name="state_failure_unknown">Etwas ist schiefgelaufen.</string>
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
<string name="state_failure_no_calendars">Keine Kalender eingerichtet.</string>
|
<string name="state_failure_no_calendars">Keine Kalender eingerichtet.</string>
|
||||||
<string name="state_failure_no_calendars_action">System-Kalender-Einstellungen öffnen</string>
|
<string name="state_failure_no_calendars_action">System-Kalender-Einstellungen öffnen</string>
|
||||||
<string name="state_failure_provider">Kalender konnte nicht gelesen werden.</string>
|
<string name="state_failure_provider">Kalender konnte nicht gelesen werden.</string>
|
||||||
|
|
||||||
<!-- Permission-Flow (F1) -->
|
<!-- Permission-Flow (F1) -->
|
||||||
<string name="permission_rationale_title">Alle Termine, schön im Blick</string>
|
<string name="permission_rationale_title">Alle Termine, schön im Blick</string>
|
||||||
<string name="permission_rationale_body">Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App nie.</string>
|
<string name="permission_rationale_body">Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App nie.</string>
|
||||||
@@ -26,7 +25,6 @@
|
|||||||
<string name="permission_benefit_privacy_title">Kein Tracking, niemals</string>
|
<string name="permission_benefit_privacy_title">Kein Tracking, niemals</string>
|
||||||
<string name="permission_benefit_privacy_body">Keine Telemetrie, keine Analyse, keine Werbung.</string>
|
<string name="permission_benefit_privacy_body">Keine Telemetrie, keine Analyse, keine Werbung.</string>
|
||||||
<string name="permission_privacy_footnote">Bleibt auf deinem Gerät · keine Internet-Berechtigung</string>
|
<string name="permission_privacy_footnote">Bleibt auf deinem Gerät · keine Internet-Berechtigung</string>
|
||||||
|
|
||||||
<!-- Monatsansicht (S1) -->
|
<!-- Monatsansicht (S1) -->
|
||||||
<string name="month_prev">Vorheriger Monat</string>
|
<string name="month_prev">Vorheriger Monat</string>
|
||||||
<string name="month_next">Nächster Monat</string>
|
<string name="month_next">Nächster Monat</string>
|
||||||
@@ -35,14 +33,11 @@
|
|||||||
<string name="month_open_menu">Menü öffnen</string>
|
<string name="month_open_menu">Menü öffnen</string>
|
||||||
<string name="month_action_settings">Einstellungen</string>
|
<string name="month_action_settings">Einstellungen</string>
|
||||||
<string name="month_a11y_today_prefix">Heute</string>
|
<string name="month_a11y_today_prefix">Heute</string>
|
||||||
|
|
||||||
<!-- Wochenansicht (S2) -->
|
<!-- Wochenansicht (S2) -->
|
||||||
<string name="week_today_action">Diese Woche</string>
|
<string name="week_today_action">Diese Woche</string>
|
||||||
<string name="week_number_label">KW</string>
|
<string name="week_number_label">KW</string>
|
||||||
|
|
||||||
<!-- Tagesansicht (S3) -->
|
<!-- Tagesansicht (S3) -->
|
||||||
<string name="day_today_action">Heute</string>
|
<string name="day_today_action">Heute</string>
|
||||||
|
|
||||||
<!-- Event-Detail-Screen (S4) -->
|
<!-- Event-Detail-Screen (S4) -->
|
||||||
<string name="event_detail_back">Zurück</string>
|
<string name="event_detail_back">Zurück</string>
|
||||||
<string name="event_detail_edit">Bearbeiten</string>
|
<string name="event_detail_edit">Bearbeiten</string>
|
||||||
@@ -61,7 +56,6 @@
|
|||||||
<string name="event_delete_write_denied">Calendula braucht Schreibzugriff, um Termine zu löschen</string>
|
<string name="event_delete_write_denied">Calendula braucht Schreibzugriff, um Termine zu löschen</string>
|
||||||
<string name="dialog_cancel">Abbrechen</string>
|
<string name="dialog_cancel">Abbrechen</string>
|
||||||
<string name="dialog_ok">OK</string>
|
<string name="dialog_ok">OK</string>
|
||||||
|
|
||||||
<!-- Termin-Formular (v1.2 Erstellen) -->
|
<!-- Termin-Formular (v1.2 Erstellen) -->
|
||||||
<string name="event_edit_new_title">Neuer Termin</string>
|
<string name="event_edit_new_title">Neuer Termin</string>
|
||||||
<string name="event_edit_close">Schließen</string>
|
<string name="event_edit_close">Schließen</string>
|
||||||
@@ -94,7 +88,6 @@
|
|||||||
<string name="reminder_unit_weeks">Wochen</string>
|
<string name="reminder_unit_weeks">Wochen</string>
|
||||||
<string name="event_edit_availability">Verfügbarkeit</string>
|
<string name="event_edit_availability">Verfügbarkeit</string>
|
||||||
<string name="event_edit_visibility">Sichtbarkeit</string>
|
<string name="event_edit_visibility">Sichtbarkeit</string>
|
||||||
|
|
||||||
<!-- Termin-Formular — eigene Terminfarbe -->
|
<!-- Termin-Formular — eigene Terminfarbe -->
|
||||||
<string name="event_edit_color">Farbe</string>
|
<string name="event_edit_color">Farbe</string>
|
||||||
<string name="event_edit_color_default">Kalenderfarbe</string>
|
<string name="event_edit_color_default">Kalenderfarbe</string>
|
||||||
@@ -103,7 +96,6 @@
|
|||||||
<string name="event_edit_color_unsupported">Für diesen Kalender nicht verfügbar</string>
|
<string name="event_edit_color_unsupported">Für diesen Kalender nicht verfügbar</string>
|
||||||
<string name="event_edit_color_unsupported_hint">Dieser Kalender stellt keine Farbpalette bereit. Du kannst eigene Farben für solche Kalender in den Einstellungen aktivieren.</string>
|
<string name="event_edit_color_unsupported_hint">Dieser Kalender stellt keine Farbpalette bereit. Du kannst eigene Farben für solche Kalender in den Einstellungen aktivieren.</string>
|
||||||
<string name="event_edit_color_sync_warning">Dieser Kalender verwirft oder überschreibt die Farbe unter Umständen bei der nächsten Synchronisierung.</string>
|
<string name="event_edit_color_sync_warning">Dieser Kalender verwirft oder überschreibt die Farbe unter Umständen bei der nächsten Synchronisierung.</string>
|
||||||
|
|
||||||
<!-- Termin-Formular — Speicher-Konflikt (v2.0) -->
|
<!-- Termin-Formular — Speicher-Konflikt (v2.0) -->
|
||||||
<string name="event_edit_conflict_title">Termin wurde extern geändert</string>
|
<string name="event_edit_conflict_title">Termin wurde extern geändert</string>
|
||||||
<string name="event_edit_conflict_body">Während du bearbeitet hast, wurde dieser Termin anderswo geändert — durch Synchronisierung oder eine andere App. Was soll mit deinen Änderungen passieren?</string>
|
<string name="event_edit_conflict_body">Während du bearbeitet hast, wurde dieser Termin anderswo geändert — durch Synchronisierung oder eine andere App. Was soll mit deinen Änderungen passieren?</string>
|
||||||
@@ -113,7 +105,6 @@
|
|||||||
<string name="event_edit_conflict_discard_hint">Der Termin bleibt, wie er jetzt ist</string>
|
<string name="event_edit_conflict_discard_hint">Der Termin bleibt, wie er jetzt ist</string>
|
||||||
<string name="event_edit_gone_title">Termin wurde gelöscht</string>
|
<string name="event_edit_gone_title">Termin wurde gelöscht</string>
|
||||||
<string name="event_edit_gone_body">Dieser Termin wurde zwischenzeitlich gelöscht, etwa auf einem anderen Gerät. Deine Änderungen können nicht mehr gespeichert werden.</string>
|
<string name="event_edit_gone_body">Dieser Termin wurde zwischenzeitlich gelöscht, etwa auf einem anderen Gerät. Deine Änderungen können nicht mehr gespeichert werden.</string>
|
||||||
|
|
||||||
<!-- Termin-Formular — Wiederholungs-Picker (v1.3) -->
|
<!-- Termin-Formular — Wiederholungs-Picker (v1.3) -->
|
||||||
<string name="event_edit_recurrence_none">Wiederholt sich nicht</string>
|
<string name="event_edit_recurrence_none">Wiederholt sich nicht</string>
|
||||||
<string name="event_edit_recurrence_custom">Benutzerdefiniert</string>
|
<string name="event_edit_recurrence_custom">Benutzerdefiniert</string>
|
||||||
@@ -156,7 +147,6 @@
|
|||||||
<string name="event_attendee_tentative">Vorläufig</string>
|
<string name="event_attendee_tentative">Vorläufig</string>
|
||||||
<string name="event_attendee_needs_action">Keine Antwort</string>
|
<string name="event_attendee_needs_action">Keine Antwort</string>
|
||||||
<string name="event_attendee_unknown">—</string>
|
<string name="event_attendee_unknown">—</string>
|
||||||
|
|
||||||
<!-- Event-Detail — vollständiges Auslesen (v0.6) -->
|
<!-- Event-Detail — vollständiges Auslesen (v0.6) -->
|
||||||
<string name="event_detail_reminders">Erinnerungen</string>
|
<string name="event_detail_reminders">Erinnerungen</string>
|
||||||
<string name="event_detail_timezone">Zeitzone</string>
|
<string name="event_detail_timezone">Zeitzone</string>
|
||||||
@@ -196,10 +186,8 @@
|
|||||||
<item quantity="one">%d Stunde</item>
|
<item quantity="one">%d Stunde</item>
|
||||||
<item quantity="other">%d Stunden</item>
|
<item quantity="other">%d Stunden</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<!-- Geteilte Event-Strings -->
|
<!-- Geteilte Event-Strings -->
|
||||||
<string name="event_untitled">(Ohne Titel)</string>
|
<string name="event_untitled">(Ohne Titel)</string>
|
||||||
|
|
||||||
<!-- Erinnerungs-Benachrichtigungen (v1.4) -->
|
<!-- Erinnerungs-Benachrichtigungen (v1.4) -->
|
||||||
<string name="reminder_channel_name">Termin-Erinnerungen</string>
|
<string name="reminder_channel_name">Termin-Erinnerungen</string>
|
||||||
<string name="reminder_channel_description">Benachrichtigungen zu den Erinnerungszeiten deiner Termine</string>
|
<string name="reminder_channel_description">Benachrichtigungen zu den Erinnerungszeiten deiner Termine</string>
|
||||||
@@ -215,23 +203,19 @@
|
|||||||
<string name="reminder_onboarding_skip_button">Später</string>
|
<string name="reminder_onboarding_skip_button">Später</string>
|
||||||
<string name="reminder_action_snooze">Schlummern</string>
|
<string name="reminder_action_snooze">Schlummern</string>
|
||||||
<string name="reminder_action_dismiss">Verwerfen</string>
|
<string name="reminder_action_dismiss">Verwerfen</string>
|
||||||
|
|
||||||
<!-- View-Switcher (M1) -->
|
<!-- View-Switcher (M1) -->
|
||||||
<string name="view_month">Monat</string>
|
<string name="view_month">Monat</string>
|
||||||
<string name="view_week">Woche</string>
|
<string name="view_week">Woche</string>
|
||||||
<string name="view_day">Tag</string>
|
<string name="view_day">Tag</string>
|
||||||
<string name="view_agenda">Agenda</string>
|
<string name="view_agenda">Agenda</string>
|
||||||
<string name="view_section">Ansicht</string>
|
<string name="view_section">Ansicht</string>
|
||||||
|
|
||||||
<!-- Zu Datum springen (Navigationsleiste) -->
|
<!-- Zu Datum springen (Navigationsleiste) -->
|
||||||
<string name="drawer_jump_to_date">Zu Datum springen</string>
|
<string name="drawer_jump_to_date">Zu Datum springen</string>
|
||||||
|
|
||||||
<!-- Agenda-Ansicht -->
|
<!-- Agenda-Ansicht -->
|
||||||
<string name="agenda_today_action">Heute</string>
|
<string name="agenda_today_action">Heute</string>
|
||||||
<string name="agenda_header_today">Heute</string>
|
<string name="agenda_header_today">Heute</string>
|
||||||
<string name="agenda_header_tomorrow">Morgen</string>
|
<string name="agenda_header_tomorrow">Morgen</string>
|
||||||
<string name="agenda_empty_title">Alles erledigt</string>
|
<string name="agenda_empty_title">Alles erledigt</string>
|
||||||
|
|
||||||
<!-- Terminsuche -->
|
<!-- Terminsuche -->
|
||||||
<string name="search_action">Suchen</string>
|
<string name="search_action">Suchen</string>
|
||||||
<string name="search_hint">Termine suchen</string>
|
<string name="search_hint">Termine suchen</string>
|
||||||
@@ -239,7 +223,6 @@
|
|||||||
<string name="search_clear">Löschen</string>
|
<string name="search_clear">Löschen</string>
|
||||||
<string name="search_idle_hint">Durchsuche deine Termine nach Titel, Ort oder Notizen.</string>
|
<string name="search_idle_hint">Durchsuche deine Termine nach Titel, Ort oder Notizen.</string>
|
||||||
<string name="search_empty">Keine Termine passen zu „%1$s“.</string>
|
<string name="search_empty">Keine Termine passen zu „%1$s“.</string>
|
||||||
|
|
||||||
<!-- Startbildschirm-Widgets -->
|
<!-- Startbildschirm-Widgets -->
|
||||||
<string name="widget_agenda_title">Anstehend</string>
|
<string name="widget_agenda_title">Anstehend</string>
|
||||||
<string name="widget_agenda_label">Calendula Agenda</string>
|
<string name="widget_agenda_label">Calendula Agenda</string>
|
||||||
@@ -250,19 +233,15 @@
|
|||||||
<string name="widget_prev_month">Vorheriger Monat</string>
|
<string name="widget_prev_month">Vorheriger Monat</string>
|
||||||
<string name="widget_next_month">Nächster Monat</string>
|
<string name="widget_next_month">Nächster Monat</string>
|
||||||
<string name="widget_today">Heute</string>
|
<string name="widget_today">Heute</string>
|
||||||
|
|
||||||
<!-- Verknüpfungen (Long-Press auf das App-Symbol) -->
|
<!-- Verknüpfungen (Long-Press auf das App-Symbol) -->
|
||||||
<string name="shortcut_new_event_short">Neuer Termin</string>
|
<string name="shortcut_new_event_short">Neuer Termin</string>
|
||||||
<string name="shortcut_new_event_long">Neuen Termin erstellen</string>
|
<string name="shortcut_new_event_long">Neuen Termin erstellen</string>
|
||||||
|
|
||||||
<!-- Schnelleinstellungen-Kachel -->
|
<!-- Schnelleinstellungen-Kachel -->
|
||||||
<string name="qs_tile_new_event_label">Neuer Termin</string>
|
<string name="qs_tile_new_event_label">Neuer Termin</string>
|
||||||
<string name="settings_qs_tile">Schnelleinstellungen-Kachel hinzufügen</string>
|
<string name="settings_qs_tile">Schnelleinstellungen-Kachel hinzufügen</string>
|
||||||
<string name="settings_qs_tile_hint">Eine Kachel „Neuer Termin“ zu den Schnelleinstellungen hinzufügen.</string>
|
<string name="settings_qs_tile_hint">Eine Kachel „Neuer Termin“ zu den Schnelleinstellungen hinzufügen.</string>
|
||||||
|
|
||||||
<!-- Kalender-Filter (M3) -->
|
<!-- Kalender-Filter (M3) -->
|
||||||
<string name="filter_title">Kalender</string>
|
<string name="filter_title">Kalender</string>
|
||||||
|
|
||||||
<!-- Einstellungen (M4) -->
|
<!-- Einstellungen (M4) -->
|
||||||
<string name="settings_title">Einstellungen</string>
|
<string name="settings_title">Einstellungen</string>
|
||||||
<string name="settings_back">Zurück</string>
|
<string name="settings_back">Zurück</string>
|
||||||
@@ -344,7 +323,6 @@
|
|||||||
<string name="settings_about_logo_desc">Calendula-App-Symbol</string>
|
<string name="settings_about_logo_desc">Calendula-App-Symbol</string>
|
||||||
<string name="settings_report_problem">Problem melden</string>
|
<string name="settings_report_problem">Problem melden</string>
|
||||||
<string name="settings_report_problem_hint">Absturzbericht senden oder Issue-Tracker öffnen</string>
|
<string name="settings_report_problem_hint">Absturzbericht senden oder Issue-Tracker öffnen</string>
|
||||||
|
|
||||||
<!-- Calendar manager -->
|
<!-- Calendar manager -->
|
||||||
<string name="calendars_title">Kalender</string>
|
<string name="calendars_title">Kalender</string>
|
||||||
<string name="calendars_local_header">Deine Kalender</string>
|
<string name="calendars_local_header">Deine Kalender</string>
|
||||||
@@ -399,7 +377,6 @@
|
|||||||
<item quantity="one">%d bereits in diesem Kalender übersprungen.</item>
|
<item quantity="one">%d bereits in diesem Kalender übersprungen.</item>
|
||||||
<item quantity="other">%d bereits in diesem Kalender übersprungen.</item>
|
<item quantity="other">%d bereits in diesem Kalender übersprungen.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<!-- Absturzberichte: vom Nutzer selbst als Gitea-Issue einreichbar -->
|
<!-- Absturzberichte: vom Nutzer selbst als Gitea-Issue einreichbar -->
|
||||||
<string name="crash_dialog_title">Calendula ist abgestürzt</string>
|
<string name="crash_dialog_title">Calendula ist abgestürzt</string>
|
||||||
<string name="crash_dialog_message">Calendula wurde beim letzten Mal unerwartet beendet. Du kannst bei der Behebung helfen, indem du diesen Bericht als Issue sendest. Er bleibt auf deinem Gerät, bis du ihn teilst, und enthält keine persönlichen Daten oder Kalenderinhalte — nur die technischen Angaben unten.</string>
|
<string name="crash_dialog_message">Calendula wurde beim letzten Mal unerwartet beendet. Du kannst bei der Behebung helfen, indem du diesen Bericht als Issue sendest. Er bleibt auf deinem Gerät, bis du ihn teilst, und enthält keine persönlichen Daten oder Kalenderinhalte — nur die technischen Angaben unten.</string>
|
||||||
|
|||||||
9
app/src/main/res/values-es/strings.xml
Normal file
9
app/src/main/res/values-es/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values-fr/strings.xml
Normal file
9
app/src/main/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values-it/strings.xml
Normal file
9
app/src/main/res/values-it/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values-pl/strings.xml
Normal file
9
app/src/main/res/values-pl/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values-pt/strings.xml
Normal file
9
app/src/main/res/values-pt/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
9
app/src/main/res/values-ru/strings.xml
Normal file
9
app/src/main/res/values-ru/strings.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string-array name="backup_interval_units">
|
||||||
|
<item>Minutes</item>
|
||||||
|
<item>Hours</item>
|
||||||
|
<item>Days</item>
|
||||||
|
<item>Weeks</item>
|
||||||
|
</string-array>
|
||||||
|
</resources>
|
||||||
@@ -282,6 +282,13 @@
|
|||||||
<string name="settings_time_format_24h">24-hour (14:00)</string>
|
<string name="settings_time_format_24h">24-hour (14:00)</string>
|
||||||
<string name="settings_hour_lines">Hour lines</string>
|
<string name="settings_hour_lines">Hour lines</string>
|
||||||
<string name="settings_hour_lines_summary">Show a separator line at each hour in week and day view</string>
|
<string name="settings_hour_lines_summary">Show a separator line at each hour in week and day view</string>
|
||||||
|
<string name="settings_dim_completed">Dim completed events</string>
|
||||||
|
<string name="settings_dim_completed_summary">Fade events that have already ended in month and week view</string>
|
||||||
|
<string name="settings_past_events">Past events</string>
|
||||||
|
<string name="settings_past_events_show">Show</string>
|
||||||
|
<string name="settings_past_events_dim">Dim</string>
|
||||||
|
<string name="settings_past_events_hide">Hide</string>
|
||||||
|
<string name="settings_agenda_header">Agenda</string>
|
||||||
<string name="settings_agenda_range">Agenda range</string>
|
<string name="settings_agenda_range">Agenda range</string>
|
||||||
<string name="settings_agenda_range_hint">How far ahead the Agenda screen lists events.</string>
|
<string name="settings_agenda_range_hint">How far ahead the Agenda screen lists events.</string>
|
||||||
<string name="settings_agenda_widget_range">Agenda widget range</string>
|
<string name="settings_agenda_widget_range">Agenda widget range</string>
|
||||||
|
|||||||
19
fastlane/metadata/android/en-US/changelogs/21200.txt
Normal file
19
fastlane/metadata/android/en-US/changelogs/21200.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
### Added
|
||||||
|
- See past events your way. Two new settings change how events that have already
|
||||||
|
ended are shown. **Past events** (Settings → Appearance → Agenda) lets the
|
||||||
|
agenda — both in the app and the home-screen "Upcoming" widget — keep them as
|
||||||
|
usual, dim them, or hide them from the list entirely. **Dim completed events**
|
||||||
|
(Settings → Appearance) separately fades finished events in the month and week
|
||||||
|
views. Both are off by default, an event only counts as finished once it has
|
||||||
|
actually ended (events still in progress are never dimmed), and the lists
|
||||||
|
update on their own as the day goes on. Thanks to @ptab for the suggestion
|
||||||
|
([#12]).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Readable titles on overlapping events. In the week and day views, events that
|
||||||
|
overlap split a day into slim columns where the title used to be clipped to a
|
||||||
|
character or two. The title now wraps across as many lines as the block can
|
||||||
|
fit, and the time is hidden on those narrow blocks so the name can fill the
|
||||||
|
space — so you can tell events apart without opening each one. Thanks to @ptab
|
||||||
|
for the suggestion ([#13]).
|
||||||
|
|
||||||
Reference in New Issue
Block a user