Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
549ef1dfbf | ||
|
|
164b5fb25d |
@@ -15,7 +15,7 @@ jobs:
|
|||||||
detect:
|
detect:
|
||||||
# Gitea only; see the same guard in release.yaml.
|
# Gitea only; see the same guard in release.yaml.
|
||||||
if: github.repository_owner == 'makiolaj'
|
if: github.repository_owner == 'makiolaj'
|
||||||
runs-on: light
|
runs-on: docker
|
||||||
outputs:
|
outputs:
|
||||||
is_beta: ${{ steps.v.outputs.is_beta }}
|
is_beta: ${{ steps.v.outputs.is_beta }}
|
||||||
version: ${{ steps.v.outputs.version }}
|
version: ${{ steps.v.outputs.version }}
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
beta:
|
beta:
|
||||||
needs: detect
|
needs: detect
|
||||||
if: needs.detect.outputs.is_beta == 'true'
|
if: needs.detect.outputs.is_beta == 'true'
|
||||||
runs-on: gradle
|
runs-on: docker
|
||||||
env:
|
env:
|
||||||
ANDROID_HOME: /opt/android-sdk
|
ANDROID_HOME: /opt/android-sdk
|
||||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
# secrets. repository_owner differs between the two forges regardless of
|
# secrets. repository_owner differs between the two forges regardless of
|
||||||
# URL, proxy or instance rename, so this closes it permanently.
|
# URL, proxy or instance rename, so this closes it permanently.
|
||||||
if: github.repository_owner == 'makiolaj'
|
if: github.repository_owner == 'makiolaj'
|
||||||
runs-on: light
|
runs-on: docker
|
||||||
outputs:
|
outputs:
|
||||||
is_release: ${{ steps.v.outputs.is_release }}
|
is_release: ${{ steps.v.outputs.is_release }}
|
||||||
version: ${{ steps.v.outputs.version }}
|
version: ${{ steps.v.outputs.version }}
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
needs: detect
|
needs: detect
|
||||||
if: needs.detect.outputs.is_release == 'true' || github.event_name == 'workflow_dispatch'
|
if: needs.detect.outputs.is_release == 'true' || github.event_name == 'workflow_dispatch'
|
||||||
runs-on: gradle
|
runs-on: docker
|
||||||
env:
|
env:
|
||||||
ANDROID_HOME: /opt/android-sdk
|
ANDROID_HOME: /opt/android-sdk
|
||||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Scroll back to earlier days in the agenda.** Scroll up past today to see earlier days' events, loaded a month at a time. Past days follow the past-events setting: Dim fades them, and with Hide the agenda still starts at today ([#367]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **The agenda always starts today.** Coming from the month or day view, the agenda used to start at the day you had open there, so "Next 30 days" could show the 1st to the 30th of the month. It now opens on today with your chosen range. Backing out of a day you opened from the agenda still returns you to where you were ([#367]).
|
||||||
|
- **Multi-day events dim on the days already behind you.** With past events dimmed, an event running over several days stayed at full strength on its earlier days until it ended. Those days now dim in the agenda, month, week and day views ([#367]).
|
||||||
|
|
||||||
## [2.22.5] — 2026-10-07
|
## [2.22.5] — 2026-10-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -1848,4 +1855,5 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#361]: https://codeberg.org/jlmakiola/calendula/issues/361
|
[#361]: https://codeberg.org/jlmakiola/calendula/issues/361
|
||||||
[#363]: https://codeberg.org/jlmakiola/calendula/issues/363
|
[#363]: https://codeberg.org/jlmakiola/calendula/issues/363
|
||||||
[#364]: https://codeberg.org/jlmakiola/calendula/issues/364
|
[#364]: https://codeberg.org/jlmakiola/calendula/issues/364
|
||||||
|
[#367]: https://codeberg.org/jlmakiola/calendula/issues/367
|
||||||
[#373]: https://codeberg.org/jlmakiola/calendula/issues/373
|
[#373]: https://codeberg.org/jlmakiola/calendula/issues/373
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ android {
|
|||||||
// (up to 2.22.3: 2.22.3 -> 22203; from 2.22.4: 2.23.0-beta.1 -> 2230001,
|
// (up to 2.22.3: 2.22.3 -> 22203; from 2.22.4: 2.23.0-beta.1 -> 2230001,
|
||||||
// 2.22.4 -> 2220499), and CI fails if the committed one doesn't match.
|
// 2.22.4 -> 2220499), and CI fails if the committed one doesn't match.
|
||||||
// See docs/RELEASING.md.
|
// See docs/RELEASING.md.
|
||||||
versionCode = 2220599
|
versionCode = 2230001
|
||||||
versionName = "2.22.5"
|
versionName = "2.23.0-beta.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,9 @@ fun CalendarHost(
|
|||||||
mutableStateOf(listOf(defaultView))
|
mutableStateOf(listOf(defaultView))
|
||||||
}
|
}
|
||||||
var navigationVersion by remember { mutableIntStateOf(0) }
|
var navigationVersion by remember { mutableIntStateOf(0) }
|
||||||
|
// True after backing out of a day opened from the view now shown, so the
|
||||||
|
// agenda returns to where it was instead of today (#367).
|
||||||
|
var returnedFromDay by remember { mutableStateOf(false) }
|
||||||
val view = viewStack.last()
|
val view = viewStack.last()
|
||||||
// The date every view opens on and carries along (#184); tapping a day
|
// The date every view opens on and carries along (#184); tapping a day
|
||||||
// anywhere focuses it and opens the day view there.
|
// anywhere focuses it and opens the day view there.
|
||||||
@@ -163,6 +166,7 @@ fun CalendarHost(
|
|||||||
val switchScope = rememberCoroutineScope()
|
val switchScope = rememberCoroutineScope()
|
||||||
val switchTo: ((List<CalendarView>) -> List<CalendarView>) -> Unit = { move ->
|
val switchTo: ((List<CalendarView>) -> List<CalendarView>) -> Unit = { move ->
|
||||||
navigationVersion++
|
navigationVersion++
|
||||||
|
returnedFromDay = false
|
||||||
viewStack = move(viewStack)
|
viewStack = move(viewStack)
|
||||||
}
|
}
|
||||||
val onSelectView: (CalendarView) -> Unit = { target -> switchTo { it.selectView(target) } }
|
val onSelectView: (CalendarView) -> Unit = { target -> switchTo { it.selectView(target) } }
|
||||||
@@ -322,7 +326,10 @@ fun CalendarHost(
|
|||||||
// [requestedDetailKey]. Date/event opens root the stack in the widget's own
|
// [requestedDetailKey]. Date/event opens root the stack in the widget's own
|
||||||
// view so backing out returns there (then home), not to the default.
|
// view so backing out returns there (then home), not to the default.
|
||||||
LaunchedEffect(widgetNavRequest) {
|
LaunchedEffect(widgetNavRequest) {
|
||||||
if (widgetNavRequest != null) navigationVersion++
|
if (widgetNavRequest != null) {
|
||||||
|
navigationVersion++
|
||||||
|
returnedFromDay = false
|
||||||
|
}
|
||||||
when (val req = widgetNavRequest) {
|
when (val req = widgetNavRequest) {
|
||||||
is WidgetNavRequest.OpenDate -> {
|
is WidgetNavRequest.OpenDate -> {
|
||||||
// Drill the day view in over the widget's view: drop any overlay
|
// Drill the day view in over the widget's view: drop any overlay
|
||||||
@@ -424,11 +431,14 @@ fun CalendarHost(
|
|||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
BackHandler(enabled = viewBackEnabled) {
|
BackHandler(enabled = viewBackEnabled) {
|
||||||
navigationVersion++
|
navigationVersion++
|
||||||
|
returnedFromDay = viewStack.last() == CalendarView.Day
|
||||||
viewStack = viewStack.dropLast(1)
|
viewStack = viewStack.dropLast(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
PredictiveBackHandler(enabled = viewBackEnabled) { events ->
|
PredictiveBackHandler(enabled = viewBackEnabled) { events ->
|
||||||
val previous = viewStack[viewStack.size - 2]
|
val previous = viewStack[viewStack.size - 2]
|
||||||
|
// Set before the seek, which already composes the previous view.
|
||||||
|
returnedFromDay = viewStack.last() == CalendarView.Day
|
||||||
try {
|
try {
|
||||||
events.collect { viewTransition.seekTo(it.progress, targetState = previous) }
|
events.collect { viewTransition.seekTo(it.progress, targetState = previous) }
|
||||||
navigationVersion++
|
navigationVersion++
|
||||||
@@ -560,6 +570,7 @@ fun CalendarHost(
|
|||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
onCreateEvent = onCreateEvent,
|
onCreateEvent = onCreateEvent,
|
||||||
quickSwitchViews = quickSwitchViews,
|
quickSwitchViews = quickSwitchViews,
|
||||||
|
restorePosition = returnedFromDay,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import de.jeanlucmakiola.calendula.ui.common.ScreenActions
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.ScreenBody
|
import de.jeanlucmakiola.calendula.ui.common.ScreenBody
|
||||||
import de.jeanlucmakiola.calendula.ui.common.ScreenChrome
|
import de.jeanlucmakiola.calendula.ui.common.ScreenChrome
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewFocus
|
import de.jeanlucmakiola.calendula.ui.common.LocalViewFocus
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EnterOnFocus
|
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -38,7 +37,16 @@ import androidx.compose.material3.Icon
|
|||||||
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.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableIntState
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -53,7 +61,7 @@ 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.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.isPastOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker
|
||||||
import de.jeanlucmakiola.calendula.ui.common.AppBarSpacing
|
import de.jeanlucmakiola.calendula.ui.common.AppBarSpacing
|
||||||
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
|
||||||
@@ -82,11 +90,23 @@ fun AgendaScreen(
|
|||||||
onCreateEvent: (LocalDate, Int?) -> Unit,
|
onCreateEvent: (LocalDate, Int?) -> Unit,
|
||||||
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
|
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
restorePosition: Boolean = false,
|
||||||
viewModel: AgendaViewModel = hiltViewModel(),
|
viewModel: AgendaViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
// Opens on the focused date and hands back the day scrolled to the top.
|
// Opens on today, not on the day another view was on (#367). Backing out of
|
||||||
|
// a day opened from here, or a recreated screen, returns to the day last at
|
||||||
|
// the top. The top day still goes back to the shared focus.
|
||||||
val focus = LocalViewFocus.current
|
val focus = LocalViewFocus.current
|
||||||
EnterOnFocus(viewModel) { if (it != viewModel.anchor.value) viewModel.goToDate(it) }
|
val opened = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val restoreTop = remember(viewModel) {
|
||||||
|
if (restorePosition || opened.value) {
|
||||||
|
viewModel.lastTopDate
|
||||||
|
} else {
|
||||||
|
viewModel.goToToday()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SideEffect { opened.value = true }
|
||||||
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 pastDisplay by viewModel.pastEventDisplay.collectAsStateWithLifecycle()
|
||||||
@@ -106,20 +126,19 @@ fun AgendaScreen(
|
|||||||
// list scroll, not an anchor change — "today" has to bring the list back to
|
// list scroll, not an anchor change — "today" has to bring the list back to
|
||||||
// the top as well, and stay offered while it is scrolled away (#305).
|
// the top as well, and stay offered while it is scrolled away (#305).
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
// Past days load above the anchor (#367), so the anchor's row counts as the
|
||||||
|
// start of the list.
|
||||||
|
val anchorIndex = remember { mutableIntStateOf(0) }
|
||||||
val scrolledAway by remember {
|
val scrolledAway by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
|
listState.firstVisibleItemIndex != anchorIndex.intValue ||
|
||||||
|
listState.firstVisibleItemScrollOffset > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A new window is rendered from its first day, so the list belongs at the
|
|
||||||
// top. Keyed on the anchor the days arrived with, not the one just
|
|
||||||
// requested: scrolling before the new rows compose leaves the list keyed to
|
|
||||||
// a row that reappears further down, and it follows it there.
|
|
||||||
LaunchedEffect(successState?.anchor) { listState.scrollToItem(0) }
|
|
||||||
val jumpToToday: () -> Unit = {
|
val jumpToToday: () -> Unit = {
|
||||||
// Off today, moving the anchor is what resets the list (above); on today
|
// Off today, moving the anchor is what repositions the list; on today
|
||||||
// the window doesn't change, so the scroll back is the whole action.
|
// the window doesn't change, so the scroll back is the whole action.
|
||||||
if (isOnToday) scope.launch { listState.animateScrollToItem(0) }
|
if (isOnToday) scope.launch { listState.animateScrollToItem(anchorIndex.intValue) }
|
||||||
viewModel.goToToday()
|
viewModel.goToToday()
|
||||||
(state as? AgendaUiState.Success)?.today?.let { focus?.date = it }
|
(state as? AgendaUiState.Success)?.today?.let { focus?.date = it }
|
||||||
}
|
}
|
||||||
@@ -188,11 +207,17 @@ fun AgendaScreen(
|
|||||||
pastDisplay = pastDisplay,
|
pastDisplay = pastDisplay,
|
||||||
showToday = showToday,
|
showToday = showToday,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
|
anchorIndex = anchorIndex,
|
||||||
|
initialTopDate = restoreTop,
|
||||||
|
onLoadEarlier = viewModel::loadEarlier,
|
||||||
onRetry = viewModel::goToToday,
|
onRetry = viewModel::goToToday,
|
||||||
onEventClick = onEventClick,
|
onEventClick = onEventClick,
|
||||||
onOpenDay = onOpenDay,
|
onOpenDay = onOpenDay,
|
||||||
onTopDay = if (successState?.anchor == anchor) {
|
onTopDay = if (successState?.anchor == anchor) {
|
||||||
{ focus?.date = it }
|
{
|
||||||
|
focus?.date = it
|
||||||
|
viewModel.rememberTopDay(it)
|
||||||
|
}
|
||||||
} else null,
|
} else null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -294,6 +319,9 @@ internal fun AgendaContent(
|
|||||||
onOpenDay: (LocalDate) -> Unit,
|
onOpenDay: (LocalDate) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
listState: LazyListState = rememberLazyListState(),
|
listState: LazyListState = rememberLazyListState(),
|
||||||
|
anchorIndex: MutableIntState = remember { mutableIntStateOf(0) },
|
||||||
|
initialTopDate: LocalDate? = null,
|
||||||
|
onLoadEarlier: () -> Unit = {},
|
||||||
onTopDay: ((LocalDate) -> Unit)? = null,
|
onTopDay: ((LocalDate) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
when (state) {
|
when (state) {
|
||||||
@@ -308,28 +336,34 @@ internal fun AgendaContent(
|
|||||||
// away (or fade) as they end while the screen stays open.
|
// away (or fade) as they end while the screen stays open.
|
||||||
val filtered = if (pastDisplay == PastEventDisplay.HIDE) {
|
val filtered = if (pastDisplay == PastEventDisplay.HIDE) {
|
||||||
state.days.mapNotNull { day ->
|
state.days.mapNotNull { day ->
|
||||||
val remaining = day.events.filterNot { it.hasEnded(now) }
|
val remaining = day.events.filterNot { it.isPastOn(day.date, now, state.zone) }
|
||||||
if (remaining.isEmpty()) null else day.copy(events = remaining)
|
if (remaining.isEmpty()) null else day.copy(events = remaining)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.days
|
state.days
|
||||||
}
|
}
|
||||||
// Anchor today with a "nothing left today" placeholder — but only when
|
// Anchor today with a "nothing left today" placeholder — but only when
|
||||||
// the window actually starts on today; a jumped-to date has no today in
|
// the agenda actually opens on today; a jumped-to date has no today
|
||||||
// it, so anchoring there would be misleading (#35).
|
// in it, so anchoring there would be misleading (#35).
|
||||||
val days = anchorTodayIfMissing(
|
val days = anchorTodayIfMissing(
|
||||||
days = filtered,
|
days = filtered,
|
||||||
today = state.today,
|
today = state.today,
|
||||||
enabled = showToday && state.anchor == state.today,
|
enabled = showToday && state.anchor == state.today,
|
||||||
)
|
)
|
||||||
if (days.isEmpty()) {
|
// Past days alone don't count as anything coming up.
|
||||||
|
if (days.none { it.date >= state.anchor }) {
|
||||||
// Nothing to scroll: drop the position a previous list left, so
|
// Nothing to scroll: drop the position a previous list left, so
|
||||||
// the today FAB doesn't linger over an empty screen.
|
// the today FAB doesn't linger over an empty screen.
|
||||||
LaunchedEffect(Unit) { listState.scrollToItem(0) }
|
LaunchedEffect(Unit) { listState.scrollToItem(0) }
|
||||||
|
SideEffect { anchorIndex.intValue = 0 }
|
||||||
AgendaEmpty(modifier)
|
AgendaEmpty(modifier)
|
||||||
} else {
|
} else {
|
||||||
AgendaList(
|
AgendaList(
|
||||||
days = days,
|
days = days,
|
||||||
|
anchor = state.anchor,
|
||||||
|
anchorIndex = anchorIndex,
|
||||||
|
initialTopDate = initialTopDate,
|
||||||
|
onLoadEarlier = onLoadEarlier,
|
||||||
today = state.today,
|
today = state.today,
|
||||||
zone = state.zone,
|
zone = state.zone,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
@@ -349,6 +383,10 @@ internal fun AgendaContent(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AgendaList(
|
private fun AgendaList(
|
||||||
days: List<AgendaDay>,
|
days: List<AgendaDay>,
|
||||||
|
anchor: LocalDate,
|
||||||
|
anchorIndex: MutableIntState,
|
||||||
|
initialTopDate: LocalDate?,
|
||||||
|
onLoadEarlier: () -> Unit,
|
||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
zone: TimeZone,
|
zone: TimeZone,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
@@ -359,19 +397,49 @@ private fun AgendaList(
|
|||||||
onTopDay: ((LocalDate) -> Unit)?,
|
onTopDay: ((LocalDate) -> Unit)?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val itemDates = agendaItemDates(days)
|
||||||
|
val anchorRow = itemDates.indexOfFirst { it >= anchor }.coerceAtLeast(0)
|
||||||
|
SideEffect { anchorIndex.intValue = anchorRow }
|
||||||
|
// A new anchor opens the list on its row from the first frame, with past
|
||||||
|
// days above. Keyed on the anchor the days arrived with, not the one just
|
||||||
|
// requested: scrolling before the new rows compose leaves the list keyed to
|
||||||
|
// a row that reappears further down, and it follows it there. The first run
|
||||||
|
// may restore a remembered day instead.
|
||||||
|
val pendingRestore = remember { arrayOf(initialTopDate) }
|
||||||
|
DisposableEffect(anchor) {
|
||||||
|
val target = pendingRestore[0]
|
||||||
|
pendingRestore[0] = null
|
||||||
|
val index = target?.let { date -> itemDates.indexOfFirst { it >= date } }
|
||||||
|
?.takeIf { it >= 0 }
|
||||||
|
?: anchorRow
|
||||||
|
listState.requestScrollToItem(index)
|
||||||
|
onDispose {}
|
||||||
|
}
|
||||||
|
// Scrolling up near the first loaded day loads earlier days.
|
||||||
|
val currentLoadEarlier by rememberUpdatedState(onLoadEarlier)
|
||||||
|
val loadOnScrollUp = remember(listState) {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
if (available.y > 0 && listState.firstVisibleItemIndex < LOAD_EARLIER_WITHIN) {
|
||||||
|
currentLoadEarlier()
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (onTopDay != null) {
|
if (onTopDay != null) {
|
||||||
val itemDates by rememberUpdatedState(agendaItemDates(days))
|
val currentItemDates by rememberUpdatedState(itemDates)
|
||||||
val currentOnTopDay by rememberUpdatedState(onTopDay)
|
val currentOnTopDay by rememberUpdatedState(onTopDay)
|
||||||
// The day the list opened on is where the view already is; only a
|
// The day the list opened on is where the view already is; only a
|
||||||
// scroll moves it on.
|
// scroll moves it on.
|
||||||
LaunchedEffect(listState) {
|
LaunchedEffect(listState) {
|
||||||
snapshotFlow { listState.firstVisibleItemIndex }
|
snapshotFlow { listState.firstVisibleItemIndex }
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.collect { index -> itemDates.getOrNull(index)?.let(currentOnTopDay) }
|
.collect { index -> currentItemDates.getOrNull(index)?.let(currentOnTopDay) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier.nestedScroll(loadOnScrollUp),
|
||||||
state = listState,
|
state = listState,
|
||||||
// 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),
|
||||||
@@ -401,7 +469,7 @@ private fun AgendaList(
|
|||||||
day = day.date,
|
day = day.date,
|
||||||
zone = zone,
|
zone = zone,
|
||||||
position = positionOf(index, day.events.size),
|
position = positionOf(index, day.events.size),
|
||||||
dimmed = dimPast && event.hasEnded(now),
|
dimmed = dimPast && event.isPastOn(day.date, now, zone),
|
||||||
modifier = animateItemMotion().morphEvent(ViewMorphKey.event(event, day.date)),
|
modifier = animateItemMotion().morphEvent(ViewMorphKey.event(event, day.date)),
|
||||||
onClick = { onEventClick(event) },
|
onClick = { onEventClick(event) },
|
||||||
)
|
)
|
||||||
@@ -412,6 +480,9 @@ private fun AgendaList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How close to the first loaded row an upward scroll starts loading earlier days. */
|
||||||
|
private const val LOAD_EARLIER_WITHIN = 10
|
||||||
|
|
||||||
/** The day of each item [AgendaList] lays out for [days], in its order. */
|
/** The day of each item [AgendaList] lays out for [days], in its order. */
|
||||||
internal fun agendaItemDates(days: List<AgendaDay>): List<LocalDate> =
|
internal fun agendaItemDates(days: List<AgendaDay>): List<LocalDate> =
|
||||||
days.flatMap { day ->
|
days.flatMap { day ->
|
||||||
|
|||||||
@@ -93,22 +93,21 @@ fun groupAgendaDays(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure [today] surfaces as the first agenda day even when it carries no
|
* Ensure [today] surfaces in the agenda even when it carries no (remaining)
|
||||||
* (remaining) events, by prepending an empty-event [AgendaDay] the agenda widget
|
* events, by inserting an empty-event [AgendaDay] in date order that the agenda
|
||||||
* renders as a "nothing left today" placeholder. A no-op unless [enabled], and
|
* renders as a "nothing left today" placeholder. A no-op unless [enabled], and
|
||||||
* when today already has its own day in [days]. Keeps today anchored at the top
|
* when today already has its own day in [days]. Keeps today anchored so a glance
|
||||||
* so a glance tells today's events apart from a future day's (issue #35).
|
* tells today's events apart from a future day's (issue #35).
|
||||||
*/
|
*/
|
||||||
fun anchorTodayIfMissing(
|
fun anchorTodayIfMissing(
|
||||||
days: List<AgendaDay>,
|
days: List<AgendaDay>,
|
||||||
today: LocalDate,
|
today: LocalDate,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
): List<AgendaDay> =
|
): List<AgendaDay> {
|
||||||
if (enabled && days.none { it.date == today }) {
|
if (!enabled || days.any { it.date == today }) return days
|
||||||
listOf(AgendaDay(today, emptyList())) + days
|
val at = days.indexOfFirst { it.date > today }.let { if (it < 0) days.size else it }
|
||||||
} else {
|
return days.take(at) + AgendaDay(today, emptyList()) + days.drop(at)
|
||||||
days
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State for the Agenda view: a flat, forward-looking list of upcoming events
|
* State for the Agenda view: a flat, forward-looking list of upcoming events
|
||||||
@@ -118,8 +117,10 @@ sealed interface AgendaUiState {
|
|||||||
data object Loading : AgendaUiState
|
data object Loading : AgendaUiState
|
||||||
data class Failure(val reason: FailureReason) : AgendaUiState
|
data class Failure(val reason: FailureReason) : AgendaUiState
|
||||||
data class Success(
|
data class Success(
|
||||||
/** First day of the loaded window (today, or a jumped-to date). */
|
/** The day the agenda opens on (today, or a jumped-to date). */
|
||||||
val anchor: LocalDate,
|
val anchor: LocalDate,
|
||||||
|
/** First loaded day, before [anchor] once past days are loaded (#367). */
|
||||||
|
val windowStart: LocalDate,
|
||||||
val today: LocalDate,
|
val today: LocalDate,
|
||||||
val days: List<AgendaDay>,
|
val days: List<AgendaDay>,
|
||||||
/** The range currently in effect — the saved default or a session override. */
|
/** The range currently in effect — the saved default or a session override. */
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.catch
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.datetime.DateTimeUnit
|
import kotlinx.datetime.DateTimeUnit
|
||||||
@@ -31,6 +32,7 @@ import kotlinx.datetime.LocalDate
|
|||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.atStartOfDayIn
|
import kotlinx.datetime.atStartOfDayIn
|
||||||
import kotlinx.datetime.atTime
|
import kotlinx.datetime.atTime
|
||||||
|
import kotlinx.datetime.minus
|
||||||
import kotlinx.datetime.plus
|
import kotlinx.datetime.plus
|
||||||
import kotlinx.datetime.toInstant
|
import kotlinx.datetime.toInstant
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
@@ -88,8 +90,16 @@ class AgendaViewModel @Inject constructor(
|
|||||||
private val todayDate: LocalDate
|
private val todayDate: LocalDate
|
||||||
get() = currentDate()
|
get() = currentDate()
|
||||||
|
|
||||||
private val _anchor = MutableStateFlow(todayDate)
|
// The anchor and how many days before it are loaded, moved together so a
|
||||||
val anchor: StateFlow<LocalDate> = _anchor
|
// jump never queries a window that pairs the new anchor with the old span.
|
||||||
|
private val _window = MutableStateFlow(AgendaWindow(todayDate, PAST_CHUNK_DAYS))
|
||||||
|
val anchor: StateFlow<LocalDate> = _window
|
||||||
|
.map { it.anchor }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, todayDate)
|
||||||
|
|
||||||
|
/** The day last scrolled to the top, so a return to the agenda lands back on it. */
|
||||||
|
var lastTopDate: LocalDate? = null
|
||||||
|
private set
|
||||||
|
|
||||||
private var lastToday = todayDate
|
private var lastToday = todayDate
|
||||||
|
|
||||||
@@ -101,7 +111,9 @@ class AgendaViewModel @Inject constructor(
|
|||||||
.onEach { date ->
|
.onEach { date ->
|
||||||
val old = lastToday
|
val old = lastToday
|
||||||
lastToday = date
|
lastToday = date
|
||||||
if (old != date && _anchor.value == old) _anchor.value = date
|
if (old != date && _window.value.anchor == old) {
|
||||||
|
_window.value = _window.value.copy(anchor = date)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate)
|
||||||
|
|
||||||
@@ -111,9 +123,17 @@ class AgendaViewModel @Inject constructor(
|
|||||||
private val _rangeOverride = MutableStateFlow<AgendaRange?>(null)
|
private val _rangeOverride = MutableStateFlow<AgendaRange?>(null)
|
||||||
|
|
||||||
val state: StateFlow<AgendaUiState> =
|
val state: StateFlow<AgendaUiState> =
|
||||||
combine(_anchor, agendaSettings, _rangeOverride, weekStart) { anchor, settings, override, weekStart ->
|
combine(
|
||||||
|
_window,
|
||||||
|
agendaSettings,
|
||||||
|
_rangeOverride,
|
||||||
|
weekStart,
|
||||||
|
settingsPrefs.pastEventDisplay,
|
||||||
|
) { window, settings, override, weekStart, pastDisplay ->
|
||||||
AgendaParams(
|
AgendaParams(
|
||||||
anchor = anchor,
|
anchor = window.anchor,
|
||||||
|
// Hidden past events leave nothing to scroll back to (#367).
|
||||||
|
pastDays = if (pastDisplay == PastEventDisplay.HIDE) 0 else window.pastDays,
|
||||||
range = override ?: settings.range,
|
range = override ?: settings.range,
|
||||||
rangeIsOverride = override != null && override != settings.range,
|
rangeIsOverride = override != null && override != settings.range,
|
||||||
weekStart = weekStart,
|
weekStart = weekStart,
|
||||||
@@ -122,8 +142,8 @@ class AgendaViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
.flatMapLatest { params ->
|
.flatMapLatest { params ->
|
||||||
val window = agendaRange(
|
val window = agendaRange(
|
||||||
params.anchor,
|
params.windowStart,
|
||||||
params.range.dayCount(params.anchor, params.weekStart) - 1,
|
params.pastDays + params.range.dayCount(params.anchor, params.weekStart) - 1,
|
||||||
zone,
|
zone,
|
||||||
)
|
)
|
||||||
combine(
|
combine(
|
||||||
@@ -143,12 +163,30 @@ class AgendaViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun goToToday() {
|
fun goToToday() {
|
||||||
_anchor.value = todayDate
|
goToDate(todayDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Jump the agenda window to start on a specific date (drawer jump-to-date). */
|
/** Jump the agenda window to start on a specific date (drawer jump-to-date). */
|
||||||
fun goToDate(date: LocalDate) {
|
fun goToDate(date: LocalDate) {
|
||||||
_anchor.value = date
|
if (_window.value.anchor == date) return
|
||||||
|
_window.value = AgendaWindow(date, PAST_CHUNK_DAYS)
|
||||||
|
lastTopDate = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the window [PAST_CHUNK_DAYS] further back (#367). Ignored while the
|
||||||
|
* previous extension is still loading, so a scroll that keeps asking only
|
||||||
|
* moves one chunk at a time.
|
||||||
|
*/
|
||||||
|
fun loadEarlier() {
|
||||||
|
val loaded = (state.value as? AgendaUiState.Success)?.windowStart ?: return
|
||||||
|
val window = _window.value
|
||||||
|
if (loaded != window.start) return
|
||||||
|
_window.value = window.copy(pastDays = window.pastDays + PAST_CHUNK_DAYS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rememberTopDay(date: LocalDate) {
|
||||||
|
lastTopDate = date
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Temporarily override the agenda range for this session (the bottom-left pill). */
|
/** Temporarily override the agenda range for this session (the bottom-left pill). */
|
||||||
@@ -161,13 +199,20 @@ class AgendaViewModel @Inject constructor(
|
|||||||
val showBar: Boolean,
|
val showBar: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class AgendaWindow(val anchor: LocalDate, val pastDays: Int) {
|
||||||
|
val start: LocalDate get() = anchor.minus(pastDays, DateTimeUnit.DAY)
|
||||||
|
}
|
||||||
|
|
||||||
private data class AgendaParams(
|
private data class AgendaParams(
|
||||||
val anchor: LocalDate,
|
val anchor: LocalDate,
|
||||||
|
val pastDays: Int,
|
||||||
val range: AgendaRange,
|
val range: AgendaRange,
|
||||||
val rangeIsOverride: Boolean,
|
val rangeIsOverride: Boolean,
|
||||||
val weekStart: DayOfWeek,
|
val weekStart: DayOfWeek,
|
||||||
val showRangeBar: Boolean,
|
val showRangeBar: Boolean,
|
||||||
)
|
) {
|
||||||
|
val windowStart: LocalDate get() = anchor.minus(pastDays, DateTimeUnit.DAY)
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildState(
|
private fun buildState(
|
||||||
params: AgendaParams,
|
params: AgendaParams,
|
||||||
@@ -181,9 +226,10 @@ class AgendaViewModel @Inject constructor(
|
|||||||
params.range.dayCount(anchor, params.weekStart) - 1,
|
params.range.dayCount(anchor, params.weekStart) - 1,
|
||||||
DateTimeUnit.DAY,
|
DateTimeUnit.DAY,
|
||||||
)
|
)
|
||||||
val days = groupAgendaDays(anchor, rangeEnd, instances, zone)
|
val days = groupAgendaDays(params.windowStart, rangeEnd, instances, zone)
|
||||||
return AgendaUiState.Success(
|
return AgendaUiState.Success(
|
||||||
anchor = anchor,
|
anchor = anchor,
|
||||||
|
windowStart = params.windowStart,
|
||||||
today = today,
|
today = today,
|
||||||
days = days,
|
days = days,
|
||||||
range = params.range,
|
range = params.range,
|
||||||
@@ -195,6 +241,9 @@ class AgendaViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** How many days each scroll back into the past loads (#367). */
|
||||||
|
internal const val PAST_CHUNK_DAYS = 30
|
||||||
|
|
||||||
/** Inclusive instant range from the start of [anchor] through [days] days ahead. */
|
/** Inclusive instant range from the start of [anchor] through [days] days ahead. */
|
||||||
internal fun agendaRange(anchor: LocalDate, days: Int, zone: TimeZone): ClosedRange<Instant> {
|
internal fun agendaRange(anchor: LocalDate, days: Int, zone: TimeZone): ClosedRange<Instant> {
|
||||||
val from = anchor.atStartOfDayIn(zone)
|
val from = anchor.atStartOfDayIn(zone)
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ private fun sampleAgendaState(today: LocalDate, zone: TimeZone): AgendaUiState.S
|
|||||||
val windowEnd = today.plus(PREVIEW_WINDOW_DAYS, DateTimeUnit.DAY)
|
val windowEnd = today.plus(PREVIEW_WINDOW_DAYS, DateTimeUnit.DAY)
|
||||||
return AgendaUiState.Success(
|
return AgendaUiState.Success(
|
||||||
anchor = today,
|
anchor = today,
|
||||||
|
windowStart = today,
|
||||||
today = today,
|
today = today,
|
||||||
days = groupAgendaDays(today, windowEnd, sampleAgendaEvents(today, zone), zone),
|
days = groupAgendaDays(today, windowEnd, sampleAgendaEvents(today, zone), zone),
|
||||||
range = AgendaRange.Custom(PREVIEW_WINDOW_DAYS),
|
range = AgendaRange.Custom(PREVIEW_WINDOW_DAYS),
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ import androidx.compose.ui.text.AnnotatedString
|
|||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import de.jeanlucmakiola.calendula.domain.hasEnded
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,6 +21,17 @@ import kotlin.time.Instant
|
|||||||
*/
|
*/
|
||||||
val LocalDimCutoff = compositionLocalOf<Instant?> { null }
|
val LocalDimCutoff = compositionLocalOf<Instant?> { null }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether [this] counts as past where it's drawn on [day]: once it has ended,
|
||||||
|
* or on any day before the one [now] falls in, so a multi-day event still
|
||||||
|
* running doesn't stay undimmed on the days already behind it.
|
||||||
|
*/
|
||||||
|
fun EventInstance.isPastOn(
|
||||||
|
day: LocalDate,
|
||||||
|
now: Instant,
|
||||||
|
zone: TimeZone = TimeZone.currentSystemDefault(),
|
||||||
|
): Boolean = hasEnded(now) || day < now.toLocalDateTime(zone).date
|
||||||
|
|
||||||
/** Opacity applied to a completed/past event chip when it is dimmed. */
|
/** Opacity applied to a completed/past event chip when it is dimmed. */
|
||||||
const val EventDimAlpha = 0.4f
|
const val EventDimAlpha = 0.4f
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.isPastOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
import de.jeanlucmakiola.calendula.ui.common.inlineTimeLabel
|
import de.jeanlucmakiola.calendula.ui.common.inlineTimeLabel
|
||||||
@@ -1940,7 +1941,7 @@ private fun SplitDots(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.morphBounds(MonthMorphKey.Event(date, event.instanceId))
|
.morphBounds(MonthMorphKey.Event(date, event.instanceId))
|
||||||
.size(SPLIT_DOT_SIZE)
|
.size(SPLIT_DOT_SIZE)
|
||||||
.alpha(if (dimCutoff != null && event.hasEnded(dimCutoff)) EventDimAlpha else 1f)
|
.alpha(if (dimCutoff != null && event.isPastOn(date, dimCutoff)) EventDimAlpha else 1f)
|
||||||
.background(eventAccent(event.color, dark, soften), CircleShape),
|
.background(eventAccent(event.color, dark, soften), CircleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1954,7 +1955,7 @@ private fun SplitDots(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.morphBounds(MonthMorphKey.Overflow(date))
|
.morphBounds(MonthMorphKey.Overflow(date))
|
||||||
.alpha(if (allEnded(hidden, dimCutoff)) EventDimAlpha else 1f),
|
.alpha(if (allEnded(hidden, date, dimCutoff)) EventDimAlpha else 1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1997,7 +1998,7 @@ private fun SplitMonthAgendaPane(
|
|||||||
day = day,
|
day = day,
|
||||||
zone = state.zone,
|
zone = state.zone,
|
||||||
position = positionOf(index, events.size),
|
position = positionOf(index, events.size),
|
||||||
dimmed = dimCutoff != null && event.hasEnded(dimCutoff),
|
dimmed = dimCutoff != null && event.isPastOn(day, dimCutoff),
|
||||||
modifier = animateItemMotion().morphEvent(ViewMorphKey.event(event, day)),
|
modifier = animateItemMotion().morphEvent(ViewMorphKey.event(event, day)),
|
||||||
onClick = { onEventClick(event) },
|
onClick = { onEventClick(event) },
|
||||||
)
|
)
|
||||||
@@ -2066,7 +2067,7 @@ internal fun SplitDayPane(
|
|||||||
day = day,
|
day = day,
|
||||||
zone = zone,
|
zone = zone,
|
||||||
position = positionOf(index, dayEvents.size),
|
position = positionOf(index, dayEvents.size),
|
||||||
dimmed = dimCutoff != null && event.hasEnded(dimCutoff),
|
dimmed = dimCutoff != null && event.isPastOn(day, dimCutoff),
|
||||||
onClick = { onEventClick(event) },
|
onClick = { onEventClick(event) },
|
||||||
// An event added to or removed from the day you are
|
// An event added to or removed from the day you are
|
||||||
// already looking at moves its neighbours rather
|
// already looking at moves its neighbours rather
|
||||||
@@ -2542,6 +2543,7 @@ private fun MonthWeekRow(
|
|||||||
}
|
}
|
||||||
OverflowDots(
|
OverflowDots(
|
||||||
events = hiddenEvents,
|
events = hiddenEvents,
|
||||||
|
day = d,
|
||||||
total = hidden,
|
total = hidden,
|
||||||
dark = dark,
|
dark = dark,
|
||||||
rowHeight = overflowRow,
|
rowHeight = overflowRow,
|
||||||
@@ -2866,7 +2868,9 @@ private fun MonthBar(
|
|||||||
) {
|
) {
|
||||||
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 dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
// A bar is one piece, so it dims once the last day it covers has passed.
|
||||||
|
val dimmed = dimCutoff != null &&
|
||||||
|
(event.hasEnded(dimCutoff) || days?.let { event.isPastOn(it.last(), dimCutoff) } == true)
|
||||||
val paint = eventPaint(event, dark)
|
val paint = eventPaint(event, dark)
|
||||||
// The same title/secondary ink pairing the week and day blocks use, with
|
// The same title/secondary ink pairing the week and day blocks use, with
|
||||||
// the time on the quieter half.
|
// the time on the quieter half.
|
||||||
@@ -2919,6 +2923,7 @@ private fun MonthBar(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun OverflowDots(
|
private fun OverflowDots(
|
||||||
events: List<EventInstance>,
|
events: List<EventInstance>,
|
||||||
|
day: LocalDate,
|
||||||
total: Int,
|
total: Int,
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
rowHeight: Dp,
|
rowHeight: Dp,
|
||||||
@@ -2937,7 +2942,7 @@ private fun OverflowDots(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(OVERFLOW_DOT_SIZE)
|
.size(OVERFLOW_DOT_SIZE)
|
||||||
.alpha(if (allEnded(byColor.getValue(argb), dimCutoff)) EventDimAlpha else 1f)
|
.alpha(if (allEnded(byColor.getValue(argb), day, dimCutoff)) EventDimAlpha else 1f)
|
||||||
.background(eventAccent(argb, dark, soften), CircleShape),
|
.background(eventAccent(argb, dark, soften), CircleShape),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2947,15 +2952,15 @@ private fun OverflowDots(
|
|||||||
text = "+$extra",
|
text = "+$extra",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.alpha(if (allEnded(events, dimCutoff)) EventDimAlpha else 1f),
|
modifier = Modifier.alpha(if (allEnded(events, day, dimCutoff)) EventDimAlpha else 1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** True when dimming is on and every one of [events] is already over. */
|
/** True when dimming is on and every one of [events] is already over on [day]. */
|
||||||
private fun allEnded(events: List<EventInstance>, dimCutoff: Instant?): Boolean =
|
private fun allEnded(events: List<EventInstance>, day: LocalDate, dimCutoff: Instant?): Boolean =
|
||||||
dimCutoff != null && events.isNotEmpty() && events.all { it.hasEnded(dimCutoff) }
|
dimCutoff != null && events.isNotEmpty() && events.all { it.isPastOn(day, dimCutoff) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MonthGridLoading() {
|
private fun MonthGridLoading() {
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ 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.LocalChipGround
|
import de.jeanlucmakiola.calendula.ui.common.LocalChipGround
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventPaint
|
import de.jeanlucmakiola.calendula.ui.common.eventPaint
|
||||||
import de.jeanlucmakiola.calendula.ui.common.eventSurface
|
import de.jeanlucmakiola.calendula.ui.common.eventSurface
|
||||||
@@ -135,6 +134,7 @@ import de.jeanlucmakiola.calendula.ui.common.rememberEventDragSource
|
|||||||
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
|
||||||
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
import de.jeanlucmakiola.calendula.ui.common.startInstant
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||||
|
import de.jeanlucmakiola.calendula.ui.common.isPastOn
|
||||||
import de.jeanlucmakiola.calendula.ui.common.EventChipShape
|
import de.jeanlucmakiola.calendula.ui.common.EventChipShape
|
||||||
import de.jeanlucmakiola.calendula.ui.common.trimmedLines
|
import de.jeanlucmakiola.calendula.ui.common.trimmedLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
import de.jeanlucmakiola.calendula.ui.common.NowLine
|
||||||
@@ -1076,6 +1076,7 @@ private fun AllDayStrip(
|
|||||||
key(bar.event.eventId, bar.event.start) {
|
key(bar.event.eventId, bar.event.start) {
|
||||||
AllDayBar(
|
AllDayBar(
|
||||||
event = bar.event,
|
event = bar.event,
|
||||||
|
lastDay = bar.toRange.endInclusive,
|
||||||
dark = dark,
|
dark = dark,
|
||||||
onClick = { onEventClick(bar.event) },
|
onClick = { onEventClick(bar.event) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1111,13 +1112,15 @@ private fun AllDayStrip(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AllDayBar(
|
private fun AllDayBar(
|
||||||
event: EventInstance,
|
event: EventInstance,
|
||||||
|
lastDay: LocalDate,
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
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 dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && event.hasEnded(dimCutoff)
|
// A bar is one piece, so it dims once the last day it covers here has passed.
|
||||||
|
val dimmed = dimCutoff != null && event.isPastOn(lastDay, dimCutoff)
|
||||||
val paint = eventPaint(event, dark)
|
val paint = eventPaint(event, dark)
|
||||||
Box(
|
Box(
|
||||||
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
modifier = (if (dimmed) modifier.alpha(EventDimAlpha) else modifier)
|
||||||
@@ -1474,7 +1477,7 @@ private fun EventBlock(
|
|||||||
if (showTime) metrics.timeLine else 0.dp
|
if (showTime) metrics.timeLine else 0.dp
|
||||||
val timeMaxLines = if (showTime) blockTimeLines(shownTime, textWidth, spare) else 1
|
val timeMaxLines = if (showTime) blockTimeLines(shownTime, textWidth, spare) else 1
|
||||||
val dimCutoff = LocalDimCutoff.current
|
val dimCutoff = LocalDimCutoff.current
|
||||||
val dimmed = dimCutoff != null && block.event.hasEnded(dimCutoff)
|
val dimmed = dimCutoff != null && block.event.isPastOn(date, dimCutoff)
|
||||||
val zone = remember { TimeZone.currentSystemDefault() }
|
val zone = remember { TimeZone.currentSystemDefault() }
|
||||||
val moveAction = eventMoveAction(block.event)
|
val moveAction = eventMoveAction(block.event)
|
||||||
val draggable = eventDragAllowed(block.event)
|
val draggable = eventDragAllowed(block.event)
|
||||||
|
|||||||
@@ -302,7 +302,7 @@
|
|||||||
<string name="agenda_range_month">٣٠ يومًا القادمة</string>
|
<string name="agenda_range_month">٣٠ يومًا القادمة</string>
|
||||||
<string name="settings_month_view_style">نمط عرض الشهر</string>
|
<string name="settings_month_view_style">نمط عرض الشهر</string>
|
||||||
<string name="month_style_paged">صفحات</string>
|
<string name="month_style_paged">صفحات</string>
|
||||||
<string name="month_style_paged_summary">الشهر الواحد يملأ الشاشة. اسحب لليسار أو لليمين لتغيير الشهر، أو اسحب للأعلى لعرض جدول الشهر.</string>
|
<string name="month_style_paged_summary">الشهر الواحد يملأ الشاشة. اسحب لليسار أو لليمين لتغيير الشهر.</string>
|
||||||
<string name="month_style_split">تقسيم</string>
|
<string name="month_style_split">تقسيم</string>
|
||||||
<string name="settings_color_unsupported">السماح بالألوان في التقويمات غير المدعومة</string>
|
<string name="settings_color_unsupported">السماح بالألوان في التقويمات غير المدعومة</string>
|
||||||
<string name="settings_color_unsupported_hint">بعض التقويمات (مثل البعض من CalDAV) لا تنشر أي مجموعة ألوان؛ لون حدث مخصص قد يزال أو يستبدل في مزامنته التالية. هذا قيد على تلك التقويمات، وليس شيئًا يمكن لـ Calendula إصلاحه.</string>
|
<string name="settings_color_unsupported_hint">بعض التقويمات (مثل البعض من CalDAV) لا تنشر أي مجموعة ألوان؛ لون حدث مخصص قد يزال أو يستبدل في مزامنته التالية. هذا قيد على تلك التقويمات، وليس شيئًا يمكن لـ Calendula إصلاحه.</string>
|
||||||
@@ -732,7 +732,7 @@
|
|||||||
<string name="event_detail_title">العنوان</string>
|
<string name="event_detail_title">العنوان</string>
|
||||||
<string name="settings_licences">تراخيص مفتوحة المصدر</string>
|
<string name="settings_licences">تراخيص مفتوحة المصدر</string>
|
||||||
<string name="settings_licences_subtitle">المشاريع التي بنيت Calendula عليها</string>
|
<string name="settings_licences_subtitle">المشاريع التي بنيت Calendula عليها</string>
|
||||||
<string name="licences_title">التراخيص مفتوحة المصدر</string>
|
<string name="licences_title">تراخيص مفتوحة المصدر</string>
|
||||||
<string name="licences_intro">يتضمن Calendula العمل أدناه. اضغط على أي إدخال للحصول على شفرة المصدر الخاصة به.</string>
|
<string name="licences_intro">يتضمن Calendula العمل أدناه. اضغط على أي إدخال للحصول على شفرة المصدر الخاصة به.</string>
|
||||||
<string name="licences_footer">يتم تضمين كل مشروع هنا دون تعديل، بموجب الترخيص الموضح.</string>
|
<string name="licences_footer">يتم تضمين كل مشروع هنا دون تعديل، بموجب الترخيص الموضح.</string>
|
||||||
<string name="state_failure_permission_body">يقرأ Calendula أحداثك مباشرةً من تقويم النظام، لذلك فأنه يحتاج للوصول إلى التقويم لعرض أي شيء.</string>
|
<string name="state_failure_permission_body">يقرأ Calendula أحداثك مباشرةً من تقويم النظام، لذلك فأنه يحتاج للوصول إلى التقويم لعرض أي شيء.</string>
|
||||||
@@ -778,30 +778,4 @@
|
|||||||
<string name="color_name_amber">أصفر كهرماني</string>
|
<string name="color_name_amber">أصفر كهرماني</string>
|
||||||
<string name="calendars_restore_unavailable">لا تقويم يمكنه استيعاب الأحداث المستوردة بعد.</string>
|
<string name="calendars_restore_unavailable">لا تقويم يمكنه استيعاب الأحداث المستوردة بعد.</string>
|
||||||
<string name="calendars_description_label">الوصف</string>
|
<string name="calendars_description_label">الوصف</string>
|
||||||
<string name="app_name_calendar_alias">التقويم</string>
|
|
||||||
<plurals name="view_multi_day">
|
|
||||||
<item quantity="zero">%d يوم</item>
|
|
||||||
<item quantity="one">%d يوم</item>
|
|
||||||
<item quantity="two">%d يومان</item>
|
|
||||||
<item quantity="few">%d أيام</item>
|
|
||||||
<item quantity="many">%d يوم</item>
|
|
||||||
<item quantity="other">%d يوم</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="settings_visible_hours_start">من</string>
|
|
||||||
<string name="settings_visible_hours_end">حتى</string>
|
|
||||||
<string name="settings_multi_day_count">عرض الأيام المتعدّدة</string>
|
|
||||||
<string name="settings_multi_day_count_hint">كم عدد الأيام التي يعرضها طريقة العرض متعدد الأيام جنبًا إلى جنب. اليوم يُفتح في العمود الأول.</string>
|
|
||||||
<string name="settings_visible_hours">الساعات الظاهرة</string>
|
|
||||||
<string name="settings_visible_hours_summary">حدّد مدى من الساعات للشاشة عند فتح طريقة عرض الأسبوع، متعددة الأيام أو اليوم</string>
|
|
||||||
<string name="settings_visible_hours_on_summary">%1$s – %2$s يملأ الشاشة عند فتح طريقة عرض</string>
|
|
||||||
<string name="settings_visible_hours_start_hint">الساعة في أعلى الشاشة عند فتح طريقة عرض الأسبوع، متعددة الأيام أو اليوم. لا تزال الساعات السابقة على بُعد تمرير.</string>
|
|
||||||
<string name="settings_visible_hours_end_hint">الساعة في أسفل الشاشة عند فتح طريقة عرض. لا تزال الساعات اللاحقة على بُعد تمرير، و عمليات التكبير بالضّم حتى يتم فتح طريقة العرض مرة أخرى.</string>
|
|
||||||
<string name="month_open_week">فتح الأسبوع %1$d</string>
|
|
||||||
<string name="settings_timeline_scale_by_visible_hours">ضبط بواسطة الساعات الظاهرة</string>
|
|
||||||
<string name="month_split_clear_filter">إظهار جدول الشهر كامل</string>
|
|
||||||
<string name="timeline_earlier_events">أحداث سابقة</string>
|
|
||||||
<string name="timeline_later_events">أحداث لاحقة</string>
|
|
||||||
<string name="reminder_day_today">اليوم</string>
|
|
||||||
<string name="event_detail_open_in_maps">فتح في الخرائط</string>
|
|
||||||
<string name="link_open_failed">لا تطبيق يمكنه فتح هذا الرابط</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -671,5 +671,4 @@
|
|||||||
<string name="state_failure_all_hidden_action">Кіраваньне календарамі</string>
|
<string name="state_failure_all_hidden_action">Кіраваньне календарамі</string>
|
||||||
<string name="state_failure_no_import_target">Ніводны з календароў ня можа прымаць новыя падзеі.</string>
|
<string name="state_failure_no_import_target">Ніводны з календароў ня можа прымаць новыя падзеі.</string>
|
||||||
<string name="state_failure_no_import_target_body">Календары адключаныя, у рэжыме «толькі чытаньне», знаходзяцца пад кіраваньнем іншай праграмы, або не сынхранізуюцца на гэтай прыладзе. Уключэце каляндар, які можа прымаць падзеі, або дадайце лякальны.</string>
|
<string name="state_failure_no_import_target_body">Календары адключаныя, у рэжыме «толькі чытаньне», знаходзяцца пад кіраваньнем іншай праграмы, або не сынхранізуюцца на гэтай прыладзе. Уключэце каляндар, які можа прымаць падзеі, або дадайце лякальны.</string>
|
||||||
<string name="app_name_calendar_alias">Каляндар</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -719,7 +719,7 @@
|
|||||||
<string name="onboarding_hidden_title">Vaše kalendáře jsou vypnuté</string>
|
<string name="onboarding_hidden_title">Vaše kalendáře jsou vypnuté</string>
|
||||||
<string name="onboarding_hidden_body">Calendula zobrazuje kalendáře, které má tento telefon zapnuté, což není momentálně žádný. Zapněte si kalendáře, které chcete vidět – vaše ostatní aplikace kalendáře je pak také zobrazí.</string>
|
<string name="onboarding_hidden_body">Calendula zobrazuje kalendáře, které má tento telefon zapnuté, což není momentálně žádný. Zapněte si kalendáře, které chcete vidět – vaše ostatní aplikace kalendáře je pak také zobrazí.</string>
|
||||||
<string name="onboarding_hidden_switch_all_button">Zapnout všechny kalendáře</string>
|
<string name="onboarding_hidden_switch_all_button">Zapnout všechny kalendáře</string>
|
||||||
<string name="onboarding_hidden_skip_button">Později</string>
|
<string name="onboarding_hidden_skip_button">Teď ne</string>
|
||||||
<string name="onboarding_hidden_failed">To se nepodařilo zapnout. V Nastavení → Kalendáře jsou stejné přepínače.</string>
|
<string name="onboarding_hidden_failed">To se nepodařilo zapnout. V Nastavení → Kalendáře jsou stejné přepínače.</string>
|
||||||
<string name="onboarding_calendars_create_failed">Kalendář se nepodařilo vytvořit. Úložiště kalendáře ve vašem telefonu jej odmítlo vytvořit.</string>
|
<string name="onboarding_calendars_create_failed">Kalendář se nepodařilo vytvořit. Úložiště kalendáře ve vašem telefonu jej odmítlo vytvořit.</string>
|
||||||
<string name="calendars_description_label">Popis</string>
|
<string name="calendars_description_label">Popis</string>
|
||||||
@@ -736,21 +736,4 @@
|
|||||||
<string name="color_name_purple">Purpurová</string>
|
<string name="color_name_purple">Purpurová</string>
|
||||||
<string name="color_name_pink">Růžová</string>
|
<string name="color_name_pink">Růžová</string>
|
||||||
<string name="calendars_restore_unavailable">Žádný z kalendářů zatím nedokáže importované události přijmout.</string>
|
<string name="calendars_restore_unavailable">Žádný z kalendářů zatím nedokáže importované události přijmout.</string>
|
||||||
<string name="app_name_calendar_alias">Kalendář</string>
|
|
||||||
<string name="month_open_week">Otevřít týden %1$d</string>
|
|
||||||
<string name="month_split_clear_filter">Zobrazit celou měsíční agendu</string>
|
|
||||||
<string name="timeline_earlier_events">Dřívější události</string>
|
|
||||||
<string name="timeline_later_events">Pozdější události</string>
|
|
||||||
<string name="event_detail_open_in_maps">Otevřít na mapě</string>
|
|
||||||
<string name="link_open_failed">Žádná aplikace nemůže tento odkaz otevřít</string>
|
|
||||||
<string name="reminder_day_today">Dnes</string>
|
|
||||||
<plurals name="view_multi_day">
|
|
||||||
<item quantity="one">%d den</item>
|
|
||||||
<item quantity="few">%d dny</item>
|
|
||||||
<item quantity="many">%d dnů</item>
|
|
||||||
<item quantity="other">%d dnů</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="settings_multi_day_count">Vícedenní zobrazení</string>
|
|
||||||
<string name="settings_multi_day_count_hint">Počet dní zobrazujících se ve vícedenním přehledu vedle sebe. Dnešek se otevírá v prvním sloupci.</string>
|
|
||||||
<string name="settings_timeline_scale_by_visible_hours">Nastaveno v části Viditelné hodiny</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -715,16 +715,4 @@
|
|||||||
<item quantity="one">%d Termin konnte nicht hinzugefügt werden.</item>
|
<item quantity="one">%d Termin konnte nicht hinzugefügt werden.</item>
|
||||||
<item quantity="other">%d Termine konnten nicht hinzugefügt werden.</item>
|
<item quantity="other">%d Termine konnten nicht hinzugefügt werden.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="app_name_calendar_alias">Kalender</string>
|
|
||||||
<string name="timeline_earlier_events">Frühere Termine</string>
|
|
||||||
<string name="timeline_later_events">Spätere Termine</string>
|
|
||||||
<string name="link_open_failed">Keine App kann diesen Link öffnen</string>
|
|
||||||
<string name="reminder_day_today">Heute</string>
|
|
||||||
<plurals name="view_multi_day">
|
|
||||||
<item quantity="one">%d Tag</item>
|
|
||||||
<item quantity="other">%d Tage</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="settings_visible_hours">Sichtbare Stunden</string>
|
|
||||||
<string name="settings_visible_hours_start">Von</string>
|
|
||||||
<string name="settings_visible_hours_end">Bis</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -672,5 +672,4 @@
|
|||||||
<item quantity="many">Les événements %d n\'ont pas pu être ajoutés.</item>
|
<item quantity="many">Les événements %d n\'ont pas pu être ajoutés.</item>
|
||||||
<item quantity="other">Les événements %d n\'ont pas pu être ajoutés.</item>
|
<item quantity="other">Les événements %d n\'ont pas pu être ajoutés.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="app_name_calendar_alias">Calendrier</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
<string name="settings_time_format_auto">Automatico</string>
|
<string name="settings_time_format_auto">Automatico</string>
|
||||||
<string name="settings_time_format_12h">12 ore</string>
|
<string name="settings_time_format_12h">12 ore</string>
|
||||||
<string name="settings_time_format_24h">24 ore</string>
|
<string name="settings_time_format_24h">24 ore</string>
|
||||||
<string name="settings_hour_lines">Grglia oraria</string>
|
<string name="settings_hour_lines">Righe per le ore</string>
|
||||||
<string name="settings_hour_lines_summary">Mostra una riga separatrice per ogni ora nella vista Settimana e Giorno</string>
|
<string name="settings_hour_lines_summary">Mostra una riga separatrice per ogni ora nella vista Settimana e Giorno</string>
|
||||||
<string name="settings_dim_completed">Attenua gli eventi completati</string>
|
<string name="settings_dim_completed">Attenua gli eventi completati</string>
|
||||||
<string name="settings_dim_completed_summary">Attenua gli eventi conclusi nella vista Mese e Settimana</string>
|
<string name="settings_dim_completed_summary">Attenua gli eventi conclusi nella vista Mese e Settimana</string>
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
<string name="settings_default_reminder">Promemoria per eventi standard</string>
|
<string name="settings_default_reminder">Promemoria per eventi standard</string>
|
||||||
<string name="settings_notifications_subtitle">Promemoria</string>
|
<string name="settings_notifications_subtitle">Promemoria</string>
|
||||||
<string name="shortcut_new_event_short">Nuovo evento</string>
|
<string name="shortcut_new_event_short">Nuovo evento</string>
|
||||||
<string name="event_edit_managed_hint">Gestito da “%1$s” — titolo, data e ripetizioni sono sincronizzate dai tuoi contatti. Promemoria e note sono modificabili dall\'utente.</string>
|
<string name="event_edit_managed_hint">Gestito da “%1$s” - titolo, data e ripetizioni sono sincronizzate dai tuoi contatti. Promemoria e note sono modificabili dall\'utente.</string>
|
||||||
<string name="settings_font_headings">Carattere del titolo</string>
|
<string name="settings_font_headings">Carattere del titolo</string>
|
||||||
<string name="settings_font_body">Carattere del corpo</string>
|
<string name="settings_font_body">Carattere del corpo</string>
|
||||||
<string name="settings_font_system">Lingua di sistema</string>
|
<string name="settings_font_system">Lingua di sistema</string>
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
<string name="event_move_undone">Ripianificazione annullata</string>
|
<string name="event_move_undone">Ripianificazione annullata</string>
|
||||||
<string name="event_move_failed">Impossibile ripianificare l\'evento</string>
|
<string name="event_move_failed">Impossibile ripianificare l\'evento</string>
|
||||||
<string name="event_move_write_denied">Calendula ha bisogno dei permessi di scrittura per ripianificare gli eventi</string>
|
<string name="event_move_write_denied">Calendula ha bisogno dei permessi di scrittura per ripianificare gli eventi</string>
|
||||||
<string name="event_move_gone">Questo evento è stato eliminato</string>
|
<string name="event_move_gone">Questo evento è stato eliminato.</string>
|
||||||
<string name="event_move_blocked_series_end">Non è possibile ripianificare un evento oltre la data di fine della sua serie</string>
|
<string name="event_move_blocked_series_end">Non è possibile ripianificare un evento oltre la data di fine della sua serie</string>
|
||||||
<string name="event_edit_timezone_device">Fuso orario del dispositivo</string>
|
<string name="event_edit_timezone_device">Fuso orario del dispositivo</string>
|
||||||
<string name="event_edit_timezone_device_summary">Segue il fuso orario in cui ti trovi</string>
|
<string name="event_edit_timezone_device_summary">Segue il fuso orario in cui ti trovi</string>
|
||||||
@@ -657,83 +657,4 @@
|
|||||||
<string name="import_done_failed_label">Non aggiunto</string>
|
<string name="import_done_failed_label">Non aggiunto</string>
|
||||||
<string name="import_done_failed_note">Alcuni eventi non possono essere aggiunti e sono stati esclusi.</string>
|
<string name="import_done_failed_note">Alcuni eventi non possono essere aggiunti e sono stati esclusi.</string>
|
||||||
<string name="import_done_stopped_title">Importazione interrotta</string>
|
<string name="import_done_stopped_title">Importazione interrotta</string>
|
||||||
<string name="app_name_calendar_alias">Calendario</string>
|
|
||||||
<string name="state_failure_permission_body">Calendula legge i tuoi eventi direttamente dal calendario di sistema, per questo ha bisogno dell\'accesso al calendario per poterli visualizzare.</string>
|
|
||||||
<string name="state_failure_no_calendars_body">Aggiungi un account a questo dispositivo e i relativi calendari verranno visualizzati qui.</string>
|
|
||||||
<string name="state_failure_all_hidden">Tutti i tuoi calendari sono disattivati.</string>
|
|
||||||
<string name="state_failure_all_hidden_body">Riattiva almeno un calendario per visualizzarne gli eventi.</string>
|
|
||||||
<string name="state_failure_no_import_target">Nessun calendario può accettare nuovi eventi.</string>
|
|
||||||
<string name="state_failure_no_import_target_body">I calendari attivi sono di sola lettura, gestiti da un\'altra app o non sincronizzati con questo dispositivo. Attiva un calendario in cui è possibile inserire eventi oppure aggiungine uno locale.</string>
|
|
||||||
<string name="state_failure_event_not_found">Questo evento non è più disponibile.</string>
|
|
||||||
<string name="state_failure_event_not_found_body">Potrebbe essere stato cancellato oppure spostato in un calendario disattivato.</string>
|
|
||||||
<string name="state_failure_provider_body">Il calendario di sistema non ha risposto. Riprova a breve.</string>
|
|
||||||
<string name="timeline_earlier_events">Eventi precedenti</string>
|
|
||||||
<string name="timeline_later_events">Eventi successivi</string>
|
|
||||||
<string name="month_open_week">Settimana aperta %1$d</string>
|
|
||||||
<string name="month_split_clear_filter">Visualizza il calendario completo del mese</string>
|
|
||||||
<string name="event_detail_open_in_maps">Apri su Maps</string>
|
|
||||||
<string name="link_open_failed">Nessuna app è in grado di aprire questo link</string>
|
|
||||||
<string name="calendar_unnamed">(Senza nome)</string>
|
|
||||||
<string name="onboarding_calendars_title">Un posto dove inserire i tuoi eventi</string>
|
|
||||||
<string name="onboarding_calendars_body">Questo dispositivo non ha ancora un calendario. Calendula scrive sui calendari già presenti sul telefono — ne serve almeno uno.</string>
|
|
||||||
<string name="onboarding_calendars_local_button">Crea un calendario su questo dispositivo</string>
|
|
||||||
<string name="onboarding_calendars_caldav_button">Sincronizzazione con un server CalDAV</string>
|
|
||||||
<string name="onboarding_calendars_skip_button">Non ora</string>
|
|
||||||
<string name="onboarding_calendars_benefit_local_title">Rimane su questo telefono</string>
|
|
||||||
<string name="onboarding_calendars_benefit_local_body">Il calendario del dispositivo non si sincronizza con nessun altro dispositivo, quindi esegui un backup se gli eventi sono importanti.</string>
|
|
||||||
<string name="onboarding_calendars_benefit_sync_title">Oppure sincronizza da un account</string>
|
|
||||||
<string name="onboarding_calendars_benefit_sync_body">Calendula mostra quali account sono già sincronizzati. I server CalDAV necessitano di DAVx⁵ per eseguire la sincronizzazione.</string>
|
|
||||||
<string name="onboarding_calendars_created_title">“%1$s” è pronto</string>
|
|
||||||
<string name="onboarding_calendars_created_body">Si trova su questo dispositivo e gli eventi nuovi vengono inseriti lì, a meno che non si scelga un altro calendario.</string>
|
|
||||||
<string name="onboarding_calendars_created_more_title">C\'è spazio per altro</string>
|
|
||||||
<string name="onboarding_calendars_created_more_body">Da Impostazioni → Calendari puoi crearne quanti ne vuoi e rinominare o cambiare il colore di quelli già presenti.</string>
|
|
||||||
<string name="onboarding_calendars_created_continue">Continua</string>
|
|
||||||
<string name="onboarding_hidden_title">I vostri calendari sono disattivati</string>
|
|
||||||
<string name="onboarding_hidden_body">Calendula mostra i calendari attivi su questo telefono e, al momento, non ce n’è nessuno. Attiva quelli che desideri visualizzare: anche le altre app di calendario li mostreranno nuovamente.</string>
|
|
||||||
<string name="onboarding_hidden_switch_all_button">Attivali tutti</string>
|
|
||||||
<string name="onboarding_hidden_skip_button">Non ora</string>
|
|
||||||
<string name="onboarding_hidden_failed">Non sono riuscito ad attivarlo. In Impostazioni → Calendari ci sono gli stessi pulsanti di attivazione.</string>
|
|
||||||
<string name="onboarding_calendars_create_failed">Impossibile creare il calendario. La memoria del calendario del tuo telefono non lo ha accettato.</string>
|
|
||||||
<string name="reminder_day_today">Oggi</string>
|
|
||||||
<plurals name="view_multi_day">
|
|
||||||
<item quantity="one">Giorno</item>
|
|
||||||
<item quantity="many">Giorni</item>
|
|
||||||
<item quantity="other">Giorno</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="settings_multi_day_count">Visualizzazione su più giorni</string>
|
|
||||||
<string name="settings_multi_day_count_hint">La vista più giorni mostra, affiancati, i giorni. La data odierna è visualizzata nella prima colonna.</string>
|
|
||||||
<string name="settings_timeline_scale_by_visible_hours">Imposta in base alle ore di visibilità</string>
|
|
||||||
<string name="settings_visible_hours">Orari di apertura</string>
|
|
||||||
<string name="settings_visible_hours_summary">Adatta la gamma di ore alla schermata all\'apertura della visualizzazione settimanale, plurigiornaliera o giornaliera</string>
|
|
||||||
<string name="settings_visible_hours_on_summary">%1$s – %2$s riempie lo schermo all’apertura di una vista</string>
|
|
||||||
<string name="settings_visible_hours_start">Da</string>
|
|
||||||
<string name="settings_visible_hours_end">Fino a</string>
|
|
||||||
<string name="settings_visible_hours_start_hint">L\'ora visualizzata nella parte superiore dello schermo quando si apre la vista settimanale, plurigiornaliera o giornaliera. Le ore precedenti sono comunque accessibili scorrendo la schermata.</string>
|
|
||||||
<string name="settings_visible_hours_end_hint">L\'ora visualizzata nella parte inferiore dello schermo quando si apre una schermata. Le ore successive sono ancora raggiungibili scorrendo lo schermo, mentre pizzicando lo schermo si esegue lo zoom fino a quando la schermata non viene riaperta.</string>
|
|
||||||
<string name="settings_licences">Licenze open source</string>
|
|
||||||
<string name="settings_licences_subtitle">I progetti su cui si basa Calendula</string>
|
|
||||||
<string name="calendars_description_label">Descrizione</string>
|
|
||||||
<string name="color_name_mauve">Rosa malva</string>
|
|
||||||
<string name="color_name_red">Rosso</string>
|
|
||||||
<string name="color_name_orange">Arancione</string>
|
|
||||||
<string name="color_name_amber">Ambra</string>
|
|
||||||
<string name="color_name_olive">Oliva</string>
|
|
||||||
<string name="color_name_green">Verde</string>
|
|
||||||
<string name="color_name_teal">Verde acqua</string>
|
|
||||||
<string name="color_name_cyan">Ciano</string>
|
|
||||||
<string name="color_name_blue">Blu</string>
|
|
||||||
<string name="color_name_indigo">Indaco</string>
|
|
||||||
<string name="color_name_purple">Viola</string>
|
|
||||||
<string name="color_name_pink">Rosa</string>
|
|
||||||
<string name="calendars_restore_unavailable">Al momento nessun calendario è in grado di gestire gli eventi importati.</string>
|
|
||||||
<plurals name="import_done_stopped">
|
|
||||||
<item quantity="one">Il calendario ha smesso di accettare eventi a metà dell\'operazione. L\'evento %d presente nel file non è stato aggiunto.</item>
|
|
||||||
<item quantity="many">Il calendari hanno smesso di accettare eventi a metà dell\'operazione. Gli eventi %d presenti nel file non sono stati aggiunti.</item>
|
|
||||||
<item quantity="other">Il calendario ha smesso di accettare eventi a metà dell\'operazione. L\'evento %d presente nel file non è stato aggiunto.</item>
|
|
||||||
</plurals>
|
|
||||||
<plurals name="import_done_failed">
|
|
||||||
<item quantity="one">Non è stato possibile aggiungere l\'evento %d.</item>
|
|
||||||
<item quantity="many">Non è stato possibile aggiungere gli eventi %d.</item>
|
|
||||||
<item quantity="other">Non è stato possibile aggiungere l\'evento %d.</item>
|
|
||||||
</plurals>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -377,7 +377,7 @@
|
|||||||
<string name="settings_today_toolbar">Knop Vandaag in werkbalk</string>
|
<string name="settings_today_toolbar">Knop Vandaag in werkbalk</string>
|
||||||
<string name="settings_today_toolbar_summary">Een knop om naar vandaag te gaan tonen in de werkbalk, in plaats van een zwevende knop</string>
|
<string name="settings_today_toolbar_summary">Een knop om naar vandaag te gaan tonen in de werkbalk, in plaats van een zwevende knop</string>
|
||||||
<string name="settings_app_name">Appnaam</string>
|
<string name="settings_app_name">Appnaam</string>
|
||||||
<string name="settings_app_name_summary">Calendula als “Agenda” tonen in je launcher. Alleen de naam in de launcher verandert; het pictogram kan na het wisselen op een andere plek komen te staan.</string>
|
<string name="settings_app_name_summary">Calendula als “Calendar” tonen in je launcher. Alleen de naam in de launcher verandert; het pictogram kan na het wisselen op een andere plek komen te staan.</string>
|
||||||
<string name="settings_time_format">Tijdnotatie</string>
|
<string name="settings_time_format">Tijdnotatie</string>
|
||||||
<string name="settings_time_format_auto">Automatisch</string>
|
<string name="settings_time_format_auto">Automatisch</string>
|
||||||
<string name="settings_time_format_12h">12-uursnotatie (2:00 PM)</string>
|
<string name="settings_time_format_12h">12-uursnotatie (2:00 PM)</string>
|
||||||
@@ -441,7 +441,7 @@
|
|||||||
<string name="settings_month_header">Maandweergave</string>
|
<string name="settings_month_header">Maandweergave</string>
|
||||||
<string name="settings_month_view_style">Stijl van maandweergave</string>
|
<string name="settings_month_view_style">Stijl van maandweergave</string>
|
||||||
<string name="month_style_paged">Pagina\'s</string>
|
<string name="month_style_paged">Pagina\'s</string>
|
||||||
<string name="month_style_paged_summary">Eén maand vult het scherm. Veeg naar links of rechts om van maand te wisselen, of veeg omhoog om de agenda van deze maand te tonen.</string>
|
<string name="month_style_paged_summary">Eén maand vult het scherm. Veeg naar links of rechts om van maand te wisselen.</string>
|
||||||
<string name="month_style_continuous">Doorlopende maanden</string>
|
<string name="month_style_continuous">Doorlopende maanden</string>
|
||||||
<string name="month_style_continuous_summary">Elke maand staat onder een eigen kop, met wat ruimte tot de volgende maand.</string>
|
<string name="month_style_continuous_summary">Elke maand staat onder een eigen kop, met wat ruimte tot de volgende maand.</string>
|
||||||
<string name="month_style_dense">Naadloze weken</string>
|
<string name="month_style_dense">Naadloze weken</string>
|
||||||
@@ -694,26 +694,4 @@
|
|||||||
<string name="special_dates_default_title_birthday">Verjaardag van {name} ({year})</string>
|
<string name="special_dates_default_title_birthday">Verjaardag van {name} ({year})</string>
|
||||||
<string name="special_dates_default_title_anniversary">Jubileum van {name} ({year})</string>
|
<string name="special_dates_default_title_anniversary">Jubileum van {name} ({year})</string>
|
||||||
<string name="special_dates_default_title_custom">{name}</string>
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
<string name="app_name_calendar_alias">Agenda</string>
|
|
||||||
<string name="month_open_week">Week %1$d openen</string>
|
|
||||||
<string name="month_split_clear_filter">Agenda van de hele maand tonen</string>
|
|
||||||
<string name="timeline_earlier_events">Eerdere gebeurtenissen</string>
|
|
||||||
<string name="timeline_later_events">Latere gebeurtenissen</string>
|
|
||||||
<string name="event_detail_open_in_maps">Op kaart tonen</string>
|
|
||||||
<string name="link_open_failed">Er is geen app die deze link kan openen</string>
|
|
||||||
<string name="reminder_day_today">Vandaag</string>
|
|
||||||
<plurals name="view_multi_day">
|
|
||||||
<item quantity="one">%d dag</item>
|
|
||||||
<item quantity="other">%d dagen</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="settings_multi_day_count">Meerdaagse weergave</string>
|
|
||||||
<string name="settings_multi_day_count_hint">Hoeveel dagen de meerdaagse weergave naast elkaar toont. Vandaag staat in de eerste kolom.</string>
|
|
||||||
<string name="settings_timeline_scale_by_visible_hours">Ingesteld via Zichtbare uren</string>
|
|
||||||
<string name="settings_visible_hours">Zichtbare uren</string>
|
|
||||||
<string name="settings_visible_hours_summary">Een reeks uren passend op het scherm tonen bij het openen van de week-, meerdaagse of dagweergave</string>
|
|
||||||
<string name="settings_visible_hours_on_summary">%1$s – %2$s vult het scherm bij het openen van een weergave</string>
|
|
||||||
<string name="settings_visible_hours_start">Van</string>
|
|
||||||
<string name="settings_visible_hours_end">Tot</string>
|
|
||||||
<string name="settings_visible_hours_start_hint">Het uur bovenaan het scherm bij het openen van de week-, meerdaagse of dagweergave. Eerdere uren blijven bereikbaar door te scrollen.</string>
|
|
||||||
<string name="settings_visible_hours_end_hint">Het uur onderaan het scherm bij het openen van een weergave. Latere uren blijven bereikbaar door te scrollen, en in- of uitzoomen met twee vingers geldt totdat de weergave opnieuw wordt geopend.</string>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class AnchorTodayTest {
|
|||||||
|
|
||||||
private val today = LocalDate(2026, 6, 17)
|
private val today = LocalDate(2026, 6, 17)
|
||||||
private val tomorrow = LocalDate(2026, 6, 18)
|
private val tomorrow = LocalDate(2026, 6, 18)
|
||||||
|
private val yesterday = LocalDate(2026, 6, 16)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `disabled leaves the days untouched even when today is absent`() {
|
fun `disabled leaves the days untouched even when today is absent`() {
|
||||||
@@ -36,4 +37,18 @@ class AnchorTodayTest {
|
|||||||
val days = listOf(AgendaDay(today, emptyList()), AgendaDay(tomorrow, emptyList()))
|
val days = listOf(AgendaDay(today, emptyList()), AgendaDay(tomorrow, emptyList()))
|
||||||
assertThat(anchorTodayIfMissing(days, today, enabled = true)).isEqualTo(days)
|
assertThat(anchorTodayIfMissing(days, today, enabled = true)).isEqualTo(days)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `enabled slots today in after loaded past days`() {
|
||||||
|
val days = listOf(AgendaDay(yesterday, emptyList()), AgendaDay(tomorrow, emptyList()))
|
||||||
|
val result = anchorTodayIfMissing(days, today, enabled = true)
|
||||||
|
assertThat(result.map { it.date }).containsExactly(yesterday, today, tomorrow).inOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `enabled appends today after only past days`() {
|
||||||
|
val days = listOf(AgendaDay(yesterday, emptyList()))
|
||||||
|
val result = anchorTodayIfMissing(days, today, enabled = true)
|
||||||
|
assertThat(result.map { it.date }).containsExactly(yesterday, today).inOrder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.ui.common
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
class IsPastOnTest {
|
||||||
|
|
||||||
|
private val zone = TimeZone.of("Europe/Berlin")
|
||||||
|
private val today = LocalDate(2026, 10, 8)
|
||||||
|
private val now = Instant.parse("2026-10-08T12:00:00Z")
|
||||||
|
|
||||||
|
// All-day Oct 6–9, still running today.
|
||||||
|
private val summit = EventInstance(
|
||||||
|
instanceId = 1,
|
||||||
|
eventId = 1,
|
||||||
|
calendarId = 1,
|
||||||
|
title = "Summit",
|
||||||
|
start = Instant.parse("2026-10-06T00:00:00Z"),
|
||||||
|
end = Instant.parse("2026-10-10T00:00:00Z"),
|
||||||
|
isAllDay = true,
|
||||||
|
color = 0,
|
||||||
|
location = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an ongoing multi-day event is past on the days already behind`() {
|
||||||
|
assertThat(summit.isPastOn(LocalDate(2026, 10, 7), now, zone)).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an ongoing multi-day event is not past today or later`() {
|
||||||
|
assertThat(summit.isPastOn(today, now, zone)).isFalse()
|
||||||
|
assertThat(summit.isPastOn(LocalDate(2026, 10, 9), now, zone)).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an event that ended earlier today is past`() {
|
||||||
|
val standup = summit.copy(
|
||||||
|
isAllDay = false,
|
||||||
|
start = Instant.parse("2026-10-08T07:00:00Z"),
|
||||||
|
end = Instant.parse("2026-10-08T07:15:00Z"),
|
||||||
|
)
|
||||||
|
assertThat(standup.isPastOn(today, now, zone)).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user