Compare commits
18 Commits
34108b9c50
...
renovate/k
| Author | SHA1 | Date | |
|---|---|---|---|
| 774b950c6c | |||
|
|
5b099714f6 | ||
| 163d821d1f | |||
|
|
fe98b1ed66 | ||
|
|
c0270ab3a0 | ||
|
|
6019d6c0b8 | ||
|
|
19bc5967a4 | ||
|
|
ddab3454bb | ||
|
|
247c9ee6cd | ||
|
|
6be170cae2 | ||
|
|
8133584f1e | ||
|
|
4cdb42afba | ||
|
|
19a4c05e93 | ||
|
|
aba06affbf | ||
|
|
2b132a5b7a | ||
|
|
52ecce03b8 | ||
|
|
0eb76db12d | ||
|
|
d03985eada |
57
CHANGELOG.md
57
CHANGELOG.md
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.19.3] — 2026-08-22
|
||||
|
||||
### Added
|
||||
- **Czech, Japanese and Russian**, all three contributed as community
|
||||
translations through
|
||||
[Calendula's Weblate](https://weblate.dev.jeanlucmakiola.de/projects/calendula/).
|
||||
They are early — Czech covers about a third of the app, Russian a fifth,
|
||||
Japanese has just started — so untranslated parts still show in English until
|
||||
they fill out. Pick any of them under Settings → Language or in Android's
|
||||
per-app language settings. Thanks to James Trivette (Czech), ame0011
|
||||
(Japanese), and chip and Pablo (Russian); help finishing them is very welcome.
|
||||
|
||||
### Changed
|
||||
- **Brazilian Portuguese is now complete**, covering the whole app. Thanks to
|
||||
bkrz for carrying it the rest of the way.
|
||||
|
||||
## [2.19.2] — 2026-08-17
|
||||
|
||||
### Changed
|
||||
- **A meeting you declined is now struck through** wherever it appears — month,
|
||||
week, day, agenda, search and both widgets — and no longer schedules a
|
||||
reminder. Declining an invitation in Google Calendar left the event looking
|
||||
like any other in Calendula, and it still notified you about a meeting you had
|
||||
said no to. It stays visible rather than disappearing: the organiser still
|
||||
expects an answer from you, and the slot is still spoken for ([#180]).
|
||||
|
||||
### Fixed
|
||||
- **Tapping an event in month view opens the event**, not the day it sits on.
|
||||
Every month style is affected — page, rolling, seamless weeks — and until now
|
||||
the only way to reach an event from the month was to open its day first and
|
||||
find it again there. Tapping anywhere else in the cell still opens the day
|
||||
([#187]).
|
||||
- **An edit made to an event now shows the moment you re-open it.** Adding a
|
||||
description to an event and opening it again showed the sheet as it was before
|
||||
the save, because the detail was only re-read when a different occurrence was
|
||||
opened ([#196]).
|
||||
- **The month widget's arrows stopped working after a couple of taps.** The grid
|
||||
was serialised as roughly 740 views, and one update ran to half a megabyte —
|
||||
more than the launcher's buffer takes. The third update overran it, and Android
|
||||
responded by dropping the whole widget host, which killed updates for *every*
|
||||
widget on the home screen, ours and other apps', until the launcher rebound.
|
||||
The grid now draws 192 views, and a resized widget reflows to its new size
|
||||
instead of clipping ([#214]).
|
||||
- Tapping a widget's refresh or month arrows redraws that widget by its own id
|
||||
instead of asking Android to update all of them, which does nothing in a
|
||||
process the tap has just woken from cold ([#18]).
|
||||
- **Jump-to-today in seamless weeks lands on the current week** instead of
|
||||
leaving a sliver of the previous row on screen ([#191]).
|
||||
- Day and week view no longer run their events flush against the right edge
|
||||
([#192]).
|
||||
|
||||
## [2.19.1] — 2026-08-11
|
||||
|
||||
### Added
|
||||
@@ -1414,3 +1465,9 @@ automatically, with zero telemetry and no internet permission.
|
||||
[#123]: https://codeberg.org/jlmakiola/calendula/issues/123
|
||||
[#163]: https://codeberg.org/jlmakiola/calendula/issues/163
|
||||
[#173]: https://codeberg.org/jlmakiola/calendula/issues/173
|
||||
[#180]: https://codeberg.org/jlmakiola/calendula/issues/180
|
||||
[#187]: https://codeberg.org/jlmakiola/calendula/issues/187
|
||||
[#191]: https://codeberg.org/jlmakiola/calendula/issues/191
|
||||
[#192]: https://codeberg.org/jlmakiola/calendula/issues/192
|
||||
[#196]: https://codeberg.org/jlmakiola/calendula/issues/196
|
||||
[#214]: https://codeberg.org/jlmakiola/calendula/issues/214
|
||||
|
||||
@@ -28,8 +28,8 @@ android {
|
||||
// which builds this version and then creates the matching vX.Y.Z tag +
|
||||
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
||||
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
||||
versionCode = 21901
|
||||
versionName = "2.19.1"
|
||||
versionCode = 21903
|
||||
versionName = "2.19.3"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis
|
||||
import android.provider.CalendarContract
|
||||
import android.util.Log
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
|
||||
@@ -38,5 +39,7 @@ internal fun ColumnReader.toEventInstance(): EventInstance? {
|
||||
isAllDay = getInt(InstanceProjection.IDX_ALL_DAY) != 0,
|
||||
color = color,
|
||||
location = getString(InstanceProjection.IDX_LOCATION),
|
||||
isDeclined = getInt(InstanceProjection.IDX_SELF_ATTENDEE_STATUS) ==
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ internal object InstanceProjection {
|
||||
CalendarContract.Instances.EVENT_COLOR,
|
||||
CalendarContract.Instances.CALENDAR_COLOR,
|
||||
CalendarContract.Instances.EVENT_LOCATION,
|
||||
CalendarContract.Instances.SELF_ATTENDEE_STATUS,
|
||||
)
|
||||
|
||||
const val IDX_INSTANCE_ID = 0
|
||||
@@ -64,6 +65,7 @@ internal object InstanceProjection {
|
||||
const val IDX_EVENT_COLOR = 7
|
||||
const val IDX_CALENDAR_COLOR = 8
|
||||
const val IDX_LOCATION = 9
|
||||
const val IDX_SELF_ATTENDEE_STATUS = 10
|
||||
}
|
||||
|
||||
internal object EventDetailProjection {
|
||||
@@ -182,6 +184,7 @@ internal object SearchProjection {
|
||||
CalendarContract.Events.RDATE,
|
||||
// Excerpted, not just filtered on: a hit has to show what it matched.
|
||||
CalendarContract.Events.DESCRIPTION,
|
||||
CalendarContract.Events.SELF_ATTENDEE_STATUS,
|
||||
)
|
||||
|
||||
const val IDX_ID = 0
|
||||
@@ -197,6 +200,7 @@ internal object SearchProjection {
|
||||
const val IDX_RRULE = 10
|
||||
const val IDX_RDATE = 11
|
||||
const val IDX_DESCRIPTION = 12
|
||||
const val IDX_SELF_ATTENDEE_STATUS = 13
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import android.provider.CalendarContract
|
||||
import de.jeanlucmakiola.floret.time.toKotlinInstantFromEpochMillis
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.domain.ics.parseRfc2445DurationMillis
|
||||
@@ -42,5 +43,7 @@ internal fun ColumnReader.toSearchResult(): EventInstance? {
|
||||
location = getString(SearchProjection.IDX_LOCATION),
|
||||
isRecurring = !getString(SearchProjection.IDX_RRULE).isNullOrEmpty() ||
|
||||
!getString(SearchProjection.IDX_RDATE).isNullOrEmpty(),
|
||||
isDeclined = getInt(SearchProjection.IDX_SELF_ATTENDEE_STATUS) ==
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,9 +44,16 @@ class ProviderReminderInstanceSource @Inject constructor(
|
||||
// `visible` is the flag the app's one visibility model writes (#75).
|
||||
// The status clause mirrors CalendarDataSource.instances: NULL means
|
||||
// "normal", so a bare `!= CANCELED` would drop every ordinary event.
|
||||
// An invitation the user declined is answered — it stays on the calendar
|
||||
// struck through, but it plans nothing (#180). NULL again means "no
|
||||
// answer recorded", which is not a "no".
|
||||
val selection = "${CalendarContract.Calendars.VISIBLE} = 1 AND " +
|
||||
"(${CalendarContract.Instances.STATUS} IS NULL OR " +
|
||||
"${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED})"
|
||||
"${CalendarContract.Instances.STATUS} != " +
|
||||
"${CalendarContract.Events.STATUS_CANCELED}) AND " +
|
||||
"(${CalendarContract.Instances.SELF_ATTENDEE_STATUS} IS NULL OR " +
|
||||
"${CalendarContract.Instances.SELF_ATTENDEE_STATUS} != " +
|
||||
"${CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED})"
|
||||
return context.contentResolver.query(
|
||||
uri, OCCURRENCE_PROJECTION, selection, null, null,
|
||||
)?.use { c ->
|
||||
|
||||
@@ -67,6 +67,13 @@ data class EventInstance(
|
||||
* Instances query already yields one row per occurrence.
|
||||
*/
|
||||
val isRecurring: Boolean = false,
|
||||
/**
|
||||
* This device user answered "no" to the invitation
|
||||
* (`Events.SELF_ATTENDEE_STATUS`). The event stays on the calendar — it is
|
||||
* still an appointment someone expects an answer about — but every surface
|
||||
* strikes it through, and it plans no reminders (#180).
|
||||
*/
|
||||
val isDeclined: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,11 +24,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.jeanlucmakiola.calendula.R
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.declinedTitle
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
||||
@@ -129,8 +131,8 @@ internal fun AgendaEventRow(
|
||||
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
|
||||
GroupedRow(
|
||||
modifier = if (dimmed) modifier.alpha(EventDimAlpha) else modifier,
|
||||
title = title,
|
||||
summary = agendaTimeSummary(event, day, zone),
|
||||
title = declinedTitle(title, event.isDeclined),
|
||||
summary = AnnotatedString(agendaTimeSummary(event, day, zone)),
|
||||
position = position,
|
||||
minHeight = 64.dp,
|
||||
leading = {
|
||||
|
||||
@@ -36,6 +36,14 @@ val GUTTER_WIDTH = 48.dp
|
||||
*/
|
||||
val GUTTER_CONTENT_START_INSET = 8.dp
|
||||
|
||||
/**
|
||||
* End inset for everything that lines up with the day columns — the week's day
|
||||
* header, the all-day strip and the timeline itself. Without it the last column
|
||||
* runs flush into the screen edge while the gutter gives the other side room
|
||||
* (#192); 8dp mirrors [GUTTER_CONTENT_START_INSET].
|
||||
*/
|
||||
val TIMELINE_CONTENT_END_INSET = 8.dp
|
||||
|
||||
private val BADGE_HEIGHT = 20.dp
|
||||
|
||||
/** How far the fixed hour labels recede while a block is being dragged. */
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package de.jeanlucmakiola.calendula.ui.common
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
@@ -14,3 +18,30 @@ val LocalDimCutoff = compositionLocalOf<Instant?> { null }
|
||||
|
||||
/** Opacity applied to a completed/past event chip when it is dimmed. */
|
||||
const val EventDimAlpha = 0.4f
|
||||
|
||||
/**
|
||||
* How a declined invitation's title is struck through (#180) — the mark every
|
||||
* surface uses to say "you answered no", chosen over hiding the event because it
|
||||
* is still something the organiser expects you at. Null for everything else, so
|
||||
* it drops straight into a `Text`'s `textDecoration`.
|
||||
*/
|
||||
fun declinedDecoration(isDeclined: Boolean): TextDecoration? =
|
||||
if (isDeclined) TextDecoration.LineThrough else null
|
||||
|
||||
/** [declinedDecoration] for the rows that take styled text rather than a `String`. */
|
||||
fun declinedTitle(title: String, isDeclined: Boolean): AnnotatedString = if (isDeclined) {
|
||||
AnnotatedString(title, SpanStyle(textDecoration = TextDecoration.LineThrough))
|
||||
} else {
|
||||
AnnotatedString(title)
|
||||
}
|
||||
|
||||
/** [declinedTitle] over already-styled text, e.g. a search hit's marked runs. */
|
||||
fun declinedTitle(title: AnnotatedString, isDeclined: Boolean): AnnotatedString =
|
||||
if (!isDeclined) {
|
||||
title
|
||||
} else {
|
||||
buildAnnotatedString {
|
||||
append(title)
|
||||
addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), 0, title.length)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ import de.jeanlucmakiola.calendula.ui.common.next
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventFill
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventInk
|
||||
import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
|
||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||
import de.jeanlucmakiola.floret.locale.currentLocale
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||
@@ -118,6 +119,7 @@ import de.jeanlucmakiola.calendula.ui.common.rememberTimelinePinchZoom
|
||||
import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
|
||||
import de.jeanlucmakiola.calendula.ui.common.HourGutter
|
||||
import de.jeanlucmakiola.calendula.ui.common.TIMELINE_CONTENT_END_INSET
|
||||
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
||||
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
|
||||
import de.jeanlucmakiola.calendula.ui.week.TimedBlock
|
||||
@@ -471,7 +473,11 @@ private fun AllDayStrip(
|
||||
// Height is hoisted + animated so it resizes smoothly; padding sits
|
||||
// inside it so the content area is lanes * row height.
|
||||
.height(height)
|
||||
.padding(vertical = ALL_DAY_VERTICAL_PADDING),
|
||||
.padding(
|
||||
top = ALL_DAY_VERTICAL_PADDING,
|
||||
bottom = ALL_DAY_VERTICAL_PADDING,
|
||||
end = TIMELINE_CONTENT_END_INSET,
|
||||
),
|
||||
) {
|
||||
// Keep the gutter-width offset so the bars line up with the day column.
|
||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||
@@ -525,6 +531,7 @@ private fun AllDayBar(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = eventInk(fill),
|
||||
textDecoration = declinedDecoration(event.isDeclined),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -570,6 +577,7 @@ private fun Timeline(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.verticalScroll(scrollState)
|
||||
.onGloballyPositioned { dragController.geometry.viewport = it },
|
||||
@@ -772,6 +780,7 @@ private fun EventBlock(
|
||||
maxLines = if (showTime) 1 else 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = eventInk(fill, alpha = 0.85f),
|
||||
textDecoration = declinedDecoration(block.event.isDeclined),
|
||||
)
|
||||
}
|
||||
if (showTime) {
|
||||
@@ -792,7 +801,12 @@ private fun DayLoading() {
|
||||
// Same scale resolution as the loaded timeline, so the skeleton's column
|
||||
// doesn't resize the moment the real day arrives.
|
||||
val totalHeight = scale.hourHeight(maxHeight) * 24
|
||||
Row(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -48,9 +48,14 @@ class EventDetailViewModel @Inject constructor(
|
||||
) : ViewModel() {
|
||||
|
||||
private val _target = MutableStateFlow<Target?>(null)
|
||||
// Bumped by retry() to re-run the load for the same target.
|
||||
// Bumped by retry() and by re-opening the target already shown, to re-run
|
||||
// the load without changing the target.
|
||||
private val _reload = MutableStateFlow(0)
|
||||
|
||||
// Last target whose content is already on screen; a re-read of it skips the
|
||||
// Loading skeleton so the sheet doesn't blank out between two identical reads.
|
||||
private var loadedTarget: Target? = null
|
||||
|
||||
private val _deleteState = MutableStateFlow<DeleteUiState>(DeleteUiState.Idle)
|
||||
val deleteState: StateFlow<DeleteUiState> = _deleteState.asStateFlow()
|
||||
|
||||
@@ -58,11 +63,14 @@ class EventDetailViewModel @Inject constructor(
|
||||
combine(_target, _reload) { target, _ -> target }
|
||||
.flatMapLatest { target ->
|
||||
if (target == null) {
|
||||
loadedTarget = null
|
||||
flowOf<EventDetailUiState>(EventDetailUiState.Loading)
|
||||
} else {
|
||||
flow {
|
||||
emit(EventDetailUiState.Loading)
|
||||
emit(loadDetail(target))
|
||||
if (loadedTarget != target) emit(EventDetailUiState.Loading)
|
||||
val loaded = loadDetail(target)
|
||||
loadedTarget = target.takeIf { loaded is EventDetailUiState.Success }
|
||||
emit(loaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,9 +86,15 @@ class EventDetailViewModel @Inject constructor(
|
||||
* the occurrence's own times (from `CalendarContract.Instances`); they
|
||||
* override the series DTSTART/DTEND so recurring events show the correct
|
||||
* date instead of the first occurrence.
|
||||
*
|
||||
* Re-opening the *same* occurrence always re-reads it. The view model
|
||||
* outlives the sheet, and a `StateFlow` conflates an identical value away,
|
||||
* so assigning the target alone would leave an edit that changed no time —
|
||||
* adding a description, say (#196) — showing the state from before the save.
|
||||
*/
|
||||
fun open(eventId: Long, beginMillis: Long, endMillis: Long) {
|
||||
_target.value = Target(eventId, beginMillis, endMillis)
|
||||
val target = Target(eventId, beginMillis, endMillis)
|
||||
if (_target.value == target) _reload.value += 1 else _target.value = target
|
||||
}
|
||||
|
||||
/** Re-run the current load after a failure. */
|
||||
|
||||
@@ -19,6 +19,8 @@ import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -111,6 +113,7 @@ import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -138,6 +141,7 @@ import de.jeanlucmakiola.calendula.ui.common.CALENDAR_SWIPE_THRESHOLD
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalSoftenColors
|
||||
import de.jeanlucmakiola.calendula.ui.common.eventAccent
|
||||
@@ -439,6 +443,7 @@ fun MonthScreen(
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onRetry = jumpToToday,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
} else if (viewStyle == MonthViewStyle.Split) {
|
||||
SplitMonthContent(
|
||||
@@ -463,6 +468,7 @@ fun MonthScreen(
|
||||
onSwipePrev = goPrev,
|
||||
onRetry = jumpToToday,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -573,6 +579,7 @@ private fun MonthContent(
|
||||
onSwipePrev: () -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
) {
|
||||
val slideSpec = rememberCalendarSlideSpec()
|
||||
val fadeSpec = rememberCalendarFadeSpec()
|
||||
@@ -599,6 +606,7 @@ private fun MonthContent(
|
||||
state = s,
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -617,6 +625,7 @@ private fun ContinuousMonthContent(
|
||||
showWeekNumbers: Boolean,
|
||||
onRetry: () -> Unit,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
// The scrolling styles get their own skeleton rather than the paged
|
||||
@@ -631,6 +640,7 @@ private fun ContinuousMonthContent(
|
||||
listState = listState,
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
} else {
|
||||
ContinuousMonthGrid(
|
||||
@@ -638,6 +648,7 @@ private fun ContinuousMonthContent(
|
||||
listState = listState,
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -736,7 +747,8 @@ private val CELL_SHAPE = RoundedCornerShape(CELL_CORNER)
|
||||
|
||||
/** Width of the split style's selected-day outline. */
|
||||
private val SPLIT_SELECTION_STROKE = 1.5.dp
|
||||
private const val MAX_EVENT_ROWS = 3
|
||||
/** Lanes of bars/pills a day cell draws before the rest become overflow dots. */
|
||||
internal const val MAX_EVENT_ROWS = 3
|
||||
|
||||
/**
|
||||
* Row height in the continuous grid. The paged grid divides the viewport between
|
||||
@@ -754,11 +766,15 @@ private val CONTINUOUS_ROW_HEIGHT = 112.dp
|
||||
*/
|
||||
private val CONTINUOUS_MONTH_GAP = 20.dp
|
||||
|
||||
/** Gap between the weekday header and the seamless stream's first week row. */
|
||||
private val DENSE_HEADER_GAP = 4.dp
|
||||
|
||||
@Composable
|
||||
internal fun MonthGrid(
|
||||
state: MonthUiState.Success,
|
||||
showWeekNumbers: Boolean,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
/** See [MonthWeekRow]'s `selected`: an anchor for the morph, never a mark. */
|
||||
selected: LocalDate? = null,
|
||||
) {
|
||||
@@ -779,6 +795,7 @@ internal fun MonthGrid(
|
||||
inMonth = { it.month == month.month && it.year == month.year },
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
selected = selected,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -809,6 +826,7 @@ internal fun ContinuousMonthGrid(
|
||||
listState: LazyListState,
|
||||
showWeekNumbers: Boolean,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val monthCount = remember { continuousMonthCount() }
|
||||
@@ -836,6 +854,7 @@ internal fun ContinuousMonthGrid(
|
||||
today = state.today,
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -855,6 +874,7 @@ private fun ContinuousMonthBlock(
|
||||
today: LocalDate,
|
||||
showWeekNumbers: Boolean,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
) {
|
||||
val rowCount = remember(month, weekStart) { weekRowsInMonth(month, weekStart) }
|
||||
Column(
|
||||
@@ -877,6 +897,7 @@ private fun ContinuousMonthBlock(
|
||||
blankOutside = true,
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(CONTINUOUS_ROW_HEIGHT),
|
||||
@@ -935,6 +956,7 @@ internal fun DenseMonthGrid(
|
||||
listState: LazyListState,
|
||||
showWeekNumbers: Boolean,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val weekCount = remember(state.weekStart) { continuousWeekCount(state.weekStart) }
|
||||
@@ -942,10 +964,15 @@ internal fun DenseMonthGrid(
|
||||
state = listState,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 8.dp),
|
||||
// The gap under the weekday header is a real margin, not content
|
||||
// padding: a lazy list scrolls its rows *through* the before-content
|
||||
// padding, so scrolling to a week landed its row that far down with
|
||||
// the tail of the previous one showing above it (#191).
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(top = DENSE_HEADER_GAP),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
// Bottom inset clears the FAB stack so the last row stays tappable.
|
||||
contentPadding = PaddingValues(top = 4.dp, bottom = 96.dp),
|
||||
contentPadding = PaddingValues(bottom = 96.dp),
|
||||
) {
|
||||
items(count = weekCount, key = { it }) { index ->
|
||||
val week = state.weeksByIndex[index]
|
||||
@@ -960,6 +987,7 @@ internal fun DenseMonthGrid(
|
||||
inMonth = { true },
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onOpenDay,
|
||||
onEventClick = onEventClick,
|
||||
labelMonthOnFirst = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -1187,6 +1215,9 @@ private fun SplitMonthBody(
|
||||
onSelectDay(it)
|
||||
onSetExpanded(false)
|
||||
},
|
||||
// A tapped chip is asking for that event, not for its day, so it
|
||||
// opens the detail from here too rather than collapsing (#187).
|
||||
onEventClick = onEventClick,
|
||||
onCollapse = { onSetExpanded(false) },
|
||||
)
|
||||
} else {
|
||||
@@ -1287,6 +1318,7 @@ private fun SplitMonthExpanded(
|
||||
showWeekNumbers: Boolean,
|
||||
swipeModifier: Modifier,
|
||||
onPickDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
onCollapse: () -> Unit,
|
||||
) {
|
||||
val slideSpec = rememberCalendarSlideSpec()
|
||||
@@ -1309,6 +1341,7 @@ private fun SplitMonthExpanded(
|
||||
state = s,
|
||||
showWeekNumbers = showWeekNumbers,
|
||||
onOpenDay = onPickDay,
|
||||
onEventClick = onEventClick,
|
||||
selected = sel,
|
||||
)
|
||||
}
|
||||
@@ -1789,7 +1822,7 @@ private fun ContinuousMonthSkeleton(dense: Boolean) {
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
} else {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Spacer(Modifier.height(DENSE_HEADER_GAP))
|
||||
}
|
||||
// More rows than a viewport holds; the clip takes the overflow.
|
||||
repeat(6) {
|
||||
@@ -1837,6 +1870,7 @@ private fun MonthWeekRow(
|
||||
inMonth: (LocalDate) -> Boolean,
|
||||
showWeekNumbers: Boolean,
|
||||
onOpenDay: (LocalDate) -> Unit,
|
||||
onEventClick: (EventInstance) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
blankOutside: Boolean = false,
|
||||
labelMonthOnFirst: Boolean = false,
|
||||
@@ -2135,11 +2169,17 @@ private fun MonthWeekRow(
|
||||
}
|
||||
}
|
||||
|
||||
// Tap layer: in month view a tap on any day opens that day. Padded and
|
||||
// Tap layer: a tap on a chip opens that event, anything else opens the
|
||||
// day (#187). The chips take no pointer input of their own — this
|
||||
// layer covers them — so the lane under the finger is resolved
|
||||
// geometrically, exactly as the drag pickup above does it; the down
|
||||
// position is read on the initial pass, which consumes nothing and so
|
||||
// leaves both the click and a pickup in flight untouched. Padded and
|
||||
// clipped to the background pill so the ripple matches it. A blanked
|
||||
// cell isn't part of this month, so it takes no taps either.
|
||||
val downY = remember(week.days.size) { FloatArray(week.days.size) { NO_DOWN_Y } }
|
||||
Row(Modifier.matchParentSize()) {
|
||||
week.days.forEach { d ->
|
||||
week.days.forEachIndexed { col, d ->
|
||||
if (blankOutside && !inMonth(d)) {
|
||||
Spacer(Modifier.weight(1f).fillMaxHeight())
|
||||
} else {
|
||||
@@ -2147,9 +2187,33 @@ private fun MonthWeekRow(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(col) {
|
||||
awaitEachGesture {
|
||||
downY[col] = awaitFirstDown(
|
||||
requireUnconsumed = false,
|
||||
pass = PointerEventPass.Initial,
|
||||
).position.y
|
||||
}
|
||||
}
|
||||
.padding(horizontal = CELL_GAP, vertical = 1.dp)
|
||||
.clip(CELL_SHAPE)
|
||||
.clickable { onOpenDay(d) },
|
||||
.clickable {
|
||||
// Cleared on read: a click with no fresh down
|
||||
// (TalkBack, D-pad) would otherwise resolve the
|
||||
// previous tap's position and reopen its chip.
|
||||
val cellY = downY[col]
|
||||
downY[col] = NO_DOWN_Y
|
||||
val chip = week.chipAtCellY(
|
||||
col = col,
|
||||
cellY = cellY,
|
||||
bandTopInCell = bandTopInCell(
|
||||
cellCoordinates,
|
||||
bandCoordinates,
|
||||
),
|
||||
rowHeightPx = rowHeightPx,
|
||||
)
|
||||
if (chip != null) onEventClick(chip) else onOpenDay(d)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2158,6 +2222,43 @@ private fun MonthWeekRow(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the event band sits below the top of the row's day-column box, or null
|
||||
* while either is unmeasured. Read off the live coordinates rather than summed
|
||||
* from the padding constants, so it can't drift from what the row actually drew.
|
||||
*/
|
||||
private fun bandTopInCell(
|
||||
cell: Array<LayoutCoordinates?>,
|
||||
band: Array<LayoutCoordinates?>,
|
||||
): Float? {
|
||||
val cellTop = cell[0]?.takeIf { it.isAttached }?.positionInRoot()?.y ?: return null
|
||||
val bandTop = band[0]?.takeIf { it.isAttached }?.positionInRoot()?.y ?: return null
|
||||
return bandTop - cellTop
|
||||
}
|
||||
|
||||
/**
|
||||
* Stand-in [cellY] for "no touch down recorded", which resolves to no chip.
|
||||
*/
|
||||
private const val NO_DOWN_Y = Float.NEGATIVE_INFINITY
|
||||
|
||||
/**
|
||||
* The chip at [cellY] in column [col], where [cellY] is measured from the top of
|
||||
* the row's day-column box. Null for a tap above the band (the day number), on an
|
||||
* empty lane, or on the overflow dots — all of which mean "open the day", the
|
||||
* dots included: their point is that the day holds more than fits.
|
||||
*/
|
||||
internal fun MonthWeek.chipAtCellY(
|
||||
col: Int,
|
||||
cellY: Float,
|
||||
bandTopInCell: Float?,
|
||||
rowHeightPx: Float,
|
||||
): EventInstance? {
|
||||
if (bandTopInCell == null || rowHeightPx <= 0f) return null
|
||||
val bandY = cellY - bandTopInCell
|
||||
if (bandY < 0f) return null
|
||||
return chipAt(col, (bandY / rowHeightPx).toInt(), MAX_EVENT_ROWS)
|
||||
}
|
||||
|
||||
/**
|
||||
* The row-level pickup for month chips: resolves which chip the press landed on
|
||||
* from the geometry the row just laid out, and abandons the gesture on empty
|
||||
@@ -2358,6 +2459,7 @@ private fun MonthBar(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = eventInk(fill),
|
||||
textDecoration = declinedDecoration(event.isDeclined),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ internal fun MonthStylePreview(
|
||||
state = sample.month,
|
||||
showWeekNumbers = false,
|
||||
onOpenDay = {},
|
||||
onEventClick = {},
|
||||
)
|
||||
MonthViewStyle.Continuous -> ContinuousMonthGrid(
|
||||
state = sample.continuous,
|
||||
@@ -55,6 +56,7 @@ internal fun MonthStylePreview(
|
||||
),
|
||||
showWeekNumbers = false,
|
||||
onOpenDay = {},
|
||||
onEventClick = {},
|
||||
)
|
||||
MonthViewStyle.Dense -> DenseMonthGrid(
|
||||
state = sample.continuous,
|
||||
@@ -66,6 +68,7 @@ internal fun MonthStylePreview(
|
||||
),
|
||||
showWeekNumbers = false,
|
||||
onOpenDay = {},
|
||||
onEventClick = {},
|
||||
)
|
||||
MonthViewStyle.Split -> {
|
||||
SplitMonthGrid(
|
||||
|
||||
@@ -85,6 +85,7 @@ import de.jeanlucmakiola.calendula.domain.SearchMonth
|
||||
import de.jeanlucmakiola.floret.identity.animateItemMotion
|
||||
import de.jeanlucmakiola.floret.identity.fadeThrough
|
||||
import de.jeanlucmakiola.floret.identity.predictiveBack
|
||||
import de.jeanlucmakiola.calendula.ui.common.declinedTitle
|
||||
import de.jeanlucmakiola.floret.components.GroupedRow
|
||||
import de.jeanlucmakiola.floret.components.InlineTextField
|
||||
import de.jeanlucmakiola.floret.components.Position
|
||||
@@ -519,7 +520,7 @@ private fun SearchResultRow(
|
||||
// Faded like a past event anywhere else in the app — search reaches back
|
||||
// through the whole history.
|
||||
modifier = if (hit.isPast) modifier.alpha(EventDimAlpha) else modifier,
|
||||
title = marked(event.title, hit.titleSpans, highlight),
|
||||
title = declinedTitle(marked(event.title, hit.titleSpans, highlight), event.isDeclined),
|
||||
summary = searchSummary(hit, highlight),
|
||||
position = position,
|
||||
minHeight = 64.dp,
|
||||
|
||||
@@ -90,6 +90,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
|
||||
import de.jeanlucmakiola.calendula.ui.common.CalendarView
|
||||
import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS
|
||||
import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha
|
||||
import de.jeanlucmakiola.calendula.ui.common.declinedDecoration
|
||||
import de.jeanlucmakiola.calendula.ui.common.BlockTimeLabel
|
||||
import de.jeanlucmakiola.calendula.ui.common.animatedBlockPlacement
|
||||
import de.jeanlucmakiola.calendula.ui.common.ghostAlpha
|
||||
@@ -129,6 +130,7 @@ import de.jeanlucmakiola.calendula.ui.common.formatMinuteOfDay
|
||||
import de.jeanlucmakiola.calendula.ui.common.GUTTER_CONTENT_START_INSET
|
||||
import de.jeanlucmakiola.calendula.ui.common.GUTTER_WIDTH
|
||||
import de.jeanlucmakiola.calendula.ui.common.HourGutter
|
||||
import de.jeanlucmakiola.calendula.ui.common.TIMELINE_CONTENT_END_INSET
|
||||
import de.jeanlucmakiola.calendula.ui.common.hourSeparatorLines
|
||||
import de.jeanlucmakiola.calendula.ui.common.tappedMinuteOfDay
|
||||
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
|
||||
@@ -506,7 +508,7 @@ private fun WeekDayHeader(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp, bottom = 8.dp),
|
||||
.padding(top = 4.dp, bottom = 8.dp, end = TIMELINE_CONTENT_END_INSET),
|
||||
) {
|
||||
// Mirror the day-column layout (empty weekday line + spacer) so the
|
||||
// badge lines up vertically with the date numbers. The start inset centres
|
||||
@@ -602,7 +604,11 @@ private fun AllDayStrip(
|
||||
// Height is hoisted + animated so it slides and resizes smoothly;
|
||||
// padding sits inside it so the content area is lanes * row height.
|
||||
.height(height)
|
||||
.padding(vertical = ALL_DAY_VERTICAL_PADDING),
|
||||
.padding(
|
||||
top = ALL_DAY_VERTICAL_PADDING,
|
||||
bottom = ALL_DAY_VERTICAL_PADDING,
|
||||
end = TIMELINE_CONTENT_END_INSET,
|
||||
),
|
||||
) {
|
||||
// Keep the gutter-width offset so the bars line up with the day columns.
|
||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||
@@ -662,6 +668,7 @@ private fun AllDayBar(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = eventInk(fill),
|
||||
textDecoration = declinedDecoration(event.isDeclined),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -708,6 +715,7 @@ private fun Timeline(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.verticalScroll(scrollState)
|
||||
.onGloballyPositioned { dragController.geometry.viewport = it },
|
||||
@@ -940,6 +948,7 @@ private fun EventBlock(
|
||||
maxLines = titleMaxLines,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = eventInk(fill, alpha = 0.85f),
|
||||
textDecoration = declinedDecoration(block.event.isDeclined),
|
||||
)
|
||||
}
|
||||
if (showTime) {
|
||||
@@ -977,7 +986,12 @@ private fun WeekLoading() {
|
||||
// Same scale resolution as the loaded timeline, so the skeleton's
|
||||
// columns don't resize the moment the real week arrives.
|
||||
val totalHeight = scale.hourHeight(maxHeight) * 24
|
||||
Row(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(end = TIMELINE_CONTENT_END_INSET)
|
||||
.verticalScroll(scrollState),
|
||||
) {
|
||||
Spacer(Modifier.width(GUTTER_WIDTH))
|
||||
repeat(7) {
|
||||
Box(
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Build
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.glance.GlanceTheme
|
||||
import androidx.glance.material3.ColorProviders
|
||||
import androidx.glance.text.TextDecoration
|
||||
import de.jeanlucmakiola.calendula.ui.theme.CalendulaDarkFallback
|
||||
import de.jeanlucmakiola.calendula.ui.theme.CalendulaLightFallback
|
||||
|
||||
@@ -34,3 +35,10 @@ fun CalendulaGlanceTheme(content: @Composable () -> Unit) {
|
||||
}
|
||||
GlanceTheme(colors = colors, content = content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Glance's counterpart to the app's `declinedDecoration`: a declined invitation
|
||||
* reads the same on the home screen as it does inside the app (#180).
|
||||
*/
|
||||
fun glanceDeclinedDecoration(isDeclined: Boolean): TextDecoration =
|
||||
if (isDeclined) TextDecoration.LineThrough else TextDecoration.None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.calendula.widget.agenda
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -24,7 +25,6 @@ import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.lazy.LazyColumn
|
||||
import androidx.glance.appwidget.lazy.items
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.state.PreferencesGlanceStateDefinition
|
||||
@@ -74,11 +74,6 @@ import kotlinx.datetime.toLocalDateTime
|
||||
import kotlin.time.Instant
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* "Upcoming" agenda widget — a continuously scrolling list of the next ~30 days
|
||||
* of events grouped under day headers (the Google "Schedule" widget model).
|
||||
* Reuses the app's [groupAgendaDays] grouping so it matches the in-app agenda.
|
||||
*/
|
||||
/**
|
||||
* Per-instance Glance state key holding the agenda range (as [AgendaRange.storageValue]).
|
||||
* The range is read reactively in the composition ([currentState]) so a settings
|
||||
@@ -113,6 +108,11 @@ internal val AGENDA_SHOW_TODAY_STATE_KEY = booleanPreferencesKey("agenda_show_to
|
||||
*/
|
||||
internal val AGENDA_SIZE_KEY = stringPreferencesKey("widget_size")
|
||||
|
||||
/**
|
||||
* "Upcoming" agenda widget — a continuously scrolling list of the next ~30 days
|
||||
* of events grouped under day headers (the Google "Schedule" widget model).
|
||||
* Reuses the app's [groupAgendaDays] grouping so it matches the in-app agenda.
|
||||
*/
|
||||
class AgendaWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
@@ -135,17 +135,23 @@ class AgendaWidget : GlanceAppWidget() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-reads the calendar and redraws the widget (header refresh button). */
|
||||
/**
|
||||
* Redraws the widget (header refresh button). Targets the tapped widget's own id
|
||||
* rather than `updateAll`, whose provider-name lookup is empty in a process a tap
|
||||
* woke from cold — see `ShiftMonthAction` (#18). A cold process re-reads the
|
||||
* calendar in the `provideGlance` preamble; a live session only recomposes from
|
||||
* the snapshot it already has (see [AGENDA_RANGE_KEY]).
|
||||
*/
|
||||
class RefreshAgendaAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
AgendaWidget().updateAll(context.applicationContext)
|
||||
AgendaWidget().update(context.applicationContext, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upper bound on rows handed to the [LazyColumn], so the serialized RemoteViews
|
||||
* stays well inside the binder transaction limit regardless of range and calendar
|
||||
* size (see the [SizeMode.Exact] note above). Far more than fits on screen — a
|
||||
* size (see the [SizeMode.Single] note above). Far more than fits on screen — a
|
||||
* user scrolling a home-screen widget past a hundred rows is not a case worth
|
||||
* risking a failed update for.
|
||||
*/
|
||||
@@ -416,7 +422,11 @@ private fun EventRow(
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = titleColor, fontSize = metrics.eventTitle),
|
||||
style = TextStyle(
|
||||
color = titleColor,
|
||||
fontSize = metrics.eventTitle,
|
||||
textDecoration = glanceDeclinedDecoration(event.isDeclined),
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = eventTimeSummary(context, event, day, is24Hour),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.calendula.widget.month
|
||||
|
||||
import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -25,7 +26,6 @@ import androidx.glance.appwidget.action.actionStartActivity
|
||||
import androidx.glance.appwidget.cornerRadius
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.glance.background
|
||||
import androidx.glance.currentState
|
||||
import androidx.glance.layout.Alignment
|
||||
@@ -33,6 +33,7 @@ import androidx.glance.layout.Box
|
||||
import androidx.glance.layout.Column
|
||||
import androidx.glance.layout.Row
|
||||
import androidx.glance.layout.Spacer
|
||||
import androidx.glance.layout.fillMaxHeight
|
||||
import androidx.glance.layout.fillMaxSize
|
||||
import androidx.glance.layout.fillMaxWidth
|
||||
import androidx.glance.layout.height
|
||||
@@ -97,10 +98,27 @@ private fun yearMonthOf(index: Int): YearMonth =
|
||||
* inter-cell seam, with rounded end caps. The displayed month lives in Glance
|
||||
* state and is read reactively in the composition ([currentState]) so the arrows
|
||||
* move it via plain recomposition, not a (here-unreliable) widget session reload.
|
||||
*
|
||||
* Everything here is written to keep the published `RemoteViews` small. A widget
|
||||
* update reaches the launcher as a *oneway* binder call, and the host's async
|
||||
* buffer is 1 MB for all of it; overrun throws `TransactionTooLargeException` and
|
||||
* the system drops the whole host, freezing every widget on the home screen until
|
||||
* the launcher rebinds. [SizeMode.Exact] serialises the grid once per size the
|
||||
* host reports, so the ceiling arrives at half the view count you would expect: a
|
||||
* grid that emitted a view per cell blew past it after three taps (#214). Every
|
||||
* row here emits only what it fills.
|
||||
*/
|
||||
class MonthWidget : GlanceAppWidget() {
|
||||
|
||||
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||
|
||||
/**
|
||||
* Exact rather than [SizeMode.Single] or [SizeMode.Responsive], both of which
|
||||
* return early from `GlanceAppWidget.resize` — under those the grid is never
|
||||
* recomposed for a new size and a resized widget clips instead of reflowing.
|
||||
* Exact costs one serialised copy per host size, which is affordable now that
|
||||
* a rendering is ~190 views rather than ~740.
|
||||
*/
|
||||
override val sizeMode = SizeMode.Exact
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
@@ -118,7 +136,17 @@ class MonthWidget : GlanceAppWidget() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Step the displayed month by the `delta` action parameter (±1). */
|
||||
/**
|
||||
* Step the displayed month by the `delta` action parameter (±1).
|
||||
*
|
||||
* Redrawn through [GlanceAppWidget.update] with the id the callback was handed,
|
||||
* never `updateAll`. `updateAll` resolves its targets through the provider-name
|
||||
* -> app-widget-id map Glance persists for the *receiver*, and in a process that
|
||||
* has done nothing else yet — the one a tap wakes after a reboot — that lookup
|
||||
* comes back empty and the redraw is dropped: the state write lands, nothing
|
||||
* recomposes, and the arrows read as dead until something else opens the app and
|
||||
* starts a session (#18). The tapped widget's own id needs no lookup.
|
||||
*/
|
||||
class ShiftMonthAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
val delta = parameters[deltaKey] ?: 0
|
||||
@@ -126,7 +154,7 @@ class ShiftMonthAction : ActionCallback {
|
||||
val cur = prefs[MONTH_INDEX_KEY] ?: currentMonthIndex(systemZone())
|
||||
prefs[MONTH_INDEX_KEY] = cur + delta
|
||||
}
|
||||
MonthWidget().updateAll(context.applicationContext)
|
||||
MonthWidget().update(context.applicationContext, glanceId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -134,11 +162,11 @@ class ShiftMonthAction : ActionCallback {
|
||||
}
|
||||
}
|
||||
|
||||
/** Jump the displayed month back to the current month. */
|
||||
/** Jump the displayed month back to the current month. See [ShiftMonthAction]. */
|
||||
class ResetMonthAction : ActionCallback {
|
||||
override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) {
|
||||
updateAppWidgetState(context, glanceId) { prefs -> prefs.remove(MONTH_INDEX_KEY) }
|
||||
MonthWidget().updateAll(context.applicationContext)
|
||||
MonthWidget().update(context.applicationContext, glanceId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +314,31 @@ private fun WeekRow(
|
||||
colW: Dp,
|
||||
modifier: GlanceModifier,
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxWidth()) {
|
||||
// Day numbers.
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
val context = LocalContext.current
|
||||
val hidden = week.hiddenPerDay()
|
||||
Box(modifier = modifier.fillMaxWidth()) {
|
||||
// Day-open targets, one full-height strip per column, sitting *under* the
|
||||
// content: a tap anywhere in a day still opens it, as in the app, without
|
||||
// every cell in the column carrying a click target of its own. The content
|
||||
// above is not clickable, so touches fall through to this layer; only an
|
||||
// event bar opts out to open its own detail.
|
||||
Row(modifier = GlanceModifier.fillMaxSize()) {
|
||||
week.days.forEach { date ->
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(colW)
|
||||
.fillMaxHeight()
|
||||
.clickable(openDayAction(context, date)),
|
||||
) {}
|
||||
}
|
||||
}
|
||||
Column(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
// Day numbers. Fixed height so every week's number row lines up whether
|
||||
// or not it holds today's larger pill.
|
||||
Row(
|
||||
modifier = GlanceModifier.fillMaxWidth().height(DAY_NUMBER_HEIGHT),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
week.days.forEach { date ->
|
||||
DayNumber(
|
||||
date = date,
|
||||
@@ -301,13 +351,37 @@ private fun WeekRow(
|
||||
Spacer(GlanceModifier.height(2.dp))
|
||||
// One lane row per event row. A multi-day span is a single Box spanning
|
||||
// its columns (colW * n) so it's connected with no seam and rounded ends.
|
||||
repeat(MAX_LANES) { lane ->
|
||||
// Lanes past the last one this week fills emit nothing at all.
|
||||
repeat(week.usedLanes()) { lane ->
|
||||
LaneRow(week = week, lane = lane, dark = dark, soften = soften, colW = colW)
|
||||
Spacer(GlanceModifier.height(1.dp))
|
||||
}
|
||||
OverflowRow(week = week, colW = colW)
|
||||
if (hidden.any { it > 0 }) OverflowRow(hidden = hidden, colW = colW)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How many lanes this week actually fills, counting from the top. The grid draws
|
||||
* that many rows instead of a fixed [MAX_LANES]; an empty lane costs three views
|
||||
* per day and most weeks leave the last two empty.
|
||||
*/
|
||||
private fun MonthWeek.usedLanes(): Int {
|
||||
for (lane in MAX_LANES - 1 downTo 0) {
|
||||
val filled = spans.any { it.lane == lane } ||
|
||||
days.indices.any { col -> timedEventAt(this, lane, col, days[col]) != null }
|
||||
if (filled) return lane + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/** Events per day that no lane had room for — the "+N" counts, 0 where all fit. */
|
||||
private fun MonthWeek.hiddenPerDay(): List<Int> = days.mapIndexed { col, date ->
|
||||
val shownSpans = spans.count { col in it.startCol..it.endCol && it.lane < MAX_LANES }
|
||||
val freeSlots = (MAX_LANES - shownSpans).coerceAtLeast(0)
|
||||
val timedShown = minOf(freeSlots, timedByDay[date].orEmpty().size)
|
||||
((countByDay[date] ?: 0) - shownSpans - timedShown).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open [date]'s day view rooted in the month view (so back returns to the month
|
||||
@@ -318,25 +392,14 @@ private fun WeekRow(
|
||||
private fun openDayAction(context: Context, date: LocalDate) =
|
||||
actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month))
|
||||
|
||||
/**
|
||||
* A day's number. Every day but today is a bare centred [Text] carrying the column
|
||||
* width itself — a wrapping Box costs three views, and there are 42 of these.
|
||||
* Today keeps its filled circle, which does need the Box.
|
||||
*/
|
||||
@Composable
|
||||
private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: Dp) {
|
||||
val context = LocalContext.current
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(colW)
|
||||
.height(DAY_NUMBER_HEIGHT)
|
||||
.clickable(openDayAction(context, date)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.size(DAY_NUMBER_HEIGHT)
|
||||
.then(if (isToday) GlanceModifier.cornerRadius(DAY_NUMBER_HEIGHT / 2).background(GlanceTheme.colors.primary) else GlanceModifier),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = date.day.toString(),
|
||||
style = TextStyle(
|
||||
val style = TextStyle(
|
||||
color = when {
|
||||
isToday -> GlanceTheme.colors.onPrimary
|
||||
inMonth -> GlanceTheme.colors.onSurface
|
||||
@@ -344,38 +407,78 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW:
|
||||
},
|
||||
fontSize = 11.sp,
|
||||
fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal,
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
if (!isToday) {
|
||||
Text(text = date.day.toString(), style = style, modifier = GlanceModifier.width(colW))
|
||||
return
|
||||
}
|
||||
Box(modifier = GlanceModifier.width(colW), contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.size(DAY_NUMBER_HEIGHT)
|
||||
.cornerRadius(DAY_NUMBER_HEIGHT / 2)
|
||||
.background(GlanceTheme.colors.primary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = date.day.toString(), style = style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A bar occupying [cols] columns, or — with a null event — that many blank ones. */
|
||||
private data class LaneCell(val event: EventInstance?, val cols: Int)
|
||||
|
||||
/**
|
||||
* A lane split into bars and the gaps between them, with consecutive blank columns
|
||||
* merged into one gap: the week's day-open taps come from the strip underneath, so
|
||||
* a gap needs no per-column view and a mostly-empty lane collapses to a spacer.
|
||||
*/
|
||||
private fun MonthWeek.laneCells(lane: Int): List<LaneCell> {
|
||||
val cells = mutableListOf<LaneCell>()
|
||||
var gap = 0
|
||||
var col = 0
|
||||
fun closeGap() {
|
||||
if (gap > 0) cells += LaneCell(null, gap)
|
||||
gap = 0
|
||||
}
|
||||
while (col < 7) {
|
||||
val span = spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }
|
||||
val timed = if (span == null) timedEventAt(this, lane, col, days[col]) else null
|
||||
when {
|
||||
span != null -> {
|
||||
closeGap()
|
||||
cells += LaneCell(span.event, span.endCol - col + 1)
|
||||
col = span.endCol + 1
|
||||
}
|
||||
timed != null -> {
|
||||
closeGap()
|
||||
cells += LaneCell(timed, 1)
|
||||
col += 1
|
||||
}
|
||||
else -> {
|
||||
gap += 1
|
||||
col += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
closeGap()
|
||||
return cells
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, soften: Boolean, colW: Dp) {
|
||||
val context = LocalContext.current
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
var col = 0
|
||||
while (col < 7) {
|
||||
val span = week.spans.firstOrNull { it.lane == lane && col in it.startCol..it.endCol }
|
||||
if (span != null) {
|
||||
val cols = span.endCol - col + 1
|
||||
SpanBar(event = span.event, dark = dark, soften = soften, width = colW * cols)
|
||||
col = span.endCol + 1
|
||||
week.laneCells(lane).forEach { cell ->
|
||||
if (cell.event == null) {
|
||||
Spacer(GlanceModifier.width(colW * cell.cols).height(LANE_HEIGHT))
|
||||
} else {
|
||||
val timed = timedEventAt(week, lane, col, week.days[col])
|
||||
if (timed != null) {
|
||||
SpanBar(event = timed, dark = dark, soften = soften, width = colW)
|
||||
} else {
|
||||
// Empty lane cell: a tap opens that day, so blank space in a
|
||||
// day column is a day-open target just like the number is.
|
||||
Box(
|
||||
GlanceModifier
|
||||
.width(colW)
|
||||
.height(LANE_HEIGHT)
|
||||
.clickable(openDayAction(context, week.days[col])),
|
||||
) {}
|
||||
}
|
||||
col += 1
|
||||
SpanBar(
|
||||
event = cell.event,
|
||||
dark = dark,
|
||||
soften = soften,
|
||||
width = colW * cell.cols,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -414,41 +517,39 @@ private fun SpanBar(event: EventInstance, dark: Boolean, soften: Boolean, width:
|
||||
Text(
|
||||
text = event.title.ifBlank { context.getString(R.string.event_untitled) },
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = ColorProvider(eventInk(fill)), fontSize = 9.sp),
|
||||
style = TextStyle(
|
||||
color = ColorProvider(eventInk(fill)),
|
||||
fontSize = 9.sp,
|
||||
textDecoration = glanceDeclinedDecoration(event.isDeclined),
|
||||
),
|
||||
modifier = GlanceModifier.padding(horizontal = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The "+N" row, drawn only for weeks that hide something, gaps merged as in a lane. */
|
||||
@Composable
|
||||
private fun OverflowRow(week: MonthWeek, colW: Dp) {
|
||||
val context = LocalContext.current
|
||||
private fun OverflowRow(hidden: List<Int>, colW: Dp) {
|
||||
Row(modifier = GlanceModifier.fillMaxWidth()) {
|
||||
week.days.forEachIndexed { col, date ->
|
||||
val shownSpans = week.spans.count { col in it.startCol..it.endCol && it.lane < MAX_LANES }
|
||||
val freeSlots = (MAX_LANES - shownSpans).coerceAtLeast(0)
|
||||
val timedShown = minOf(freeSlots, week.timedByDay[date].orEmpty().size)
|
||||
val hidden = (week.countByDay[date] ?: 0) - shownSpans - timedShown
|
||||
// The overflow row is part of the day column too: tapping it (whether
|
||||
// it shows "+N" or is blank) opens that day, same as the app.
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.width(colW)
|
||||
.height(LANE_HEIGHT)
|
||||
.clickable(openDayAction(context, date)),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
if (hidden > 0) {
|
||||
var gap = 0
|
||||
hidden.forEach { count ->
|
||||
if (count == 0) {
|
||||
gap += 1
|
||||
return@forEach
|
||||
}
|
||||
if (gap > 0) {
|
||||
Spacer(GlanceModifier.width(colW * gap).height(LANE_HEIGHT))
|
||||
gap = 0
|
||||
}
|
||||
Text(
|
||||
text = "+$hidden",
|
||||
text = "+$count",
|
||||
maxLines = 1,
|
||||
style = TextStyle(color = GlanceTheme.colors.onSurfaceVariant, fontSize = 9.sp),
|
||||
modifier = GlanceModifier.padding(start = 3.dp),
|
||||
modifier = GlanceModifier.width(colW).padding(start = 3.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gap > 0) Spacer(GlanceModifier.width(colW * gap).height(LANE_HEIGHT))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
253
app/src/main/res/values-cs/strings.xml
Normal file
253
app/src/main/res/values-cs/strings.xml
Normal file
@@ -0,0 +1,253 @@
|
||||
<?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>
|
||||
<string name="app_name">Calendula</string>
|
||||
<string name="app_tagline">Moderní kalendář.</string>
|
||||
<string name="state_loading">Načítá se…</string>
|
||||
<string name="state_retry">Opakovat</string>
|
||||
<string name="state_failure_unknown">Něco se pokazilo.</string>
|
||||
<string name="state_failure_permission">Je nutný přístup ke kalendáři.</string>
|
||||
<string name="state_failure_permission_action">Udělit přístup</string>
|
||||
<string name="state_failure_no_calendars">Nejsou konfigurovány žádné kalendáře.</string>
|
||||
<string name="permission_request_button">Udělit přístup ke kalendáři</string>
|
||||
<string name="permission_rationale_body">Calendula potřebuje přístup k vašemu kalendáři, aby mohla zobrazovat a spravovat vaše události.</string>
|
||||
<string name="permission_benefit_sync_title">Všechny vaše kalendáře na jednom místě</string>
|
||||
<string name="permission_benefit_sync_body">Google, CalDAV, lokální kalendáře — cokoliv synchronizovaného se zařízením.</string>
|
||||
<string name="permission_benefit_private_title">Navrženo pro soukromí</string>
|
||||
<string name="permission_benefit_private_body">Žádný přístup k internetu, žádná telemetrie — nikdy nedojde k úniku dat z vašeho telefonu.</string>
|
||||
<string name="onboarding_step_counter">Krok %1$d z %2$d</string>
|
||||
<string name="permission_rationale_title">Prohlížejte všechny události, hezky a přehledně</string>
|
||||
<string name="permission_denied_title">Přístup ke kalendáři byl odepřen</string>
|
||||
<string name="permission_denied_body">Calendula nemůže zobrazovat události bez uděleného přístupu ke kalendáři. Přístup můžete znovu udělit v nastavení systému.</string>
|
||||
<string name="state_failure_no_calendars_action">Otevřít nastavení kalendáře systému</string>
|
||||
<string name="permission_open_settings_button">Otevřít nastavení systému</string>
|
||||
<string name="permission_retry_button">Zkusit to znovu</string>
|
||||
<string name="state_failure_provider">Nepodařilo se načíst kalendář.</string>
|
||||
<string name="permission_benefit_privacy_title">Žádné sledování, nikdy</string>
|
||||
<string name="permission_benefit_privacy_body">Žádná telemetrie, žádné analytické nástroje, bez reklam.</string>
|
||||
<string name="permission_privacy_footnote">Zůstává na vašem zařízení · bez přístupu k internetu</string>
|
||||
<string name="month_prev">Předchozí měsíc</string>
|
||||
<string name="month_next">Následující měsíc</string>
|
||||
<string name="month_today_action">Dnes</string>
|
||||
<string name="month_more_actions">Další akce</string>
|
||||
<string name="month_open_menu">Otevřít nabídku</string>
|
||||
<string name="month_action_settings">Nastavení</string>
|
||||
<string name="month_a11y_today_prefix">Dnes</string>
|
||||
<string name="week_today_action">Tento týden</string>
|
||||
<string name="week_number_label">týd.</string>
|
||||
<string name="day_today_action">Dnes</string>
|
||||
<string name="event_detail_back">Zpět</string>
|
||||
<string name="event_detail_edit">Upravit</string>
|
||||
<string name="event_detail_delete">Vymazat</string>
|
||||
<string name="event_detail_share">Sdílet</string>
|
||||
<string name="event_detail_duplicate">Kopírovat</string>
|
||||
<string name="event_share_chooser_title">Sdílet událost</string>
|
||||
<string name="event_share_failed">Tuto událost nelze sdílet.</string>
|
||||
<string name="event_delete_title">Odstranit událost?</string>
|
||||
<string name="event_delete_body">Událost je odstraněna z kalendáře i ze všech synchronizovaných zařízení.</string>
|
||||
<string name="event_delete_recurring_title">Smazat opakující se událost</string>
|
||||
<string name="event_delete_option_occurrence">Pouze tuto událost</string>
|
||||
<string name="event_delete_option_following">Tuto a všechny následující události</string>
|
||||
<string name="event_delete_option_series">Všechny události v sérii</string>
|
||||
<string name="event_edit_recurring_title">Upravit opakující se událost</string>
|
||||
<string name="event_delete_failed">Tuto událost nelze odstranit</string>
|
||||
<string name="event_delete_write_denied">Calendula potřebuje oprávnění k zápisu, aby mohla mazat události</string>
|
||||
<string name="event_move_action">Přesunout…</string>
|
||||
<string name="event_move_recurring_title">Přesunout opakující se událost</string>
|
||||
<string name="event_move_occurrence_only">Přesunutí celé série by rozhodilo pořadí dnů, na které série připadá, přesunout lze tedy pouze tuto událost.</string>
|
||||
<string name="event_move_done">Přesunuto do %1$s</string>
|
||||
<string name="event_move_undo">Zpět</string>
|
||||
<string name="event_move_undone">Vráceno zpět</string>
|
||||
<string name="event_move_failed">Událost nelze přesunout</string>
|
||||
<string name="event_move_write_denied">Calendula k přesouvání události vyžaduje oprávnění k zápisu</string>
|
||||
<string name="event_move_gone">Tato událost již neexistuje</string>
|
||||
<string name="event_move_blocked_series_end">Událost nelze přesunout za konec dané série</string>
|
||||
<string name="dialog_cancel">Zrušit</string>
|
||||
<string name="dialog_ok">OK</string>
|
||||
<string name="event_edit_new_title">Nová událost</string>
|
||||
<string name="event_edit_close">Zavřít</string>
|
||||
<string name="event_edit_save">Uložit</string>
|
||||
<string name="event_edit_title_hint">Přidat název</string>
|
||||
<string name="event_edit_managed_hint">Spravováno aplikací „%1$s“ — název, datum a opakování je synchronizováno z vašich kontaktů. Upomínky, polohu a poznámky si můžete upravit sami.</string>
|
||||
<string name="event_edit_starts">Začátek</string>
|
||||
<string name="event_edit_ends">Konec</string>
|
||||
<string name="event_edit_error_end_before_start">Končí dříve, než začíná</string>
|
||||
<string name="event_edit_error_no_calendar">Není k dispozici žádný kalendář, který by umožňoval zápis</string>
|
||||
<string name="event_edit_save_failed">Nelze uložit událost</string>
|
||||
<string name="event_edit_write_denied">Calendula pro vytváření událostí vyžaduje oprávnění k zápisu</string>
|
||||
<string name="event_edit_more_fields">další pole</string>
|
||||
<string name="event_edit_add">Přidat</string>
|
||||
<string name="event_edit_add_reminder">Přidat upomínku</string>
|
||||
<string name="event_edit_remove_reminder">Vymazat upomínku</string>
|
||||
<string name="event_edit_attendees">Hosté</string>
|
||||
<string name="event_edit_add_guest">Přidat hosta</string>
|
||||
<string name="event_edit_add_guest_hint">Přidat hosta e-mailem…</string>
|
||||
<string name="event_edit_add_guest_from_contacts">Přidat z kontaktů</string>
|
||||
<string name="event_edit_location_from_contacts">Vybrat adresu z kontaktů</string>
|
||||
<string name="event_edit_remove_guest">Odstranit hosta</string>
|
||||
<string name="event_edit_attendee_required">Požadované</string>
|
||||
<string name="event_edit_attendee_optional">Volitelné</string>
|
||||
<string name="event_edit_attendees_note_synced">Calendula nezasílá pozvánky. Váš účet kalendáře může hosty zvát e-mailem při synchronizaci.</string>
|
||||
<string name="event_edit_attendees_note_local">Uloženo v tomto zařízení. Nikomu nejsou odesílány notifikace.</string>
|
||||
<string name="event_edit_reminder_custom">Vlastní</string>
|
||||
<string name="reminder_unit_minutes">minuty</string>
|
||||
<string name="reminder_unit_hours">hodiny</string>
|
||||
<string name="reminder_unit_days">dny</string>
|
||||
<string name="reminder_unit_weeks">týdny</string>
|
||||
<string name="event_edit_availability">Dostupnost</string>
|
||||
<string name="event_edit_visibility">Viditelnost</string>
|
||||
<string name="event_edit_timezone_device">Časové pásmo zařízení</string>
|
||||
<string name="event_edit_timezone_device_summary">Monitoruje vaši polohu, ať už jste kdekoliv</string>
|
||||
<string name="event_edit_timezone_search">Vyhledat časové pásmo</string>
|
||||
<string name="event_edit_timezone_recent">Nedávné</string>
|
||||
<string name="event_edit_timezone_all">Všechna časová pásma</string>
|
||||
<string name="event_edit_timezone_none">Žádné časové pásmo neodpovídá “%1$s”</string>
|
||||
<string name="event_edit_timezone_local_time">%1$s vašeho času</string>
|
||||
<string name="event_edit_color">Barva</string>
|
||||
<string name="event_edit_color_default">Barva kalendáře</string>
|
||||
<string name="event_edit_color_custom">Vlastní barva</string>
|
||||
<string name="event_edit_color_reset">Obnovit</string>
|
||||
<string name="event_edit_color_unsupported">Pro tento kalendář není k dispozici</string>
|
||||
<string name="event_edit_color_unsupported_hint">Tento kalendář nemá žádnou sadu barev. Vlastní barvy kalendářů můžete povolit v nastavení.</string>
|
||||
<string name="event_edit_color_sync_warning">Tato barva může být během příští synchronizace kalendáře ztracena nebo přepsána.</string>
|
||||
<string name="event_edit_conflict_title">Ke změně události došlo na jiném místě.</string>
|
||||
<string name="event_edit_conflict_body">Během úpravy došlo ke změně události — v rámci synchronizace nebo pomocí jiné aplikací. Co se má stát s vašimi změnami?</string>
|
||||
<string name="event_edit_conflict_overwrite">Uložit změny</string>
|
||||
<string name="event_edit_conflict_overwrite_hint">Pouze u upravených polí dojde k přepsání vnějšího zdroje</string>
|
||||
<string name="event_edit_conflict_discard">Zrušit změny</string>
|
||||
<string name="event_edit_conflict_discard_hint">Událost zůstává beze změny</string>
|
||||
<string name="event_edit_gone_title">Událost byla smazána</string>
|
||||
<string name="event_edit_gone_body">Během úpravy došlo ke smazání události, například pomocí jiného zařízení. Vaše změny již nelze uložit.</string>
|
||||
<string name="import_reminder_prompt_title">Chcete použít výchozí upomínku?</string>
|
||||
<string name="import_reminder_prompt_body_none">Tato událost byla importována bez nastavené upomínky.</string>
|
||||
<plurals name="import_reminder_prompt_body_existing">
|
||||
<item quantity="one">Tato událost byla importována s %1$d upomínkou.</item>
|
||||
<item quantity="few">Tato událost byla importována s %1$d upomínkami.</item>
|
||||
<item quantity="many">Tato událost byla importována s %1$d upomínkami.</item>
|
||||
<item quantity="other">Tato událost byla importována s %1$d upomínkami.</item>
|
||||
</plurals>
|
||||
<string name="import_reminder_prompt_apply">Nastavit výchozí</string>
|
||||
<string name="import_reminder_prompt_keep">Nechat beze změny</string>
|
||||
<string name="event_edit_recurrence_none">Neopakuje se</string>
|
||||
<string name="event_edit_recurrence_custom">Vlastní</string>
|
||||
<string name="event_edit_recurrence_every">Každý</string>
|
||||
<string name="recurrence_unit_days">dny</string>
|
||||
<string name="recurrence_unit_weeks">týdny</string>
|
||||
<string name="recurrence_unit_months">měsíce</string>
|
||||
<string name="recurrence_unit_years">roky</string>
|
||||
<string name="event_edit_recurrence_incomplete">Zadejte číslo v rozmezí 1 až 999</string>
|
||||
<string name="event_edit_recurrence_next">Následující: %1$s</string>
|
||||
<string name="event_edit_recurrence_next_none">Toto pravidlo se nikdy neopakuje</string>
|
||||
<string name="event_edit_recurrence_ends">Konec</string>
|
||||
<string name="event_edit_recurrence_end_never">Nikdy</string>
|
||||
<string name="event_edit_recurrence_end_until">K určitému datu</string>
|
||||
<string name="event_edit_recurrence_end_count">Po určitém počtu opakování</string>
|
||||
<string name="event_edit_recurrence_times">opakování</string>
|
||||
<string name="event_edit_error_recurrence_ends_before_start">Opakování končí před začátkem události</string>
|
||||
<string name="event_availability_busy">Zaneprázdněný</string>
|
||||
<string name="event_access_default">Výchozí</string>
|
||||
<string name="event_access_public">Veřejný</string>
|
||||
<string name="event_detail_all_day">Celý den</string>
|
||||
<string name="event_detail_calendar">Kalendář</string>
|
||||
<string name="event_detail_calendar_unknown">Neznámý kalendář</string>
|
||||
<string name="event_detail_location">Umístění</string>
|
||||
<string name="event_detail_description">Popis</string>
|
||||
<string name="event_detail_attendees">Účastníci</string>
|
||||
<string name="event_detail_recurrence">Opakování</string>
|
||||
<string name="event_detail_recurring">Opakující se událost</string>
|
||||
<string name="recurrence_daily">Každý den</string>
|
||||
<string name="recurrence_weekly">Každý týden</string>
|
||||
<string name="recurrence_monthly">Každý měsíc</string>
|
||||
<string name="recurrence_yearly">Každý rok</string>
|
||||
<string name="recurrence_every_n_days">Každý %1$d. den</string>
|
||||
<string name="recurrence_every_n_weeks">Každý %1$d. týden</string>
|
||||
<string name="recurrence_every_n_months">Každý %1$d. měsíc</string>
|
||||
<string name="recurrence_every_n_years">Každý %1$d. rok</string>
|
||||
<string name="recurrence_on_days">%1$s v %2$s</string>
|
||||
<string name="recurrence_with_until">%1$s do%2$s</string>
|
||||
<string name="recurrence_with_count">%1$s, %2$d opakování</string>
|
||||
<string name="event_detail_not_found">Tato událost již neexistuje.</string>
|
||||
<string name="event_attendee_accepted">Přijato</string>
|
||||
<string name="event_attendee_declined">Odmítnuto</string>
|
||||
<string name="event_attendee_tentative">Předběžné</string>
|
||||
<string name="event_attendee_needs_action">Bez odezvy</string>
|
||||
<string name="event_attendee_unknown">—</string>
|
||||
<string name="event_detail_reminders">Upomínky</string>
|
||||
<string name="event_detail_timezone">Časové pásmo</string>
|
||||
<string name="event_status_tentative">Přeběžné</string>
|
||||
<string name="event_status_cancelled">Zrušeno</string>
|
||||
<string name="event_availability_free">Volné</string>
|
||||
<string name="event_access_private">Soukromé</string>
|
||||
<string name="event_access_confidential">Utajené</string>
|
||||
<string name="event_access_default_summary">Běžná činnost kalendáře</string>
|
||||
<string name="event_access_public_summary">Kdokoliv s možností přístupu uvidí veškeré podrobnosti</string>
|
||||
<string name="event_access_private_summary">Ostatní uvidí pouze to, že jste zaneprázdněn</string>
|
||||
<string name="event_access_confidential_summary">Označeno jako utajené; záleží na daném účtu kalendáře, jak s tímto štítkem naloží</string>
|
||||
<string name="event_attendee_organizer">Pořadatel</string>
|
||||
<string name="event_attendee_optional">Volitelné</string>
|
||||
<string name="event_attendee_resource">Zdroj</string>
|
||||
<string name="event_detail_self_response">Vaše odpověď: %1$s</string>
|
||||
<string name="reminder_at_time">V době konání události</string>
|
||||
<string name="reminder_default">Výchozí upomínka</string>
|
||||
<plurals name="reminder_minutes">
|
||||
<item quantity="one">%d minutu předtím</item>
|
||||
<item quantity="few">%d minuty předtím</item>
|
||||
<item quantity="many">%d minut předtím</item>
|
||||
<item quantity="other">%d minut předtím</item>
|
||||
</plurals>
|
||||
<plurals name="reminder_hours">
|
||||
<item quantity="one">%d hodinu předtím</item>
|
||||
<item quantity="few">%d hodiny předtím</item>
|
||||
<item quantity="many">%d hodin předtím</item>
|
||||
<item quantity="other">%d hodin předtím</item>
|
||||
</plurals>
|
||||
<plurals name="reminder_days">
|
||||
<item quantity="one">%d den předtím</item>
|
||||
<item quantity="few">%d dny předtím</item>
|
||||
<item quantity="many">%d dnů předtím</item>
|
||||
<item quantity="other">%d dnů předtím</item>
|
||||
</plurals>
|
||||
<plurals name="reminder_weeks">
|
||||
<item quantity="one">%d týden předtím</item>
|
||||
<item quantity="few">%d týdny předtím</item>
|
||||
<item quantity="many">%d týdnů předtím</item>
|
||||
<item quantity="other">%d týdnů předtím</item>
|
||||
</plurals>
|
||||
<plurals name="duration_minutes">
|
||||
<item quantity="one">%d minuta</item>
|
||||
<item quantity="few">%d minuty</item>
|
||||
<item quantity="many">%d minut</item>
|
||||
<item quantity="other">%d minut</item>
|
||||
</plurals>
|
||||
<plurals name="duration_hours">
|
||||
<item quantity="one">%d hodina</item>
|
||||
<item quantity="few">%d hodiny</item>
|
||||
<item quantity="many">%d hodin</item>
|
||||
<item quantity="other">%d hodin</item>
|
||||
</plurals>
|
||||
<plurals name="duration_days">
|
||||
<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="duration_hours_minutes">%1$s %2$s</string>
|
||||
<string name="duration_custom_max">Nanejvýš %1$s</string>
|
||||
<plurals name="duration_weeks">
|
||||
<item quantity="one">%d týden</item>
|
||||
<item quantity="few">%d týdny</item>
|
||||
<item quantity="many">%d týdnů</item>
|
||||
<item quantity="other">%d týdnů</item>
|
||||
</plurals>
|
||||
<string name="event_untitled">(Bez názvu)</string>
|
||||
<string name="onboarding_backup_title">Vaši událost naleznete pouze zde</string>
|
||||
<string name="onboarding_backup_body">V tomto telefonu se nesynchronizuje žádný účet, takže pokud ho ztratíte, přijdete i o svůj kalendář. Calendula může vytvořit lokální zálohu.</string>
|
||||
<string name="onboarding_backup_benefit_folder_title">Složka, kterou si vyberete</string>
|
||||
<string name="onboarding_backup_benefit_folder_body">Zálohy jsou obyčejné soubory v .ics formátu — uložte je někam, kde se synchronizují, nebo na SD kartu.</string>
|
||||
<string name="onboarding_backup_benefit_daily_title">Jednou denně, automaticky</string>
|
||||
</resources>
|
||||
82
app/src/main/res/values-ja/strings.xml
Normal file
82
app/src/main/res/values-ja/strings.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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>
|
||||
<string name="app_tagline">モダンなカレンダー。</string>
|
||||
<string name="state_loading">読み込み中…</string>
|
||||
<string name="state_retry">リトライ</string>
|
||||
<string name="month_today_action">今日</string>
|
||||
<string name="month_action_settings">設定</string>
|
||||
<string name="event_detail_description">説明</string>
|
||||
<string name="event_edit_add_reminder">リマインダーを追加</string>
|
||||
<string name="event_edit_save">保存</string>
|
||||
<string name="event_detail_all_day">終日</string>
|
||||
<string name="view_month">月</string>
|
||||
<string name="view_week">週</string>
|
||||
<string name="view_day">日</string>
|
||||
<string name="settings_section_backup">バックアップと復元</string>
|
||||
<string name="settings_special_dates_type_birthday">誕生日</string>
|
||||
<string name="settings_special_dates_sync_now">今すぐ同期</string>
|
||||
<string name="settings_special_dates_last_synced">最終同期 %1$s</string>
|
||||
<string name="settings_section_notifications">通知</string>
|
||||
<string name="drawer_jump_to_date">日付に移動</string>
|
||||
<string name="state_failure_permission">カレンダーへのアクセス権限が必要です。</string>
|
||||
<string name="permission_rationale_body">Calendulaで予定を表示・管理するには、カレンダーへのアクセス許可が必要です。</string>
|
||||
<string name="permission_request_button">カレンダーへのアクセスを許可</string>
|
||||
<string name="permission_denied_title">カレンダーへのアクセスが拒否されました</string>
|
||||
<string name="permission_denied_body">Calendulaはカレンダーへのアクセス権がないと予定を表示できません。システム設定から再度許可をお願いします。</string>
|
||||
<string name="permission_open_settings_button">システム設定を開く</string>
|
||||
<string name="permission_retry_button">再試行</string>
|
||||
<string name="permission_benefit_private_title">プライバシーを重視した設計</string>
|
||||
<string name="permission_benefit_private_body">インターネットへのアクセス権限は不要で、テレメトリも送信しません。すべてのデータはスマートフォン内に保持されます。</string>
|
||||
<string name="permission_benefit_sync_title">すべてのカレンダーをひとつに</string>
|
||||
<string name="permission_benefit_sync_body">Google、CalDAV、ローカルなど、デバイスに同期されているすべてのカレンダーが自動的に表示されます。</string>
|
||||
<string name="event_detail_delete">削除</string>
|
||||
<string name="event_detail_share">共有</string>
|
||||
<string name="event_detail_duplicate">複製</string>
|
||||
<string name="event_delete_title">予定を削除しますか?</string>
|
||||
<string name="event_delete_body">この予定はカレンダーだけでなく、同期しているすべてのデバイスからも削除されます。</string>
|
||||
<string name="event_delete_recurring_title">定期的な予定を削除</string>
|
||||
<string name="event_delete_option_occurrence">この予定のみ</string>
|
||||
<string name="event_delete_option_following">この予定とそれ以降すべて</string>
|
||||
<string name="event_edit_recurring_title">定期的な予定を編集</string>
|
||||
<string name="event_delete_failed">予定を削除できませんでした</string>
|
||||
<string name="recurrence_unit_years">毎年</string>
|
||||
<string name="reminder_unit_weeks">週</string>
|
||||
<string name="recurrence_unit_weeks">毎週</string>
|
||||
<string name="recurrence_unit_months">毎月</string>
|
||||
<string name="reminder_unit_minutes">分</string>
|
||||
<string name="reminder_unit_hours">時間</string>
|
||||
<string name="reminder_unit_days">日</string>
|
||||
<string name="recurrence_unit_days">毎日</string>
|
||||
<string name="reminder_day_yesterday">前日</string>
|
||||
<string name="settings_section_widgets">ウィジットとタイル</string>
|
||||
<string name="settings_week_start">週の開始日</string>
|
||||
<string name="settings_week_numbers">週番号</string>
|
||||
<string name="settings_theme_light">ライト</string>
|
||||
<string name="settings_theme_dark">ダーク</string>
|
||||
<string name="settings_font_headings">見出しのフォント</string>
|
||||
<string name="settings_font_body">本文のフォント</string>
|
||||
<string name="settings_font_system">システムの標準</string>
|
||||
<string name="settings_font_choose_file">ファイルを選択…</string>
|
||||
<string name="settings_week_start_auto">自動</string>
|
||||
<string name="settings_week_start_auto_summary">現在は %1$s</string>
|
||||
<string name="settings_app_name">アプリの名前</string>
|
||||
<string name="settings_section_language">言語</string>
|
||||
<string name="settings_language">アプリの言語</string>
|
||||
<string name="settings_language_auto">システムの既定</string>
|
||||
<string name="settings_group_app">アプリ</string>
|
||||
<string name="settings_appearance_subtitle">テーマ、カラー、フォント</string>
|
||||
<string name="settings_about_privacy">プライバシーポリシー</string>
|
||||
<string name="settings_about_support">開発を支援する</string>
|
||||
<string name="settings_license">ライセンス</string>
|
||||
<string name="settings_report_problem">問題を報告</string>
|
||||
<string name="settings_title">設定</string>
|
||||
<string name="settings_theme">テーマ</string>
|
||||
<string name="settings_theme_system">システム</string>
|
||||
<string name="settings_dynamic_color">ダイナミックカラー</string>
|
||||
</resources>
|
||||
@@ -207,4 +207,445 @@
|
||||
<item quantity="other">%d minutos</item>
|
||||
</plurals>
|
||||
<string name="settings_special_dates_grant">Conceder acesso</string>
|
||||
<string name="event_move_action">Mover…</string>
|
||||
<string name="event_move_recurring_title">Mover evento recorrente</string>
|
||||
<string name="event_move_occurrence_only">Mover a série inteira mudaria os dias em que o evento cai, então somente este evento pode se mover.</string>
|
||||
<string name="event_move_done">Movido para %1$s</string>
|
||||
<string name="event_move_undo">Desfazer</string>
|
||||
<string name="event_move_undone">Mover desfeito</string>
|
||||
<string name="event_move_failed">Não foi possível mover o evento</string>
|
||||
<string name="event_move_write_denied">O Calendula precisa de permissão de escrita para mover eventos</string>
|
||||
<string name="event_move_gone">Este evento não existe mais</string>
|
||||
<string name="event_move_blocked_series_end">Impossível mover um evento após o fim de sua série</string>
|
||||
<string name="event_status_tentative">Talvez</string>
|
||||
<plurals name="duration_hours">
|
||||
<item quantity="one">%d hora</item>
|
||||
<item quantity="many">%d horas</item>
|
||||
<item quantity="other">%d horas</item>
|
||||
</plurals>
|
||||
<plurals name="duration_days">
|
||||
<item quantity="one">%d dia</item>
|
||||
<item quantity="many">%d dias</item>
|
||||
<item quantity="other">%d dias</item>
|
||||
</plurals>
|
||||
<string name="duration_hours_minutes">%1$s %2$s</string>
|
||||
<string name="duration_custom_max">No máximo %1$s</string>
|
||||
<plurals name="duration_weeks">
|
||||
<item quantity="one">%d semana</item>
|
||||
<item quantity="many">%d semanas</item>
|
||||
<item quantity="other">%d semanas</item>
|
||||
</plurals>
|
||||
<string name="event_untitled">(Sem título)</string>
|
||||
<string name="onboarding_step_counter">Passo %1$d de %2$d</string>
|
||||
<string name="onboarding_backup_title">Seus eventos vivem somente aqui</string>
|
||||
<string name="onboarding_backup_body">Nada neste aparelho sincroniza para uma conta, então perdê-lo levaria seu calendário junto. O Calendula pode criar um backup para você.</string>
|
||||
<string name="onboarding_backup_benefit_folder_title">Uma pasta à sua escolha</string>
|
||||
<string name="onboarding_backup_benefit_folder_body">Os backups são arquivos .ics — coloque-os em um lugar para sincronizar, ou um cartão SD.</string>
|
||||
<string name="onboarding_backup_benefit_daily_title">Uma vez por dia, sozinho</string>
|
||||
<string name="onboarding_backup_benefit_daily_body">O Calendula exporta seus calendários locais em segundo plano. Mude a frequência em Ajustes.</string>
|
||||
<string name="onboarding_backup_enable_button">Escolher pasta e fazer backup</string>
|
||||
<string name="onboarding_backup_skip_button">Agora não</string>
|
||||
<string name="onboarding_view_title">O que deve abrir primeiro?</string>
|
||||
<string name="onboarding_month_style_title">Como o Mês deve se parecer?</string>
|
||||
<string name="onboarding_view_continue_button">Continuar</string>
|
||||
<string name="onboarding_back">Voltar</string>
|
||||
<string name="onboarding_visibility_button">Entendi</string>
|
||||
<string name="onboarding_done_title">Tudo pronto</string>
|
||||
<string name="onboarding_done_body">Seus calendários estão prontos. Todas as suas escolhas podem ser alteradas depois em Ajustes.</string>
|
||||
<string name="onboarding_done_button">Abrir meu calendário</string>
|
||||
<string name="reminder_channel_name">Lembretes de evento</string>
|
||||
<string name="reminder_channel_description">Notificações na hora dos lembretes dos seus eventos</string>
|
||||
<string name="reminder_onboarding_title">Nunca perca um evento</string>
|
||||
<string name="reminder_onboarding_body">O Android não mostra lembretes sozinho — um app de calendário deve fazer isso. Deixe o Calendula cuidar disso.</string>
|
||||
<string name="reminder_benefit_delivery_title">Lembretes, entregues</string>
|
||||
<string name="reminder_benefit_delivery_body">Todos os seus lembretes de evento chegam em notificação, na hora certa.</string>
|
||||
<string name="reminder_benefit_duplicates_title">Usando outro app de calendário?</string>
|
||||
<string name="reminder_benefit_duplicates_body">Se outro app também postar lembretes, você verá eles em dobro — desligue aqui ou lá.</string>
|
||||
<string name="reminder_benefit_reversible_title">Mude a qualquer hora</string>
|
||||
<string name="reminder_benefit_reversible_body">O botão está em Ajustes, dentro de Notificações.</string>
|
||||
<string name="reminder_onboarding_enable_button">Ativar lembretes</string>
|
||||
<string name="reminder_onboarding_skip_button">Agora não</string>
|
||||
<string name="reminder_action_snooze">Soneca</string>
|
||||
<string name="reminder_action_dismiss">Dispensar</string>
|
||||
<string name="reminder_day_tomorrow">Amanhã</string>
|
||||
<string name="reminder_day_yesterday">Ontem</string>
|
||||
<string name="view_month">Mês</string>
|
||||
<string name="view_week">Semana</string>
|
||||
<string name="view_day">Dia</string>
|
||||
<string name="view_agenda">Agenda</string>
|
||||
<string name="view_section">Vista</string>
|
||||
<string name="drawer_jump_to_date">Ir para data</string>
|
||||
<string name="agenda_today_action">Hoje</string>
|
||||
<string name="agenda_header_today">Hoje</string>
|
||||
<string name="agenda_header_tomorrow">Amanhã</string>
|
||||
<string name="agenda_empty_title">Tudo em dia</string>
|
||||
<string name="agenda_no_more_today">Sem mais eventos hoje</string>
|
||||
<string name="agenda_span_starts">Começa às %1$s</string>
|
||||
<string name="agenda_span_ends">Termina às %1$s</string>
|
||||
<string name="today_jump_action">Ir para Hoje</string>
|
||||
<string name="search_action">Buscar</string>
|
||||
<string name="search_hint">Buscar eventos</string>
|
||||
<string name="search_back">Voltar</string>
|
||||
<string name="search_clear">Limpar</string>
|
||||
<string name="search_idle_hint">Busque seus eventos por título, local ou notas.</string>
|
||||
<string name="search_empty">Nenhum evento corresponde a \"%1$s\".</string>
|
||||
<plurals name="search_selected_count">
|
||||
<item quantity="one">%d selecionado</item>
|
||||
<item quantity="many">%d selecionados</item>
|
||||
<item quantity="other">%d selecionados</item>
|
||||
</plurals>
|
||||
<string name="search_in_descriptions">Encontrado em descrições</string>
|
||||
<string name="search_selection_close">Cancelar seleção</string>
|
||||
<string name="search_select_all">Selecionar tudo</string>
|
||||
<string name="search_delete_selected">Deletar selecionados</string>
|
||||
<plurals name="search_delete_title">
|
||||
<item quantity="one">Deletar %d evento?</item>
|
||||
<item quantity="many">Deletar %d eventos?</item>
|
||||
<item quantity="other">Deletar %d eventos?</item>
|
||||
</plurals>
|
||||
<plurals name="search_delete_done">
|
||||
<item quantity="one">%d evento deletado</item>
|
||||
<item quantity="many">%d eventos deletados</item>
|
||||
<item quantity="other">%d eventos deletados</item>
|
||||
</plurals>
|
||||
<string name="search_delete_partial">%1$d deletado, %2$d não pôde ser deletado</string>
|
||||
<string name="search_delete_denied_partial">%1$d deletado, depois permissão de escrita removida</string>
|
||||
<string name="widget_agenda_title">A seguir</string>
|
||||
<string name="widget_agenda_label">Agenda do Calendula</string>
|
||||
<string name="widget_month_label">Mês do Calendula</string>
|
||||
<string name="widget_refresh">Atualizar</string>
|
||||
<string name="widget_new_event">Novo evento</string>
|
||||
<string name="widget_needs_permission">Abra o Calendula para conceder acesso ao calendário</string>
|
||||
<string name="widget_prev_month">Mês passado</string>
|
||||
<string name="widget_next_month">Mês que vem</string>
|
||||
<string name="widget_today">Hoje</string>
|
||||
<string name="filter_title">Calendários</string>
|
||||
<string name="settings_title">Ajustes</string>
|
||||
<string name="settings_back">Voltar</string>
|
||||
<string name="back">Voltar</string>
|
||||
<string name="settings_section_appearance">Aparência</string>
|
||||
<string name="settings_theme">Tema</string>
|
||||
<string name="settings_theme_system">Sistema</string>
|
||||
<string name="settings_theme_light">Claro</string>
|
||||
<string name="settings_theme_dark">Escuro</string>
|
||||
<string name="settings_theme_hint">Se o app é claro ou escuro. Aplica imediatamente.</string>
|
||||
<string name="settings_theme_system_summary">No momento %1$s</string>
|
||||
<string name="settings_default_view">Vista padrão</string>
|
||||
<string name="settings_dynamic_color">Cor dinâmica</string>
|
||||
<string name="settings_dynamic_color_unavailable">Requer Android 12 ou superior</string>
|
||||
<string name="settings_soften_colors">Harmonizar cores do calendário</string>
|
||||
<string name="settings_soften_colors_summary">Mantenha cada tom do calendário até mesmo sem brilho, para que cada evento continue legível e as cores combinem. Desative para mostrar as cores cruas da raiz do calendário.</string>
|
||||
<string name="settings_font_headings">Fonte dos cabeçalhos</string>
|
||||
<string name="settings_font_body">Fonte do corpo</string>
|
||||
<string name="settings_font_system">Padrão do sistema</string>
|
||||
<string name="font_atkinson_hyperlegible">Atkinson Hiperlegível</string>
|
||||
<string name="font_lora">Lora</string>
|
||||
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||
<string name="settings_font_choose_file">Escolher arquivo…</string>
|
||||
<string name="settings_font_custom_selected">Fonte personalizada</string>
|
||||
<string name="settings_font_import_failed">Não pôde ler arquivo como fonte</string>
|
||||
<string name="settings_font_specimen_heading">Quinta-feira, 14 de maio</string>
|
||||
<string name="settings_font_specimen_body">Reunião de equipe às 10:00, depois almoço com Robin no café em Markplatz.</string>
|
||||
<string name="settings_week_start">A semana inicia em</string>
|
||||
<string name="settings_week_start_auto">Automático</string>
|
||||
<string name="settings_week_start_auto_summary">No momento %1$s</string>
|
||||
<string name="settings_week_numbers">Números das semanas</string>
|
||||
<string name="settings_week_numbers_summary">Mostrar números das semanas na vista do mês</string>
|
||||
<string name="settings_today_toolbar">Botão \"Hoje\" na barra de ferramentas</string>
|
||||
<string name="settings_today_toolbar_summary">Mostrar um botão \"Hoje\" na barra de ferramentas em vez de um botão flutuante</string>
|
||||
<string name="settings_app_name">Nome do app</string>
|
||||
<string name="settings_app_name_summary">Mostrar o Calendula como \"Calendário\" no seu launcher. Somente o nome do launcher muda; o ícone pode mudar para outro lugar após a troca.</string>
|
||||
<string name="settings_time_format">Formato de hora</string>
|
||||
<string name="settings_time_format_auto">Automático</string>
|
||||
<string name="settings_time_format_12h">12 horas (2:00 PM)</string>
|
||||
<string name="settings_time_format_24h">24 horas (14:00)</string>
|
||||
<string name="settings_time_format_auto_summary">Seguir o sistema: %1$s</string>
|
||||
<string name="settings_hour_lines">Linhas de hora</string>
|
||||
<string name="settings_hour_lines_summary">Mostrar um separador a cada hora nas vistas de semana e dia</string>
|
||||
<string name="settings_timeline_scale">Altura da hora</string>
|
||||
<string name="settings_timeline_scale_hint">Quanto espaço vertical que a hora ocupa nas vistas de semana e dia. Ambas as vistas compartilham esse ajuste. Você também pode pinçar a linha do tempo com dois dedos para colocar uma altura no meio.</string>
|
||||
<string name="timeline_scale_fit_day">Encaixar o dia todo</string>
|
||||
<string name="timeline_scale_fit_day_summary">24 horas na tela, sem rolagem</string>
|
||||
<string name="timeline_scale_compact">Compacto</string>
|
||||
<string name="timeline_scale_compact_summary">Mais horas por tela, blocos menores</string>
|
||||
<string name="timeline_scale_regular">Regular</string>
|
||||
<string name="timeline_scale_regular_summary">Espaçamento padrão</string>
|
||||
<string name="timeline_scale_comfortable">Confortável</string>
|
||||
<string name="timeline_scale_comfortable_summary">Blocos maiores, mais rolagem</string>
|
||||
<string name="timeline_scale_custom">Personalizado</string>
|
||||
<string name="timeline_scale_custom_summary">A altura em que você pinçou a linha do tempo</string>
|
||||
<string name="settings_dim_completed">Esmaecer eventos passados</string>
|
||||
<string name="settings_dim_completed_summary">Esmaecer eventos que já terminaram nas vistas do mês e semana</string>
|
||||
<string name="settings_drag_to_reschedule">Arraste para reagendar</string>
|
||||
<string name="settings_drag_to_reschedule_summary">Mova um evento arrastando para outro dia ou hora</string>
|
||||
<string name="settings_past_events">Eventos passados</string>
|
||||
<string name="settings_past_events_show">Mostrar</string>
|
||||
<string name="settings_past_events_dim">Esmaecer</string>
|
||||
<string name="settings_past_events_hide">Ocultar</string>
|
||||
<string name="settings_past_events_sample_morning">Revisão de equipe</string>
|
||||
<string name="settings_past_events_sample_midday">Almoço com Robin</string>
|
||||
<string name="settings_past_events_sample_evening">Treino do coral</string>
|
||||
<string name="settings_agenda_header">Agenda</string>
|
||||
<string name="settings_agenda_range">Amplitude da agenda</string>
|
||||
<string name="settings_agenda_range_hint">O quanto à frente a tela de Agenda lista eventos.</string>
|
||||
<string name="settings_agenda_widget_range">Amplitude do widget de Agenda</string>
|
||||
<string name="settings_agenda_widget_range_hint">O quanto à frente o widget de Agenda lista eventos.</string>
|
||||
<string name="settings_widget_size">Tamanho do widget de Agenda</string>
|
||||
<string name="settings_widget_size_hint">O quão largo o texto do widget de Agenda é desenhado. O widget de mês não tem ajuste — sua grade sempre encaixa no espaço que você der.</string>
|
||||
<string name="settings_widget_size_small">Pequeno</string>
|
||||
<string name="settings_widget_size_medium">Médio</string>
|
||||
<string name="settings_widget_size_large">Grande</string>
|
||||
<string name="settings_widget_size_extra_large">Extra grande</string>
|
||||
<string name="settings_widget_size_summary">Texto do evento %1$d%%</string>
|
||||
<string name="settings_agenda_show_today">Sempre mostrar Hoje</string>
|
||||
<string name="settings_agenda_show_today_hint">Mantenha o dia de hoje no topo da agenda e no widget, mesmo que não tenha mais nada hoje.</string>
|
||||
<string name="settings_agenda_range_bar">Barra de amplitude</string>
|
||||
<string name="settings_agenda_range_bar_hint">Mostrar uma barra no topo da agenda nomeando as datas mostradas, com um botão para mudar a amplitude para a sessão</string>
|
||||
<string name="agenda_range_day">Hoje</string>
|
||||
<string name="agenda_range_this_week">Essa semana</string>
|
||||
<string name="agenda_range_this_month">Esse mês</string>
|
||||
<string name="agenda_range_week">Próximos 7 dias</string>
|
||||
<string name="agenda_range_month">Próximos 30 dias</string>
|
||||
<string name="agenda_range_custom">Personalizado…</string>
|
||||
<string name="agenda_range_custom_hint">Dias</string>
|
||||
<string name="agenda_range_override_hint">Só por agora — reajusta para sua amplitude salva ao reabrir o Calendula.</string>
|
||||
<string name="agenda_range_showing_label">Mostrando eventos a seguir para</string>
|
||||
<plurals name="agenda_range_days">
|
||||
<item quantity="one">%d dia</item>
|
||||
<item quantity="many">%d dias</item>
|
||||
<item quantity="other">%d dias</item>
|
||||
</plurals>
|
||||
<string name="settings_section_views">Vistas</string>
|
||||
<string name="settings_month_header">Vista do mês</string>
|
||||
<string name="settings_month_view_style">Estilo de vista do mês</string>
|
||||
<string name="month_style_paged">Páginas</string>
|
||||
<string name="month_style_paged_summary">Um mês ocupa a tela. Deslize para os lados para mudar o mês.</string>
|
||||
<string name="month_style_continuous">Meses contínuos</string>
|
||||
<string name="month_style_continuous_summary">Cada mês tem o próprio cabeçalho, com um espaço separando-o do próximo.</string>
|
||||
<string name="month_style_dense">Semanas integradas</string>
|
||||
<string name="month_style_dense_summary">As semanas seguem sem separação, cada mês terminando no início do outro sem distinção entre eles.</string>
|
||||
<string name="month_style_split">Dividido</string>
|
||||
<string name="month_style_split_summary">Uma grade compacta marca os dias com eventos, e o dia clicado é listado abaixo.</string>
|
||||
<string name="month_split_no_events">Nada agendado</string>
|
||||
<string name="month_split_expand">Mostrar o mês inteiro</string>
|
||||
<string name="month_split_collapse">Mostrar eventos do dia</string>
|
||||
<string name="settings_quick_switch_header">Botão de troca rápida</string>
|
||||
<string name="settings_quick_switch_hint">Escolha por quais vistas o botão superior direito circula, e arraste para reordenar. Vistas desativadas continuam acessíveis pelo menu de navegação.</string>
|
||||
<string name="settings_drawer_order_header">Menu de navegação</string>
|
||||
<string name="settings_drawer_order_hint">Arraste para reordenar vistas listadas no menu de navegação.</string>
|
||||
<string name="reorder_drag_handle">Arraste para reordenar</string>
|
||||
<string name="settings_section_event_form">Novo formulário de evento</string>
|
||||
<string name="settings_form_fields_hint">Campos padrão — o restante está dentro de \"Mais campos\"</string>
|
||||
<string name="settings_autofocus_title">Focar no título em novo evento</string>
|
||||
<string name="settings_autofocus_title_hint">Ao iniciar um novo evento, colocar o cursor no campo de título e abrir o teclado.</string>
|
||||
<string name="settings_event_duration">Duração padrão</string>
|
||||
<string name="settings_event_duration_hint">Quanto tempo um novo evento dura até você mudar o tempo de encerramento. Eventos de dia inteiro não são afetados.</string>
|
||||
<string name="settings_calendar_durations_title">Duração por calendário</string>
|
||||
<string name="settings_calendar_durations_hint">Dar a um calendário a sua própria duração padrão — ex.: 8 horas para trabalho.</string>
|
||||
<string name="settings_calendar_duration_inherits">Padrão (%1$s)</string>
|
||||
<string name="settings_calendar_duration_use_default">Usar duração padrão (%1$s)</string>
|
||||
<string name="settings_color_unsupported">Permitir cores em calendários não suportados</string>
|
||||
<string name="settings_color_unsupported_hint">Alguns calendários (ex.: certos CalDAV) não publicam cores; uma cor de evento personalizada pode ser apagada ou sobrescrita na próxima sincronização. Esta é uma limitação destes calendários, e não é algo que o Calendula pode consertar.</string>
|
||||
<string name="settings_section_notifications">Notificações</string>
|
||||
<string name="settings_reminders">Lembretes de evento</string>
|
||||
<string name="settings_reminders_hint">Vendo lembretes dobrados? Outro calendário está postando também — desligue em um dos dois.</string>
|
||||
<string name="settings_default_reminder">Lembrete padrão</string>
|
||||
<string name="settings_default_reminder_allday">Eventos de dia todo</string>
|
||||
<string name="settings_allday_reminder_time">Tempo de lembrete de dia todo</string>
|
||||
<string name="settings_allday_reminder_time_hint">Lembretes para eventos de dia todo disparam às %1$s</string>
|
||||
<string name="settings_allday_reminder_fires_at">Notifica às %1$s</string>
|
||||
<string name="reminder_none">Nenhum</string>
|
||||
<string name="reminder_use_default">Usar lembrete padrão</string>
|
||||
<string name="reminder_custom_amount">Quantidade</string>
|
||||
<string name="reminder_custom_set">Ajustar</string>
|
||||
<string name="settings_calendar_reminders_title">Lembretes por calendário</string>
|
||||
<string name="settings_calendar_reminders_hint">Mude o padrão por calendário — separadamente para eventos com horário e de dia todo. Um calendário pode manter o padrão, removê-lo, ou ajustar seu próprio.</string>
|
||||
<string name="settings_calendar_reminder_inherits">Padrão (%1$s)</string>
|
||||
<string name="settings_reliable_delivery">Entrega confiável</string>
|
||||
<string name="settings_reliable_delivery_hint">O Android pode atrasar lembretes para economizar bateria. Autorize o Calendula para que possam chegar na hora.</string>
|
||||
<string name="settings_reliable_delivery_exempt">Isentar de otimização de bateria — lembretes chegam na hora.</string>
|
||||
<string name="settings_snooze_duration">Duração de soneca</string>
|
||||
<string name="settings_section_calendars">Calendários</string>
|
||||
<string name="settings_manage_calendars">Gerenciar calendários</string>
|
||||
<string name="settings_manage_calendars_hint">Crie calendários locais; gerencie os sincronizados</string>
|
||||
<string name="settings_section_language">Idioma</string>
|
||||
<string name="settings_language">Idioma do app</string>
|
||||
<string name="settings_language_auto">Padrão do sistema</string>
|
||||
<string name="settings_translate">Ajude a traduzir</string>
|
||||
<string name="settings_translate_hint">Adicione ou melhore um idioma no Weblate</string>
|
||||
<string name="settings_group_look">Aparência e comportamento</string>
|
||||
<string name="settings_group_data">Dados</string>
|
||||
<string name="settings_group_app">App</string>
|
||||
<string name="settings_group_about">Sobre</string>
|
||||
<string name="settings_appearance_subtitle">Tema, cores, fontes</string>
|
||||
<string name="settings_views_subtitle">Vista, layout e ordem padrão</string>
|
||||
<string name="settings_event_form_subtitle">Campos e comportamento padrão</string>
|
||||
<string name="settings_notifications_subtitle">Lembretes e entrega</string>
|
||||
<string name="settings_special_dates_subtitle">Aniversários e datas de contato</string>
|
||||
<string name="settings_widgets_subtitle">Widget de Agenda, bloco de Ajustes Rápidos</string>
|
||||
<string name="settings_backup_subtitle">Exportar, importar, backup automático</string>
|
||||
<string name="settings_section_widgets">WIdgets e blocos</string>
|
||||
<string name="settings_widgets_hint">Ajustes para widgets da tela inicial e bloco de Ajustes Rápidos. Adicione um widget pressionando e segurando a tela inicial.</string>
|
||||
<string name="settings_section_backup">Backup e restaurar</string>
|
||||
<string name="settings_views_all_header">Todas as vistas</string>
|
||||
<string name="settings_week_day_header">Semana e dia</string>
|
||||
<string name="settings_default_view_hint">A vista que o Calendula abre ao iniciar.</string>
|
||||
<string name="settings_week_start_hint">O dia em que cada semana inicia, em todas as vistas e widgets.</string>
|
||||
<string name="settings_time_format_hint">Como os horários são escritos pelo app. \"Automático\" segue a configuração do sistema.</string>
|
||||
<string name="settings_past_events_hint">O que a agenda faz com eventos que já terminaram.</string>
|
||||
<string name="settings_dynamic_color_summary">Use as cores do app de acordo com seu papel de parede.</string>
|
||||
<string name="settings_section_special_dates">Datas especiais de contatos</string>
|
||||
<string name="settings_special_dates_enable">Mostrar datas de contatos</string>
|
||||
<string name="settings_special_dates_enable_hint">Espelhe os aniversários e outras datas dos contatos para calendários locais. Lê os contatos somente neste aparelho — nada é enviado, e seus contatos nunca são alterados.</string>
|
||||
<string name="settings_special_dates_type_birthday">Aniversários</string>
|
||||
<string name="settings_special_dates_type_anniversary">Aniversários de relacionamento</string>
|
||||
<string name="settings_special_dates_type_custom">Outras datas</string>
|
||||
<string name="settings_special_dates_template">Formato do título</string>
|
||||
<string name="settings_special_dates_template_hint">Use {nome} para o contato e {ano} para o ano (de nascimento, ou de início do relacionamento; oculto quando não informado).</string>
|
||||
<string name="settings_special_dates_reminders">Lembretes</string>
|
||||
<string name="settings_special_dates_show_year">Mostrar ano</string>
|
||||
<string name="settings_special_dates_show_year_hint">Incluir {ano} nos títulos caso informado</string>
|
||||
<string name="settings_special_dates_sync_now">Sincronizar agora</string>
|
||||
<string name="settings_special_dates_never_synced">Não sincronizado ainda</string>
|
||||
<string name="settings_special_dates_last_synced">Sincronizado por último %1$s</string>
|
||||
<string name="settings_special_dates_calendar_hint">Ajustar cada cor e visibilidade de calendário nos ajustes de Calendários.</string>
|
||||
<string name="settings_calendar_reminders_managed_hint">Ajustar em datas especiais de Contatos</string>
|
||||
<string name="settings_special_dates_paused_title">Pausado</string>
|
||||
<string name="settings_special_dates_paused_hint">O Calendula não consegue mais ler seus contatos, então estes calendários não estão atualizando.</string>
|
||||
<string name="settings_special_dates_disable_title">Desligar datas de contatos?</string>
|
||||
<string name="settings_special_dates_disable_all_message">Isto deleta calendários de datas de contatos e seus eventos. Qualquer lembrete ou nota adicionada serão perdidos.</string>
|
||||
<string name="settings_special_dates_disable_type_message">Isto deleta o calendário \"%1$s\" e seus eventos. Qualquer lembrete ou nota adicionados serão perdidos.</string>
|
||||
<string name="settings_special_dates_disable_confirm">Desativar</string>
|
||||
<string name="dialog_save">Salvar</string>
|
||||
<string name="settings_section_about">Sobre</string>
|
||||
<string name="settings_license">Licença</string>
|
||||
<string name="settings_license_value">MIT</string>
|
||||
<string name="settings_about_author">por Jean-Luc Makiola</string>
|
||||
<string name="settings_about_source">Código-fonte</string>
|
||||
<string name="settings_about_privacy">Política de privacidade</string>
|
||||
<string name="settings_about_support">Apoie o desenvolvimento</string>
|
||||
<string name="settings_about_version">Versão %1$s</string>
|
||||
<string name="settings_about_logo_desc">Ícone de app do Calendula</string>
|
||||
<string name="settings_report_problem">Reportar um problema</string>
|
||||
<string name="settings_report_problem_hint">Envie um reporte de falha ou abra o tracker de problemas</string>
|
||||
<string name="calendars_title">Calendários</string>
|
||||
<string name="calendars_local_header">Seus calendários</string>
|
||||
<string name="calendars_local_empty">Nenhum calendário local ainda. Crie um para manter eventos somente neste dispositivo.</string>
|
||||
<string name="calendars_add">Adicionar calendário</string>
|
||||
<string name="calendars_visibility_hint">Desative um calendário para escondê-lo neste aparelho — seus eventos desaparecem do app e param de ativar lembretes. Este é o mesmo botão que seus outros apps de calendário usam, então eles também escondem. Nada é deletado, nenhum outro aparelho é afetado, e você pode reativar aqui a qualquer momento.</string>
|
||||
<string name="calendars_visibility_a11y">Mostrar \"%1$s\"</string>
|
||||
<string name="calendars_visibility_notice_title">Alguns calendários estão desativados</string>
|
||||
<string name="calendars_visibility_notice_message">O Calendula mostra os calendários que estão ativados neste dispositivo, e alguns dos seus estão desativados. Para ver seus eventos, ative-os em Ajustes → Calendários.</string>
|
||||
<string name="calendar_picker_missing_title">Sentindo falta de um calendário?</string>
|
||||
<string name="calendar_picker_missing_summary">Pode estar desativado, somente-leitura, ou preenchido dos seus contatos — gerencie seus calendários aqui.</string>
|
||||
<string name="calendars_state_read_only">Somente leitura</string>
|
||||
<string name="calendars_state_not_synced">Não sincronizado neste dispositivo</string>
|
||||
<string name="calendars_state_managed">Preenchido dos seus contatos</string>
|
||||
<string name="calendars_managed_delete_locked">Este calendário é preenchido de seus contatos, então o Calendula o criaria de novo na próxima sincronização. Desative datas especiais em Ajustes → Datas especiais para deletar.</string>
|
||||
<string name="calendars_synced_header">Calendários sincronizados</string>
|
||||
<string name="calendars_synced_hint">Estes são de contas no seu dispositivo. Crie e edite-os em seus próprios apps.</string>
|
||||
<string name="calendars_manage_in_app">Gerenciar no app</string>
|
||||
<string name="calendars_account_from_source">%1$s (%2$s)</string>
|
||||
<string name="calendars_account_menu_a11y">Mais opções para %1$s</string>
|
||||
<string name="calendars_enable_all">Habilitar todos</string>
|
||||
<string name="calendars_disable_all">Desabilitar todos</string>
|
||||
<string name="calendars_add_account">Adicionar conta</string>
|
||||
<string name="calendars_new_title">Novo calendário</string>
|
||||
<string name="calendars_edit_title">Editar calendário</string>
|
||||
<string name="calendars_name_label">Nome</string>
|
||||
<string name="calendars_color_label">Cor</string>
|
||||
<string name="calendars_description_hint">Adicionar descrição</string>
|
||||
<string name="calendars_delete_confirm_title">Deletar calendário?</string>
|
||||
<string name="calendars_delete_confirm_message">\"%1$s\" e todos os seus eventos serão permanentemente removidos deste dispositivo.</string>
|
||||
<string name="calendars_write_error">Não foi possível salvar a alteração.</string>
|
||||
<string name="calendars_backup_header">Backup</string>
|
||||
<string name="calendars_backup_hint">Calendários locais não são sincronizados, então exporte-os para um arquivo .ics para manter uma cópia.</string>
|
||||
<string name="calendars_backup_action">Exportar como arquivo .ics</string>
|
||||
<string name="calendars_export_title">Exportar calendários</string>
|
||||
<string name="calendars_export_hint">Escolha quais calendários incluir no arquivo .ics.</string>
|
||||
<string name="calendars_export_action">Exportar</string>
|
||||
<string name="calendars_restore_header">Restaurar</string>
|
||||
<string name="calendars_restore_action">Restaurar de um arquivo .ics</string>
|
||||
<string name="calendars_restore_hint">Importar eventos de um backup ou outro app de calendário.</string>
|
||||
<string name="calendars_auto_backup">Backup automático</string>
|
||||
<string name="calendars_auto_backup_hint">Exportar periodicamente seu calendário local para uma pasta como um arquivo .ics.</string>
|
||||
<string name="calendars_auto_backup_folder">Pasta de backup</string>
|
||||
<string name="calendars_auto_backup_folder_unset">Toque para escolher uma pasta</string>
|
||||
<string name="calendars_auto_backup_interval">Intervalo</string>
|
||||
<string name="calendars_auto_backup_every">A cada %1$s</string>
|
||||
<string name="calendars_auto_backup_interval_min">Mínimo de 30 minutos.</string>
|
||||
<string name="calendars_auto_backup_status_never">Nenhum backup automático ainda</string>
|
||||
<string name="calendars_auto_backup_status_ok">Último backup: %1$s</string>
|
||||
<string name="calendars_auto_backup_status_failed">Último backup falho: %1$s</string>
|
||||
<string name="backup_channel_name">Backup</string>
|
||||
<string name="backup_channel_description">Avisa se os backups automáticos falharem repetidamente.</string>
|
||||
<string name="backup_failed_title">Backup automático falhou</string>
|
||||
<string name="backup_failed_text">O Calendula não pôde salvar o arquivo de backup. Verifique a pasta de backup em Ajustes.</string>
|
||||
<string name="calendars_backup_failed">Não foi possível exportar o backup.</string>
|
||||
<plurals name="calendars_backup_done">
|
||||
<item quantity="one">Exportou %d evento.</item>
|
||||
<item quantity="many">Exportou %d eventos.</item>
|
||||
<item quantity="other">Exportou %d eventos.</item>
|
||||
</plurals>
|
||||
<string name="import_title">Importar eventos</string>
|
||||
<string name="import_target_header">Adicionar ao calendário</string>
|
||||
<string name="import_empty">Nenhum evento encontrado neste arquivo.</string>
|
||||
<string name="import_failed">Não foi possível ler este arquivo.</string>
|
||||
<string name="import_no_calendar">Nenhum calendário salvável para importar. Crie um calendário local primeiro.</string>
|
||||
<string name="import_done_title">Importação concluída</string>
|
||||
<string name="import_done_dedup_note">Eventos já no calendário foram pulados.</string>
|
||||
<string name="import_done_added_label">Adicionado</string>
|
||||
<string name="import_done_skipped_label">Duplicatas</string>
|
||||
<string name="import_close">Fechar</string>
|
||||
<string name="import_warning_recurrence">Algumas ocorrências alteradas de eventos recorrentes foram puladas.</string>
|
||||
<string name="import_warning_no_start">Um evento sem horário de início foi pulado.</string>
|
||||
<string name="import_warning_attendees">Listas de convidados não foram importadas.</string>
|
||||
<string name="import_warning_timezone">Um fuso-horário desconhecido foi alterado para o do seu dispositivo.</string>
|
||||
<string name="import_button">Importar</string>
|
||||
<plurals name="import_title_count">
|
||||
<item quantity="one">Importando %d evento</item>
|
||||
<item quantity="many">Importando %d eventos</item>
|
||||
<item quantity="other">Importando %d eventos</item>
|
||||
</plurals>
|
||||
<plurals name="import_event_count">
|
||||
<item quantity="one">%d evento neste arquivo.</item>
|
||||
<item quantity="many">%d eventos neste arquivo.</item>
|
||||
<item quantity="other">%d eventos neste arquivo.</item>
|
||||
</plurals>
|
||||
<plurals name="import_action">
|
||||
<item quantity="one">Importar %d evento</item>
|
||||
<item quantity="many">Importar %d eventos</item>
|
||||
<item quantity="other">Importar %d eventos</item>
|
||||
</plurals>
|
||||
<plurals name="import_done_imported">
|
||||
<item quantity="one">Importou %d evento.</item>
|
||||
<item quantity="many">Importou %d eventos.</item>
|
||||
<item quantity="other">Importou %d eventos.</item>
|
||||
</plurals>
|
||||
<plurals name="import_done_skipped">
|
||||
<item quantity="one">Pulou %d já neste calendário.</item>
|
||||
<item quantity="many">Pulou %d já neste calendário.</item>
|
||||
<item quantity="other">Pulou %d já neste calendário.</item>
|
||||
</plurals>
|
||||
<string name="shortcut_new_event_short">Novo evento</string>
|
||||
<string name="shortcut_new_event_long">Criar novo evento</string>
|
||||
<string name="qs_tile_new_event_label">Novo evento</string>
|
||||
<string name="settings_qs_tile">Adicionar bloco Ajustes Rápidos</string>
|
||||
<string name="settings_qs_tile_hint">Adicionar bloo \"Novo evento\" no painel Ajustes Rápidos.</string>
|
||||
<string name="crash_dialog_title">%1$s quebrou</string>
|
||||
<string name="crash_dialog_message">%1$s fechou inesperadamente da última vez. Você pode ajudar a consertar enviando este reporte como um problema. Ele fica no seu dispositivo até que decida enviá-lo, e não inclui dados pessoais ou conteúdo de calendário — somente os detalhes técnicos abaixo.</string>
|
||||
<string name="crash_dialog_report">Reportar</string>
|
||||
<string name="crash_dialog_dismiss">Agora não</string>
|
||||
<string name="crash_report_issue_title">Reportar quebra</string>
|
||||
<string name="crash_report_clip_label">%1$s relatório de quebra</string>
|
||||
<string name="crash_report_copied">Relatório copiado para a área de transferência</string>
|
||||
<string name="crash_report_open_failed">Não foi possível abrir o tracker de problemas. O relatório está na área de transferência.</string>
|
||||
<string name="crash_report_body_template">Obrigado por reportar uma quebra em %1$s. Por favor, escreva o que estava fazendo, depois envie. \n### O que aconteceu \n\n\n### Relatório de quebra \n%2$s\n</string>
|
||||
<string name="crash_report_body_paste">_(O reporte era muito longo para este link — cole da área de transferência aqui.)_</string>
|
||||
<string name="special_dates_calendar_birthday">Aniversários</string>
|
||||
<string name="special_dates_calendar_anniversary">Aniversários de relacionamento</string>
|
||||
<string name="special_dates_calendar_custom">Datas especiais</string>
|
||||
<string name="special_dates_default_title_birthday">Aniversário de {nome} ({ano})</string>
|
||||
<string name="special_dates_default_title_anniversary">Aniversário de relacionamento de {nome} ({ano})</string>
|
||||
<string name="special_dates_default_title_custom">{nome}</string>
|
||||
</resources>
|
||||
|
||||
@@ -10,11 +10,14 @@
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<locale android:name="en" />
|
||||
<locale android:name="ar" />
|
||||
<locale android:name="cs" />
|
||||
<locale android:name="de" />
|
||||
<locale android:name="es" />
|
||||
<locale android:name="fr" />
|
||||
<locale android:name="it" />
|
||||
<locale android:name="ja" />
|
||||
<locale android:name="pl" />
|
||||
<locale android:name="pt-BR" />
|
||||
<locale android:name="ru" />
|
||||
<locale android:name="zh-CN" />
|
||||
</locale-config>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.jeanlucmakiola.calendula.data.calendar
|
||||
|
||||
import android.provider.CalendarContract
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlin.time.Instant
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -17,6 +18,7 @@ class InstanceMapperTest {
|
||||
eventColor: Any? = null,
|
||||
calendarColor: Int = 0xFFAABBCC.toInt(),
|
||||
location: String? = null,
|
||||
selfAttendeeStatus: Int = CalendarContract.Attendees.ATTENDEE_STATUS_NONE,
|
||||
): MapColumnReader = MapColumnReader(
|
||||
InstanceProjection.IDX_INSTANCE_ID to instanceId,
|
||||
InstanceProjection.IDX_EVENT_ID to eventId,
|
||||
@@ -28,6 +30,7 @@ class InstanceMapperTest {
|
||||
InstanceProjection.IDX_EVENT_COLOR to eventColor,
|
||||
InstanceProjection.IDX_CALENDAR_COLOR to calendarColor,
|
||||
InstanceProjection.IDX_LOCATION to location,
|
||||
InstanceProjection.IDX_SELF_ATTENDEE_STATUS to selfAttendeeStatus,
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -90,4 +93,20 @@ class InstanceMapperTest {
|
||||
val inst = reader(location = "Berlin").toEventInstance()
|
||||
assertThat(inst!!.location).isEqualTo("Berlin")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a declined invitation is marked, any other answer is not`() {
|
||||
assertThat(reader().toEventInstance()!!.isDeclined).isFalse()
|
||||
assertThat(
|
||||
reader(selfAttendeeStatus = CalendarContract.Attendees.ATTENDEE_STATUS_DECLINED)
|
||||
.toEventInstance()!!.isDeclined,
|
||||
).isTrue()
|
||||
listOf(
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_ACCEPTED,
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_TENTATIVE,
|
||||
CalendarContract.Attendees.ATTENDEE_STATUS_INVITED,
|
||||
).forEach { status ->
|
||||
assertThat(reader(selfAttendeeStatus = status).toEventInstance()!!.isDeclined).isFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package de.jeanlucmakiola.calendula.ui.detail
|
||||
|
||||
import android.content.ContextWrapper
|
||||
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepositoryImpl
|
||||
import de.jeanlucmakiola.calendula.data.calendar.FakeCalendarDataSource
|
||||
import de.jeanlucmakiola.calendula.data.ics.IcsExporter
|
||||
import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs
|
||||
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.calendula.domain.CalendarSource
|
||||
import de.jeanlucmakiola.calendula.domain.EventDetail
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
import kotlin.time.Instant
|
||||
|
||||
/**
|
||||
* Re-opening an occurrence must re-read it (#196): the view model outlives the
|
||||
* sheet, so an edit that changed no time would otherwise show the pre-save row.
|
||||
* The re-read stays silent — the loaded content must not blink back to the
|
||||
* skeleton on the way.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class EventDetailViewModelTest {
|
||||
|
||||
private val dispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
@BeforeEach fun setUp() = Dispatchers.setMain(dispatcher)
|
||||
@AfterEach fun tearDown() = Dispatchers.resetMain()
|
||||
|
||||
private val beginMillis = 1_781_164_800_000L
|
||||
private val endMillis = beginMillis + 3_600_000L
|
||||
|
||||
private fun detail(description: String?) = EventDetail(
|
||||
instance = EventInstance(
|
||||
instanceId = 42L, eventId = 42L, calendarId = 1L, title = "Standup",
|
||||
start = Instant.fromEpochMilliseconds(beginMillis),
|
||||
end = Instant.fromEpochMilliseconds(endMillis),
|
||||
isAllDay = false, color = 0xFF000000.toInt(), location = null,
|
||||
),
|
||||
description = description, organizer = null, attendees = emptyList(), rrule = null,
|
||||
)
|
||||
|
||||
private fun viewModel(tempDir: Path, fake: FakeCalendarDataSource): EventDetailViewModel {
|
||||
val prefs = CalendarPrefs(
|
||||
PreferenceDataStoreFactory.create(
|
||||
scope = CoroutineScope(dispatcher),
|
||||
produceFile = { tempDir.resolve("detail_prefs.preferences_pb").toFile() },
|
||||
),
|
||||
)
|
||||
val settings = SettingsPrefs(
|
||||
PreferenceDataStoreFactory.create(
|
||||
scope = CoroutineScope(dispatcher),
|
||||
produceFile = { tempDir.resolve("detail_settings.preferences_pb").toFile() },
|
||||
),
|
||||
)
|
||||
val repo = CalendarRepositoryImpl(fake, prefs, settings, dispatcher as CoroutineDispatcher)
|
||||
// Only `shareUri()` touches the exporter, and nothing here shares.
|
||||
return EventDetailViewModel(repo, IcsExporter(ContextWrapper(null)), dispatcher)
|
||||
}
|
||||
|
||||
private fun fakeSource(description: () -> String?) = FakeCalendarDataSource().apply {
|
||||
calendarsResult = listOf(
|
||||
CalendarSource(
|
||||
id = 1L, displayName = "Cal", accountName = "acc@local", accountType = "LOCAL",
|
||||
color = 0xFF112233.toInt(), isVisibleInSystem = true, canModifyContents = true,
|
||||
),
|
||||
)
|
||||
eventDetailResult = { detail(description()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `re-opening the same occurrence re-reads it`(@TempDir tempDir: Path) = runTest(dispatcher) {
|
||||
var stored: String? = null
|
||||
val vm = viewModel(tempDir, fakeSource { stored })
|
||||
val collector = launch(Job()) { vm.state.collect {} }
|
||||
|
||||
vm.open(42L, beginMillis, endMillis)
|
||||
advanceUntilIdle()
|
||||
assertThat((vm.state.value as EventDetailUiState.Success).detail.description).isNull()
|
||||
|
||||
// The edit screen saved a description; the tapped occurrence is unchanged.
|
||||
stored = "Bring the roadmap"
|
||||
vm.open(42L, beginMillis, endMillis)
|
||||
advanceUntilIdle()
|
||||
assertThat((vm.state.value as EventDetailUiState.Success).detail.description)
|
||||
.isEqualTo("Bring the roadmap")
|
||||
|
||||
collector.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the re-read does not fall back to the skeleton`(@TempDir tempDir: Path) = runTest(dispatcher) {
|
||||
val vm = viewModel(tempDir, fakeSource { null })
|
||||
val seen = mutableListOf<EventDetailUiState>()
|
||||
val collector = launch(Job()) { vm.state.collect { seen += it } }
|
||||
|
||||
vm.open(42L, beginMillis, endMillis)
|
||||
advanceUntilIdle()
|
||||
assertThat(vm.state.value).isInstanceOf(EventDetailUiState.Success::class.java)
|
||||
|
||||
seen.clear()
|
||||
vm.open(42L, beginMillis, endMillis)
|
||||
advanceUntilIdle()
|
||||
assertThat(seen).doesNotContain(EventDetailUiState.Loading)
|
||||
|
||||
collector.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package de.jeanlucmakiola.calendula.ui.month
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import de.jeanlucmakiola.calendula.domain.EventInstance
|
||||
import kotlinx.datetime.DateTimeUnit
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.Month
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.YearMonth
|
||||
import kotlinx.datetime.atTime
|
||||
import kotlinx.datetime.plus
|
||||
import kotlinx.datetime.toInstant
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Which chip a tap in a month cell lands on (#187) — the geometry the tap layer
|
||||
* uses to tell "open this event" from "open this day", given that the chips take
|
||||
* no pointer input of their own.
|
||||
*/
|
||||
class ChipAtCellYTest {
|
||||
|
||||
private val zone = TimeZone.UTC
|
||||
private val jul26 = YearMonth(2026, Month.JULY)
|
||||
|
||||
/** Band starts 40px down the cell; each lane is 20px tall. */
|
||||
private val bandTop = 40f
|
||||
private val laneHeight = 20f
|
||||
|
||||
/** July 2026 starts on a Wednesday, so this row — Jul 6–12 — sits wholly inside it. */
|
||||
private fun rowOfJuly6(events: List<EventInstance>) =
|
||||
layoutMonthWeeks(jul26, DayOfWeek.MONDAY, events, zone)[1]
|
||||
|
||||
private fun allDay(from: LocalDate, toInclusive: LocalDate, id: Long) = EventInstance(
|
||||
instanceId = id,
|
||||
eventId = id,
|
||||
calendarId = 1L,
|
||||
title = "A$id",
|
||||
start = from.atTime(0, 0).toInstant(zone),
|
||||
end = toInclusive.plus(1, DateTimeUnit.DAY).atTime(0, 0).toInstant(zone),
|
||||
isAllDay = true,
|
||||
color = 0xFF2196F3.toInt(),
|
||||
location = null,
|
||||
)
|
||||
|
||||
private fun timed(date: LocalDate, hour: Int, id: Long) = EventInstance(
|
||||
instanceId = id,
|
||||
eventId = id,
|
||||
calendarId = 1L,
|
||||
title = "T$id",
|
||||
start = date.atTime(hour, 0).toInstant(zone),
|
||||
end = date.atTime(hour + 1, 0).toInstant(zone),
|
||||
isAllDay = false,
|
||||
color = 0xFFF44336.toInt(),
|
||||
location = null,
|
||||
)
|
||||
|
||||
private fun MonthWeek.chipAt(col: Int, cellY: Float) =
|
||||
chipAtCellY(col = col, cellY = cellY, bandTopInCell = bandTop, rowHeightPx = laneHeight)
|
||||
|
||||
@Test
|
||||
fun `a tap on a lane resolves to the chip seated there`() {
|
||||
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
||||
val meeting = timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)
|
||||
val week = rowOfJuly6(listOf(bar, meeting))
|
||||
|
||||
// Jul 7 is column 1 of a Monday-anchored row starting Jul 6.
|
||||
assertThat(week.chipAt(col = 1, cellY = bandTop + 5f)?.eventId).isEqualTo(1L)
|
||||
assertThat(week.chipAt(col = 1, cellY = bandTop + laneHeight + 5f)?.eventId).isEqualTo(2L)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a multi-day bar answers on every column it covers`() {
|
||||
val bar = allDay(LocalDate(2026, 7, 7), LocalDate(2026, 7, 9), id = 1L)
|
||||
val week = rowOfJuly6(listOf(bar))
|
||||
|
||||
(1..3).forEach { col ->
|
||||
assertThat(week.chipAt(col = col, cellY = bandTop + 5f)?.eventId).isEqualTo(1L)
|
||||
}
|
||||
// Jul 10 is past the bar's last day.
|
||||
assertThat(week.chipAt(col = 4, cellY = bandTop + 5f)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a tap above the band is the day number, not a chip`() {
|
||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)))
|
||||
|
||||
assertThat(week.chipAt(col = 1, cellY = bandTop - 1f)).isNull()
|
||||
assertThat(week.chipAt(col = 1, cellY = 0f)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a tap on an empty lane of a day that has chips falls through to the day`() {
|
||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)))
|
||||
|
||||
assertThat(week.chipAt(col = 1, cellY = bandTop + laneHeight * 2 + 5f)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a tap on the overflow row opens the day rather than a hidden event`() {
|
||||
val events = (1..MAX_EVENT_ROWS + 2).map {
|
||||
timed(LocalDate(2026, 7, 7), hour = it, id = it.toLong())
|
||||
}
|
||||
val week = rowOfJuly6(events)
|
||||
|
||||
// The dots sit one lane below the last one the row draws.
|
||||
val overflowY = bandTop + laneHeight * MAX_EVENT_ROWS + 2f
|
||||
assertThat(week.chipAt(col = 1, cellY = overflowY)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unmeasured geometry resolves to no chip`() {
|
||||
val week = rowOfJuly6(listOf(timed(LocalDate(2026, 7, 7), hour = 9, id = 2L)))
|
||||
|
||||
assertThat(
|
||||
week.chipAtCellY(col = 1, cellY = 45f, bandTopInCell = null, rowHeightPx = laneHeight),
|
||||
).isNull()
|
||||
assertThat(
|
||||
week.chipAtCellY(col = 1, cellY = 45f, bandTopInCell = bandTop, rowHeightPx = 0f),
|
||||
).isNull()
|
||||
}
|
||||
}
|
||||
8
fastlane/metadata/android/ar/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/ar/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
إصلاحات
|
||||
• النقر على حدث في عرض الشهر يفتح الحدث نفسه بدل يومه.
|
||||
• التعديل يظهر فور إعادة فتح الحدث.
|
||||
• لم تعد أسهم أداة الشهر تتوقف بعد بضع نقرات: تحديث ضخم كان يُسقط مضيف الأدوات في المشغّل.
|
||||
• «اليوم» في الأسابيع المتصلة يصل إلى الأسبوع الحالي تماماً، وعادت مسافة الهامش على يسار عرضي اليوم والأسبوع.
|
||||
|
||||
تغييرات
|
||||
• الحدث الذي رفضته يظهر مشطوباً في كل مكان ولم يعد يُذكّرك به.
|
||||
8
fastlane/metadata/android/de-DE/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/de-DE/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Behoben
|
||||
• Ein Tippen auf einen Termin in der Monatsansicht öffnet ihn statt des Tages.
|
||||
• Eine Änderung ist sofort sichtbar, wenn der Termin neu geöffnet wird.
|
||||
• Die Pfeile des Monats-Widgets sterben nicht mehr nach wenigen Tipps – eine zu große Aktualisierung riss den Widget-Host mit.
|
||||
• „Heute“ springt in nahtlosen Wochen genau auf die aktuelle Woche; Wochen- und Tagesansicht haben rechts wieder Abstand.
|
||||
|
||||
Geändert
|
||||
• Ein abgelehnter Termin ist überall durchgestrichen und erinnert nicht mehr.
|
||||
8
fastlane/metadata/android/en-GB/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/en-GB/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Fixed
|
||||
• Tapping an event in month view now opens the event instead of its day.
|
||||
• An edit shows straight away when you re-open the event.
|
||||
• The month widget's arrows no longer die after a few taps — an oversized update was killing the launcher's widget host.
|
||||
• Jump-to-today in seamless weeks lands on the current week, and day/week view no longer run flush against the right edge.
|
||||
|
||||
Changed
|
||||
• A meeting you declined is struck through everywhere and no longer reminds you.
|
||||
8
fastlane/metadata/android/en-US/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Fixed
|
||||
• Tapping an event in month view now opens the event instead of its day.
|
||||
• An edit shows straight away when you re-open the event.
|
||||
• The month widget's arrows no longer die after a few taps — an oversized update was killing the launcher's widget host.
|
||||
• Jump-to-today in seamless weeks lands on the current week, and day/week view no longer run flush against the right edge.
|
||||
|
||||
Changed
|
||||
• A meeting you declined is struck through everywhere and no longer reminds you.
|
||||
5
fastlane/metadata/android/en-US/changelogs/21903.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/21903.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Added
|
||||
• Calendula now speaks Czech, Japanese and Russian, contributed by community translators on Weblate. All three are early, so untranslated parts still show in English — pick one under Settings → Language.
|
||||
|
||||
Changed
|
||||
• The Brazilian Portuguese translation is now complete.
|
||||
8
fastlane/metadata/android/es-ES/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/es-ES/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Corregido
|
||||
• Tocar un evento en la vista de mes abre el evento, no su día.
|
||||
• Un cambio se ve en cuanto vuelves a abrir el evento.
|
||||
• Las flechas del widget de mes ya no se quedan muertas tras unos toques: una actualización demasiado grande tumbaba el host de widgets del launcher.
|
||||
• «Hoy» en semanas continuas llega justo a la semana actual, y las vistas de día y semana vuelven a tener margen a la derecha.
|
||||
|
||||
Cambios
|
||||
• Un evento que has rechazado aparece tachado en todas partes y ya no avisa.
|
||||
8
fastlane/metadata/android/fr-FR/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/fr-FR/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Corrigé
|
||||
• Appuyer sur un événement en vue mois ouvre l'événement, pas sa journée.
|
||||
• Une modification apparaît dès que l'événement est rouvert.
|
||||
• Les flèches du widget mois ne meurent plus après quelques appuis : une mise à jour trop lourde emportait l'hôte de widgets du lanceur.
|
||||
• « Aujourd'hui » en semaines continues arrive pile sur la semaine en cours ; les vues jour et semaine ont de nouveau une marge à droite.
|
||||
|
||||
Modifié
|
||||
• Un événement refusé est barré partout et ne vous rappelle plus rien.
|
||||
8
fastlane/metadata/android/it-IT/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/it-IT/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Corretto
|
||||
• Toccare un evento nella vista mese apre l'evento, non il suo giorno.
|
||||
• Una modifica si vede subito riaprendo l'evento.
|
||||
• Le frecce del widget mese non muoiono più dopo pochi tocchi: un aggiornamento troppo grande abbatteva l'host dei widget del launcher.
|
||||
• «Oggi» nelle settimane continue arriva esattamente sulla settimana corrente, e le viste giorno e settimana hanno di nuovo un margine a destra.
|
||||
|
||||
Modifiche
|
||||
• Un evento rifiutato è barrato ovunque e non invia più promemoria.
|
||||
8
fastlane/metadata/android/pl-PL/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/pl-PL/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Poprawki
|
||||
• Dotknięcie wydarzenia w widoku miesiąca otwiera wydarzenie, a nie jego dzień.
|
||||
• Zmiana jest widoczna od razu po ponownym otwarciu wydarzenia.
|
||||
• Strzałki widżetu miesiąca nie zamierają już po kilku dotknięciach – zbyt duża aktualizacja kładła host widżetów launchera.
|
||||
• „Dziś” w ciągłych tygodniach trafia dokładnie w bieżący tydzień, a widoki dnia i tygodnia znów mają margines z prawej.
|
||||
|
||||
Zmiany
|
||||
• Odrzucone wydarzenie jest wszędzie przekreślone i nie przypomina o sobie.
|
||||
8
fastlane/metadata/android/pt-BR/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/pt-BR/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Corrigido
|
||||
• Tocar em um evento na visualização de mês abre o evento, e não o seu dia.
|
||||
• Uma alteração aparece assim que o evento é reaberto.
|
||||
• As setas do widget de mês não morrem mais depois de alguns toques: uma atualização grande demais derrubava o host de widgets do launcher.
|
||||
• "Hoje" nas semanas contínuas chega exatamente na semana atual, e as visualizações de dia e semana voltaram a ter margem à direita.
|
||||
|
||||
Alterações
|
||||
• Um evento recusado fica riscado em todo lugar e não lembra mais você.
|
||||
8
fastlane/metadata/android/pt-PT/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/pt-PT/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Corrigido
|
||||
• Tocar num evento na vista de mês abre o evento e não o seu dia.
|
||||
• Uma alteração aparece assim que o evento é reaberto.
|
||||
• As setas do widget de mês já não morrem ao fim de alguns toques: uma atualização demasiado grande derrubava o anfitrião de widgets do launcher.
|
||||
• «Hoje» nas semanas contínuas chega mesmo à semana atual, e as vistas de dia e semana voltam a ter margem à direita.
|
||||
|
||||
Alterações
|
||||
• Um evento recusado fica riscado em todo o lado e deixa de lembrar.
|
||||
8
fastlane/metadata/android/ru-RU/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/ru-RU/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
Исправлено
|
||||
• Нажатие на событие в виде месяца открывает событие, а не его день.
|
||||
• Изменение видно сразу при повторном открытии события.
|
||||
• Стрелки виджета месяца больше не отмирают после нескольких нажатий: слишком большое обновление роняло хост виджетов лаунчера.
|
||||
• «Сегодня» в непрерывных неделях попадает точно на текущую неделю, а у видов дня и недели снова есть отступ справа.
|
||||
|
||||
Изменения
|
||||
• Отклонённое событие везде зачёркнуто и больше не напоминает о себе.
|
||||
8
fastlane/metadata/android/zh-CN/changelogs/21902.txt
Normal file
8
fastlane/metadata/android/zh-CN/changelogs/21902.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
修复
|
||||
• 在月视图中点按事件现在会打开该事件本身,而不是它所在的那一天。
|
||||
• 编辑后再次打开事件,改动会立即显示。
|
||||
• 月视图小部件的箭头不再点几下就失灵:过大的更新会拖垮启动器的小部件宿主。
|
||||
• 连续周视图中的“今天”会精确定位到本周,日视图和周视图右侧也重新留出了间距。
|
||||
|
||||
变更
|
||||
• 已拒绝的事件在各处均以删除线标记,并且不再提醒。
|
||||
@@ -18,7 +18,7 @@ junitPlatform = "6.1.2"
|
||||
truth = "1.4.5"
|
||||
androidxJunit = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
kotlinxDatetime = "0.7.0"
|
||||
kotlinxDatetime = "0.8.0-0.6.x-compat"
|
||||
kotlinxCoroutines = "1.11.0"
|
||||
turbine = "1.2.1"
|
||||
hiltNavigationCompose = "1.4.0"
|
||||
|
||||
Reference in New Issue
Block a user