Compare commits
18 Commits
renovate/c
...
release/v2
| Author | SHA1 | Date | |
|---|---|---|---|
| cf3b897305 | |||
| 87a78ca924 | |||
| 13debac340 | |||
| 76846420a2 | |||
| 65bd6c4254 | |||
| 1a3e4f501f | |||
| 974d65f619 | |||
|
|
721ca0d7e0 | ||
|
|
2556eba84b | ||
|
|
fc1ff97d22 | ||
|
|
b7f3d421ef | ||
|
|
2722b3427a | ||
|
|
e28c293d27 | ||
|
|
5cb2a836ab | ||
|
|
b5978e7a61 | ||
| c4a048adee | |||
|
|
2746ca5784 | ||
| fc9bd33992 |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.13.1] — 2026-07-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Create events from other apps and widgets. Calendula now registers the
|
||||||
|
standard "insert event" intent (`ACTION_INSERT` on the calendar events type),
|
||||||
|
so other apps and home-screen widgets — such as the Todo Agenda widget — can
|
||||||
|
hand off to Calendula to create a new event. It opens the new-event form
|
||||||
|
prefilled with whatever they passed (title, start/end time, all-day, location,
|
||||||
|
description, recurrence), and picks your last-used or first writable calendar.
|
||||||
|
Thanks to @dschuermann for the suggestion ([#30]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Some recurring events could not be opened. Events in a series that started
|
||||||
|
before 1970 — for example yearly birthdays or anniversaries synced over CalDAV
|
||||||
|
— showed "Something went wrong" instead of opening, because their stored start
|
||||||
|
time is a negative value that was wrongly treated as invalid. They now open
|
||||||
|
normally and appear in search again. A related case (an event whose stored end
|
||||||
|
precedes its start) is now kept and openable instead of failing the same way.
|
||||||
|
Thanks to @dschuermann for the report ([#34]).
|
||||||
|
- The time picker now follows your 24-hour setting. With Calendula set to
|
||||||
|
24-hour time, the clock dial for choosing an event's start and end time still
|
||||||
|
showed AM/PM instead of a 24-hour dial; it now matches your setting (and the
|
||||||
|
same fix applies to the all-day reminder time in Settings). Thanks to
|
||||||
|
@abrossimow for the report ([#27]).
|
||||||
|
|
||||||
## [2.13.0] — 2026-07-03
|
## [2.13.0] — 2026-07-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -809,4 +834,7 @@ automatically, with zero telemetry and no internet permission.
|
|||||||
[#19]: https://codeberg.org/jlmakiola/calendula/issues/19
|
[#19]: https://codeberg.org/jlmakiola/calendula/issues/19
|
||||||
[#20]: https://codeberg.org/jlmakiola/calendula/issues/20
|
[#20]: https://codeberg.org/jlmakiola/calendula/issues/20
|
||||||
[#24]: https://codeberg.org/jlmakiola/calendula/issues/24
|
[#24]: https://codeberg.org/jlmakiola/calendula/issues/24
|
||||||
|
[#27]: https://codeberg.org/jlmakiola/calendula/issues/27
|
||||||
[#29]: https://codeberg.org/jlmakiola/calendula/issues/29
|
[#29]: https://codeberg.org/jlmakiola/calendula/issues/29
|
||||||
|
[#30]: https://codeberg.org/jlmakiola/calendula/issues/30
|
||||||
|
[#34]: https://codeberg.org/jlmakiola/calendula/issues/34
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ android {
|
|||||||
// which builds this version and then creates the matching vX.Y.Z tag +
|
// which builds this version and then creates the matching vX.Y.Z tag +
|
||||||
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
// release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 +
|
||||||
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
// PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md.
|
||||||
versionCode = 21300
|
versionCode = 21301
|
||||||
versionName = "2.13.0"
|
versionName = "2.13.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,17 @@
|
|||||||
<data android:mimeType="text/calendar" />
|
<data android:mimeType="text/calendar" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Let another app or widget (e.g. the Todo Agenda widget) launch us
|
||||||
|
to create a new event, the way the AOSP calendar accepts it:
|
||||||
|
ACTION_INSERT on the events dir mime type, carrying the new
|
||||||
|
event's fields as CalendarContract extras
|
||||||
|
(MainActivity.insertFormOrNull, issue #30). -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.INSERT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="vnd.android.cursor.dir/event" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
<!-- Launcher long-press shortcuts (e.g. "New event"). -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.CalendarContract
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@@ -25,6 +26,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
import de.jeanlucmakiola.calendula.data.crash.CrashReporter
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
|
||||||
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
|
||||||
|
import de.jeanlucmakiola.calendula.domain.EventForm
|
||||||
|
import de.jeanlucmakiola.calendula.domain.buildInsertEventForm
|
||||||
import de.jeanlucmakiola.calendula.ui.RootScreen
|
import de.jeanlucmakiola.calendula.ui.RootScreen
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines
|
||||||
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat
|
||||||
@@ -42,6 +45,7 @@ import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
|
|||||||
import kotlinx.datetime.LocalDate
|
import kotlinx.datetime.LocalDate
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import kotlinx.datetime.toLocalDateTime
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -60,6 +64,11 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// by CalendarHost's import flow.
|
// by CalendarHost's import flow.
|
||||||
private var requestedImportUri by mutableStateOf<Uri?>(null)
|
private var requestedImportUri by mutableStateOf<Uri?>(null)
|
||||||
|
|
||||||
|
// A prefilled new-event form from an external ACTION_INSERT launch (another
|
||||||
|
// app/widget asking us to create an event, issue #30). Consumed once by
|
||||||
|
// CalendarHost, which opens it in the create form for review.
|
||||||
|
private var requestedInsertForm by mutableStateOf<EventForm?>(null)
|
||||||
|
|
||||||
// A captured crash report awaiting the user's decision, surfaced as a dialog
|
// A captured crash report awaiting the user's decision, surfaced as a dialog
|
||||||
// over the calendar on the next launch (the single-crash path). A startup
|
// over the calendar on the next launch (the single-crash path). A startup
|
||||||
// crash-loop is handled out of band, before setContent — see below.
|
// crash-loop is handled out of band, before setContent — see below.
|
||||||
@@ -84,6 +93,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
requestedDetailKey = intent.detailKeyOrNull()
|
requestedDetailKey = intent.detailKeyOrNull()
|
||||||
requestedNav = intent.navRequestOrNull()
|
requestedNav = intent.navRequestOrNull()
|
||||||
requestedImportUri = intent.importUriOrNull()
|
requestedImportUri = intent.importUriOrNull()
|
||||||
|
requestedInsertForm = intent.insertFormOrNull()
|
||||||
if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this)
|
if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this)
|
||||||
setContent {
|
setContent {
|
||||||
// One activity-scoped SettingsViewModel drives both the theme here
|
// One activity-scoped SettingsViewModel drives both the theme here
|
||||||
@@ -132,6 +142,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
onWidgetNavConsumed = { requestedNav = null },
|
onWidgetNavConsumed = { requestedNav = null },
|
||||||
requestedImportUri = requestedImportUri,
|
requestedImportUri = requestedImportUri,
|
||||||
onImportConsumed = { requestedImportUri = null },
|
onImportConsumed = { requestedImportUri = null },
|
||||||
|
requestedInsertForm = requestedInsertForm,
|
||||||
|
onInsertConsumed = { requestedInsertForm = null },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// A persistent corner marker so a debug build is never
|
// A persistent corner marker so a debug build is never
|
||||||
@@ -169,6 +181,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
intent.detailKeyOrNull()?.let { requestedDetailKey = it }
|
intent.detailKeyOrNull()?.let { requestedDetailKey = it }
|
||||||
intent.navRequestOrNull()?.let { requestedNav = it }
|
intent.navRequestOrNull()?.let { requestedNav = it }
|
||||||
intent.importUriOrNull()?.let { requestedImportUri = it }
|
intent.importUriOrNull()?.let { requestedImportUri = it }
|
||||||
|
intent.insertFormOrNull()?.let { requestedInsertForm = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,6 +201,31 @@ class MainActivity : AppCompatActivity() {
|
|||||||
return uri.takeIf { it.scheme == "content" || it.scheme == "file" }
|
return uri.takeIf { it.scheme == "content" || it.scheme == "file" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A prefilled new-event form from an external `ACTION_INSERT` launch — another
|
||||||
|
* app or widget (e.g. Todo Agenda) asking us to create an event (issue #30).
|
||||||
|
* The new event's fields ride as CalendarContract extras; anything omitted
|
||||||
|
* falls back to the in-app "new event" defaults in [buildInsertEventForm].
|
||||||
|
*/
|
||||||
|
private fun Intent.insertFormOrNull(): EventForm? {
|
||||||
|
if (action != Intent.ACTION_INSERT) return null
|
||||||
|
return buildInsertEventForm(
|
||||||
|
beginMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_BEGIN_TIME),
|
||||||
|
endMillis = longExtraOrNull(CalendarContract.EXTRA_EVENT_END_TIME),
|
||||||
|
isAllDay = getBooleanExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, false),
|
||||||
|
title = getStringExtra(CalendarContract.Events.TITLE),
|
||||||
|
description = getStringExtra(CalendarContract.Events.DESCRIPTION),
|
||||||
|
location = getStringExtra(CalendarContract.Events.EVENT_LOCATION),
|
||||||
|
rrule = getStringExtra(CalendarContract.Events.RRULE),
|
||||||
|
zone = TimeZone.currentSystemDefault(),
|
||||||
|
now = Clock.System.now(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A Long extra's value, or null when the extra is absent. */
|
||||||
|
private fun Intent.longExtraOrNull(key: String): Long? =
|
||||||
|
if (hasExtra(key)) getLongExtra(key, 0L) else null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date a launcher/clock date tap points at, parsed from the AOSP calendar
|
* The date a launcher/clock date tap points at, parsed from the AOSP calendar
|
||||||
* "view time" intent: ACTION_VIEW on `content://com.android.calendar/time/
|
* "view time" intent: ACTION_VIEW on `content://com.android.calendar/time/
|
||||||
|
|||||||
@@ -23,26 +23,29 @@ internal fun ColumnReader.toEventDetailCore(
|
|||||||
attendees: List<Attendee>,
|
attendees: List<Attendee>,
|
||||||
reminders: List<Reminder>,
|
reminders: List<Reminder>,
|
||||||
): EventDetail? {
|
): EventDetail? {
|
||||||
val begin = getLong(EventDetailProjection.IDX_DTSTART)
|
// DTSTART is epoch millis in UTC, so a series anchored before 1970 (common
|
||||||
|
// for yearly birthdays/anniversaries synced over CalDAV) is legitimately
|
||||||
if (begin < 0L) {
|
// negative — only an *absent* DTSTART marks a malformed row worth dropping.
|
||||||
Log.w(TAG, "Dropping event with negative dtstart=$begin")
|
// Dropping negatives made every occurrence of such a series un-openable
|
||||||
|
// (the detail loads the ancient series-master DTSTART), see issue #34.
|
||||||
|
if (isNull(EventDetailProjection.IDX_DTSTART)) {
|
||||||
|
Log.w(TAG, "Dropping event with missing dtstart")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
val begin = getLong(EventDetailProjection.IDX_DTSTART)
|
||||||
|
|
||||||
// Recurring events store DURATION instead of DTEND, so the series row's
|
// Recurring events store DURATION instead of DTEND, so the series row's
|
||||||
// DTEND is null. Keep the event (end == begin); callers that opened a
|
// DTEND is null. Keep the event (end == begin); callers that opened a
|
||||||
// specific occurrence supply the real per-occurrence times from
|
// specific occurrence supply the real per-occurrence times from
|
||||||
// CalendarContract.Instances. Only a present-but-backwards DTEND is malformed.
|
// CalendarContract.Instances. A present-but-backwards DTEND is malformed,
|
||||||
|
// but dropping the row would make the event un-openable — the same trap as
|
||||||
|
// the pre-1970 DTSTART bug above (issue #34): it would surface as the
|
||||||
|
// generic error screen with no way to open the event and fix it. Clamp to a
|
||||||
|
// zero-length event instead (matching SearchMapper's coerceAtLeast).
|
||||||
val end = if (isNull(EventDetailProjection.IDX_DTEND)) {
|
val end = if (isNull(EventDetailProjection.IDX_DTEND)) {
|
||||||
begin
|
begin
|
||||||
} else {
|
} else {
|
||||||
val rawEnd = getLong(EventDetailProjection.IDX_DTEND)
|
getLong(EventDetailProjection.IDX_DTEND).coerceAtLeast(begin)
|
||||||
if (rawEnd < begin) {
|
|
||||||
Log.w(TAG, "Dropping event with dtend=$rawEnd < dtstart=$begin")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
rawEnd
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kept raw (no untitled fallback): the detail screen substitutes its own
|
// Kept raw (no untitled fallback): the detail screen substitutes its own
|
||||||
|
|||||||
@@ -10,8 +10,11 @@ import de.jeanlucmakiola.calendula.domain.ics.parseRfc2445DurationMillis
|
|||||||
* of DTEND — reconstruct the end the same way the `.ics` export does.
|
* of DTEND — reconstruct the end the same way the `.ics` export does.
|
||||||
*/
|
*/
|
||||||
internal fun ColumnReader.toSearchResult(): EventInstance? {
|
internal fun ColumnReader.toSearchResult(): EventInstance? {
|
||||||
|
// A pre-1970 series anchor is a legitimately negative epoch-millis DTSTART
|
||||||
|
// (see EventDetailMapper / issue #34); drop only a genuinely absent one, so
|
||||||
|
// long-running birthdays/anniversaries still surface in search.
|
||||||
|
if (isNull(SearchProjection.IDX_DTSTART)) return null
|
||||||
val dtStart = getLong(SearchProjection.IDX_DTSTART)
|
val dtStart = getLong(SearchProjection.IDX_DTSTART)
|
||||||
if (dtStart < 0L) return null
|
|
||||||
val end = when {
|
val end = when {
|
||||||
!isNull(SearchProjection.IDX_DTEND) -> getLong(SearchProjection.IDX_DTEND)
|
!isNull(SearchProjection.IDX_DTEND) -> getLong(SearchProjection.IDX_DTEND)
|
||||||
else -> dtStart + parseRfc2445DurationMillis(getString(SearchProjection.IDX_DURATION))
|
else -> dtStart + parseRfc2445DurationMillis(getString(SearchProjection.IDX_DURATION))
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.domain
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.LocalTime
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import kotlinx.datetime.toLocalDateTime
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a prefilled [EventForm] from an `ACTION_INSERT` intent's extras (issue
|
||||||
|
* #30). External apps and widgets (e.g. the Todo Agenda widget) launch the
|
||||||
|
* calendar this way to create a new event, passing the fields as
|
||||||
|
* [android.provider.CalendarContract] extras. Any field the intent omits falls
|
||||||
|
* back to the same defaults the in-app "new event" uses — a timed start at the
|
||||||
|
* next full hour and a one-hour duration. [EventForm.calendarId] is left null so
|
||||||
|
* it resolves to the last-used / first-writable calendar, exactly like the
|
||||||
|
* `.ics` single-event and plain new-event paths.
|
||||||
|
*
|
||||||
|
* Pure (no Android types) so it is unit-testable; the intent parsing that reads
|
||||||
|
* the extras lives in `MainActivity.insertFormOrNull`.
|
||||||
|
*/
|
||||||
|
fun buildInsertEventForm(
|
||||||
|
beginMillis: Long?,
|
||||||
|
endMillis: Long?,
|
||||||
|
isAllDay: Boolean,
|
||||||
|
title: String?,
|
||||||
|
description: String?,
|
||||||
|
location: String?,
|
||||||
|
rrule: String?,
|
||||||
|
zone: TimeZone,
|
||||||
|
now: Instant,
|
||||||
|
): EventForm {
|
||||||
|
val (start, end) = if (isAllDay) {
|
||||||
|
// All-day provider times are UTC midnights with an exclusive end; show
|
||||||
|
// the last covered day and keep placeholder wall-clock times in case the
|
||||||
|
// user switches the event to timed (mirrors EventDetail.toEditForm).
|
||||||
|
val startDate = beginMillis
|
||||||
|
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date }
|
||||||
|
?: now.toLocalDateTime(zone).date
|
||||||
|
val endDate = endMillis
|
||||||
|
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(TimeZone.UTC).date }
|
||||||
|
?.let { exclusive -> maxOf(startDate, LocalDate.fromEpochDays(exclusive.toEpochDays() - 1)) }
|
||||||
|
?: startDate
|
||||||
|
LocalDateTime(startDate, LocalTime(9, 0)) to LocalDateTime(endDate, LocalTime(10, 0))
|
||||||
|
} else {
|
||||||
|
val startTime = beginMillis
|
||||||
|
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(zone) }
|
||||||
|
?: nextFullHour(now, zone)
|
||||||
|
val endTime = endMillis
|
||||||
|
?.let { Instant.fromEpochMilliseconds(it).toLocalDateTime(zone) }
|
||||||
|
?.takeIf { it >= startTime }
|
||||||
|
?: (startTime.toInstant(zone) + 1.hours).toLocalDateTime(zone)
|
||||||
|
startTime to endTime
|
||||||
|
}
|
||||||
|
return EventForm(
|
||||||
|
calendarId = null,
|
||||||
|
title = title.orEmpty(),
|
||||||
|
isAllDay = isAllDay,
|
||||||
|
start = start,
|
||||||
|
end = end,
|
||||||
|
location = location.orEmpty(),
|
||||||
|
description = description.orEmpty(),
|
||||||
|
// Bare RRULE value (Events.RRULE convention); tolerate a leading "RRULE:"
|
||||||
|
// some callers include.
|
||||||
|
rrule = rrule?.removePrefix("RRULE:")?.takeIf { it.isNotBlank() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextFullHour(now: Instant, zone: TimeZone): LocalDateTime {
|
||||||
|
val hourMillis = 3_600_000L
|
||||||
|
val rounded = (now.toEpochMilliseconds() / hourMillis + 1) * hourMillis
|
||||||
|
return Instant.fromEpochMilliseconds(rounded).toLocalDateTime(zone)
|
||||||
|
}
|
||||||
@@ -72,6 +72,8 @@ fun CalendarHost(
|
|||||||
onWidgetNavConsumed: () -> Unit = {},
|
onWidgetNavConsumed: () -> Unit = {},
|
||||||
requestedImportUri: android.net.Uri? = null,
|
requestedImportUri: android.net.Uri? = null,
|
||||||
onImportConsumed: () -> Unit = {},
|
onImportConsumed: () -> Unit = {},
|
||||||
|
requestedInsertForm: EventForm? = null,
|
||||||
|
onInsertConsumed: () -> Unit = {},
|
||||||
viewModel: CalendarHostViewModel = hiltViewModel(),
|
viewModel: CalendarHostViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
// Wait for the persisted default view before seeding the stack, so the app
|
// Wait for the persisted default view before seeding the stack, so the app
|
||||||
@@ -176,6 +178,16 @@ fun CalendarHost(
|
|||||||
onImportConsumed()
|
onImportConsumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// An external ACTION_INSERT launch (another app/widget creating an event,
|
||||||
|
// issue #30) arrives already prefilled — open it in the same create form the
|
||||||
|
// single-event .ics path uses. [importForm] is the topmost overlay, so it
|
||||||
|
// reveals on top of whatever was open without extra dismissal.
|
||||||
|
LaunchedEffect(requestedInsertForm) {
|
||||||
|
if (requestedInsertForm != null) {
|
||||||
|
importForm = requestedInsertForm
|
||||||
|
onInsertConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Close every overlay that can sit over the calendar, so an externally
|
// Close every overlay that can sit over the calendar, so an externally
|
||||||
// requested destination (a widget/shortcut/QS-tile launch) is revealed on
|
// requested destination (a widget/shortcut/QS-tile launch) is revealed on
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ fun RootScreen(
|
|||||||
onWidgetNavConsumed: () -> Unit = {},
|
onWidgetNavConsumed: () -> Unit = {},
|
||||||
requestedImportUri: android.net.Uri? = null,
|
requestedImportUri: android.net.Uri? = null,
|
||||||
onImportConsumed: () -> Unit = {},
|
onImportConsumed: () -> Unit = {},
|
||||||
|
requestedInsertForm: de.jeanlucmakiola.calendula.domain.EventForm? = null,
|
||||||
|
onInsertConsumed: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var hasPermission by remember {
|
var hasPermission by remember {
|
||||||
@@ -84,6 +86,8 @@ fun RootScreen(
|
|||||||
onWidgetNavConsumed = onWidgetNavConsumed,
|
onWidgetNavConsumed = onWidgetNavConsumed,
|
||||||
requestedImportUri = requestedImportUri,
|
requestedImportUri = requestedImportUri,
|
||||||
onImportConsumed = onImportConsumed,
|
onImportConsumed = onImportConsumed,
|
||||||
|
requestedInsertForm = requestedInsertForm,
|
||||||
|
onInsertConsumed = onInsertConsumed,
|
||||||
)
|
)
|
||||||
false -> ReminderOnboardingScreen(
|
false -> ReminderOnboardingScreen(
|
||||||
onFinished = reminderOnboarding::finish,
|
onFinished = reminderOnboarding::finish,
|
||||||
|
|||||||
@@ -4,14 +4,9 @@ import androidx.compose.material3.AlertDialog
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.text.format.DateFormat
|
|
||||||
import androidx.compose.material3.TimePicker
|
import androidx.compose.material3.TimePicker
|
||||||
import androidx.compose.material3.rememberTimePickerState
|
import androidx.compose.material3.rememberTimePickerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import de.jeanlucmakiola.calendula.R
|
import de.jeanlucmakiola.calendula.R
|
||||||
import kotlinx.datetime.LocalTime
|
import kotlinx.datetime.LocalTime
|
||||||
@@ -27,10 +22,13 @@ fun TimePickerAlert(
|
|||||||
onConfirm: (LocalTime) -> Unit,
|
onConfirm: (LocalTime) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
// Honour the app's own time-format preference (the value every time label
|
||||||
|
// reads), not the system/locale clock — otherwise an explicit 24h setting
|
||||||
|
// still showed an AM/PM dial (issue #27).
|
||||||
val state = rememberTimePickerState(
|
val state = rememberTimePickerState(
|
||||||
initialHour = initial.hour,
|
initialHour = initial.hour,
|
||||||
initialMinute = initial.minute,
|
initialMinute = initial.minute,
|
||||||
is24Hour = deviceUses24HourClock(LocalContext.current),
|
is24Hour = LocalUse24HourFormat.current,
|
||||||
)
|
)
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -45,24 +43,3 @@ fun TimePickerAlert(
|
|||||||
text = { TimePicker(state = state) },
|
text = { TimePicker(state = state) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the clock should read 24-hour, matching the rest of the device.
|
|
||||||
*
|
|
||||||
* [DateFormat.is24HourFormat] resolves a "locale default" system setting against
|
|
||||||
* the *app's* context locale — and this app applies a per-app language
|
|
||||||
* (AppCompatDelegate), so an English UI on a German-region phone would wrongly
|
|
||||||
* read 12-hour while the system clock shows 24-hour. So we honour an explicit
|
|
||||||
* system 12/24 override, and otherwise fall back to the **device** locale
|
|
||||||
* (Resources.getSystem), not the app's.
|
|
||||||
*/
|
|
||||||
private fun deviceUses24HourClock(context: Context): Boolean =
|
|
||||||
when (Settings.System.getString(context.contentResolver, Settings.System.TIME_12_24)) {
|
|
||||||
"24" -> true
|
|
||||||
"12" -> false
|
|
||||||
// 'a' is the AM/PM marker; a best-fit pattern without it is 24-hour.
|
|
||||||
else -> {
|
|
||||||
val deviceLocale = Resources.getSystem().configuration.locales[0]
|
|
||||||
!DateFormat.getBestDateTimePattern(deviceLocale, "jm").contains('a')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<string name="state_failure_provider">Kalender konnte nicht gelesen werden.</string>
|
<string name="state_failure_provider">Kalender konnte nicht gelesen werden.</string>
|
||||||
<!-- Permission-Flow (F1) -->
|
<!-- Permission-Flow (F1) -->
|
||||||
<string name="permission_rationale_title">Alle Termine, schön im Blick</string>
|
<string name="permission_rationale_title">Alle Termine, schön im Blick</string>
|
||||||
<string name="permission_rationale_body">Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App nie.</string>
|
<string name="permission_rationale_body">Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr braucht sie nicht um zu funktionieren — und nichts verlässt dein Gerät.</string>
|
||||||
<string name="permission_request_button">Kalender-Zugriff erlauben</string>
|
<string name="permission_request_button">Kalender-Zugriff erlauben</string>
|
||||||
<string name="permission_denied_title">Kalender-Zugriff abgelehnt</string>
|
<string name="permission_denied_title">Kalender-Zugriff abgelehnt</string>
|
||||||
<string name="permission_denied_body">Ohne Kalender-Zugriff kann Calendula keine Termine anzeigen. Du kannst den Zugriff in den System-Einstellungen wieder erlauben.</string>
|
<string name="permission_denied_body">Ohne Kalender-Zugriff kann Calendula keine Termine anzeigen. Du kannst den Zugriff in den System-Einstellungen wieder erlauben.</string>
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
<string name="settings_language">App-Sprache</string>
|
<string name="settings_language">App-Sprache</string>
|
||||||
<string name="settings_language_auto">Systemstandard</string>
|
<string name="settings_language_auto">Systemstandard</string>
|
||||||
<!-- Hub category subtitles -->
|
<!-- Hub category subtitles -->
|
||||||
<string name="settings_appearance_subtitle">Design, dynamische Farben, Wochenstart</string>
|
<string name="settings_appearance_subtitle">Design, Standartansicht, Wochenstart</string>
|
||||||
<string name="settings_event_form_subtitle">Standardfelder für neue Termine</string>
|
<string name="settings_event_form_subtitle">Standardfelder für neue Termine</string>
|
||||||
<string name="settings_notifications_subtitle">Termin-Erinnerungen</string>
|
<string name="settings_notifications_subtitle">Termin-Erinnerungen</string>
|
||||||
<string name="settings_section_about">Über</string>
|
<string name="settings_section_about">Über</string>
|
||||||
@@ -387,4 +387,27 @@
|
|||||||
<string name="crash_report_open_failed">Der Issue-Tracker konnte nicht geöffnet werden. Der Bericht ist in deiner Zwischenablage.</string>
|
<string name="crash_report_open_failed">Der Issue-Tracker konnte nicht geöffnet werden. Der Bericht ist in deiner Zwischenablage.</string>
|
||||||
<string name="crash_report_body_template">Danke, dass du einen Absturz in Calendula meldest. Bitte ergänze, was du gerade getan hast, und sende dann ab.\n\n### Was ist passiert\n\n\n### Absturzbericht\n%1$s\n</string>
|
<string name="crash_report_body_template">Danke, dass du einen Absturz in Calendula meldest. Bitte ergänze, was du gerade getan hast, und sende dann ab.\n\n### Was ist passiert\n\n\n### Absturzbericht\n%1$s\n</string>
|
||||||
<string name="crash_report_body_paste">_(Der Bericht war zu lang für diesen Link — füge ihn aus deiner Zwischenablage hier ein.)_</string>
|
<string name="crash_report_body_paste">_(Der Bericht war zu lang für diesen Link — füge ihn aus deiner Zwischenablage hier ein.)_</string>
|
||||||
|
<string name="event_edit_managed_hint">Verwaltet durch “%1$s” — der Titel, das Datum und Wiederholungen werden synchron mit deinen Kontakten gehalten. Erinnerungen, Ort und Beschreibung kannst du frei bearbeiten.</string>
|
||||||
|
<plurals name="duration_days">
|
||||||
|
<item quantity="one">%d Tag</item>
|
||||||
|
<item quantity="other">%d Tage</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="duration_weeks">
|
||||||
|
<item quantity="one">Woche</item>
|
||||||
|
<item quantity="other">Wochen</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="settings_default_view">Standartansicht</string>
|
||||||
|
<string name="settings_font_headings">Überschriften Schriftart</string>
|
||||||
|
<string name="settings_font_system">Systemstandard</string>
|
||||||
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
|
<string name="settings_font_choose_file">Datei auswählen…</string>
|
||||||
|
<string name="settings_font_import_failed">Die Datei konnte nicht als Schriftart gelesen werden</string>
|
||||||
|
<string name="settings_dim_completed">Vergangene Ereignisse abdunkeln</string>
|
||||||
|
<string name="settings_dim_completed_summary">Vergangene Termine in der Monats- und Wochenansicht abdunkeln</string>
|
||||||
|
<string name="settings_past_events">Vergangene Termine</string>
|
||||||
|
<string name="settings_past_events_show">Zeigen</string>
|
||||||
|
<string name="settings_past_events_dim">Abdunkeln</string>
|
||||||
|
<string name="settings_past_events_hide">Ausblenden</string>
|
||||||
|
<string name="settings_agenda_header">Agenda</string>
|
||||||
|
<string name="settings_special_dates_reminders">Erinnerungen</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<string name="state_failure_no_calendars_action">Abrir los ajustes del calendario del sistema</string>
|
<string name="state_failure_no_calendars_action">Abrir los ajustes del calendario del sistema</string>
|
||||||
<string name="state_failure_provider">No se pudo leer el calendario.</string>
|
<string name="state_failure_provider">No se pudo leer el calendario.</string>
|
||||||
<string name="permission_rationale_title">Mira todos sus eventos, a la perfección</string>
|
<string name="permission_rationale_title">Mira todos sus eventos, a la perfección</string>
|
||||||
<string name="permission_rationale_body">Calendula necesita acceso a tu calendario para mostrarte y administrar eventos. Ese es el único permiso que pedirá.</string>
|
<string name="permission_rationale_body">Calendula necesita acceso a tu calendario para mostrarte y administrar eventos. Ese es el único permiso que pedirá para comenzar a funcionar. Y nada de información sale de tu dispositivo.</string>
|
||||||
<string name="permission_request_button">Conceder el acceso al calendario</string>
|
<string name="permission_request_button">Conceder el acceso al calendario</string>
|
||||||
<string name="permission_denied_title">Acceso a calendario denegado</string>
|
<string name="permission_denied_title">Acceso a calendario denegado</string>
|
||||||
<string name="permission_denied_body">Calendula no puede mostrar eventos sin acceso al calendario. Puedes concederlo en ajustes del sistema.</string>
|
<string name="permission_denied_body">Calendula no puede mostrar eventos sin acceso al calendario. Puedes concederlo en ajustes del sistema.</string>
|
||||||
@@ -287,7 +287,7 @@
|
|||||||
<item quantity="many">%d dias</item>
|
<item quantity="many">%d dias</item>
|
||||||
<item quantity="other">%d dias</item>
|
<item quantity="other">%d dias</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="settings_section_event_form">Nueva formulario de evento</string>
|
<string name="settings_section_event_form">Formulario de nuevo evento</string>
|
||||||
<string name="settings_form_fields_hint">Campos mostrados por defecto — todo lo demás se encuentra bajo \"mas campos\"</string>
|
<string name="settings_form_fields_hint">Campos mostrados por defecto — todo lo demás se encuentra bajo \"mas campos\"</string>
|
||||||
<string name="settings_autofocus_title">Enfocar titulo en nuevos eventos</string>
|
<string name="settings_autofocus_title">Enfocar titulo en nuevos eventos</string>
|
||||||
<string name="settings_autofocus_title_hint">Cuando comienzas un nuevo evento, coloca el cursor en el titulo y abre el teclado inmediatamente.</string>
|
<string name="settings_autofocus_title_hint">Cuando comienzas un nuevo evento, coloca el cursor en el titulo y abre el teclado inmediatamente.</string>
|
||||||
@@ -418,4 +418,52 @@
|
|||||||
<string name="recurrence_with_count">%1$s, %2$d veces</string>
|
<string name="recurrence_with_count">%1$s, %2$d veces</string>
|
||||||
<string name="recurrence_with_until">%1$s hasta %2$s</string>
|
<string name="recurrence_with_until">%1$s hasta %2$s</string>
|
||||||
<string name="recurrence_on_days">%1$s los %2$s</string>
|
<string name="recurrence_on_days">%1$s los %2$s</string>
|
||||||
|
<string name="event_edit_managed_hint">Administrado por “%1$s” — el titulo, fecha y recurrencia se mantienen sincronizados con la inflación de tus contactos. Puedes editar los recordatorios, ubicacion y notas.</string>
|
||||||
|
<string name="settings_font_headings">Fuente de los títulos</string>
|
||||||
|
<string name="settings_font_body">Fuente del cuerpo del texto</string>
|
||||||
|
<string name="font_atkinson_hyperlegible">Atkinson Hyperlegible</string>
|
||||||
|
<string name="font_lora">Lora</string>
|
||||||
|
<string name="font_jetbrains_mono">JetBrains Mono</string>
|
||||||
|
<string name="settings_font_choose_file">Escoger desde archivo…</string>
|
||||||
|
<string name="settings_font_custom_selected">Fuente personalizada</string>
|
||||||
|
<string name="settings_font_import_failed">No se pude leer el archivo como fuente</string>
|
||||||
|
<string name="settings_section_views">Vistas</string>
|
||||||
|
<string name="settings_quick_switch_header">Botón de cambio rápido</string>
|
||||||
|
<string name="settings_quick_switch_hint">Escoge que vistas muestra el botón superior derecho y arrastralas para reordenalas. Las vistas deshabitadas permanecen accesibles desde el menú de navegación.</string>
|
||||||
|
<string name="settings_drawer_order_header">Menú de navegación</string>
|
||||||
|
<string name="settings_drawer_order_hint">Arrastra para cambiar el orden de las vistas que aparecen en el menú de navegación.</string>
|
||||||
|
<string name="reorder_drag_handle">Arrastra para reordenar</string>
|
||||||
|
<string name="settings_views_subtitle">Orden del botón de cambio rápido y menú</string>
|
||||||
|
<string name="settings_special_dates_subtitle">Aniversarios y cumpleaños de los contactos</string>
|
||||||
|
<string name="settings_section_special_dates">Fechas especiales de contactos</string>
|
||||||
|
<string name="settings_special_dates_enable">Mostrar fechas de contactos</string>
|
||||||
|
<string name="settings_special_dates_enable_hint">Refleja los cumpleaños y otras fechas de tus contactos en calendarios locales. Solo puede leer los contactos de este dispositivo — nada es compartido ni editado.</string>
|
||||||
|
<string name="settings_special_dates_type_birthday">Cumpleaños</string>
|
||||||
|
<string name="settings_special_dates_type_anniversary">Aniversarios</string>
|
||||||
|
<string name="settings_special_dates_type_custom">Otras fechas</string>
|
||||||
|
<string name="settings_special_dates_template">Formato de titulo</string>
|
||||||
|
<string name="settings_special_dates_template_hint">Usa {name} para el contacto y {year} para el año (el año de nacimiento u origen del aniversario; oculto cuando es desconocido).</string>
|
||||||
|
<string name="settings_special_dates_reminders">Recordatorios</string>
|
||||||
|
<string name="settings_special_dates_show_year">Mostrar año</string>
|
||||||
|
<string name="settings_special_dates_show_year_hint">Incluye {year} en títulos cuando se conoce el año</string>
|
||||||
|
<string name="settings_special_dates_sync_now">Sincronizar ahora</string>
|
||||||
|
<string name="settings_special_dates_never_synced">No se ha sincronizado todavía</string>
|
||||||
|
<string name="settings_special_dates_last_synced">Sincronizado por ultima vez %1$s</string>
|
||||||
|
<string name="settings_special_dates_calendar_hint">Cambia el color y visibilidad de cada calendario en ajustes de calendarios.</string>
|
||||||
|
<string name="settings_calendar_reminders_managed_hint">Establecido en Fechas especiales de contactos</string>
|
||||||
|
<string name="settings_special_dates_paused_hint">Calendula ya no puede leer tus contactos, por lo que estos calendarios ya no se estan actualizando.</string>
|
||||||
|
<string name="settings_special_dates_disable_title">¿Desactivar las fechas de contactos?</string>
|
||||||
|
<string name="settings_special_dates_disable_all_message">Esto eliminara los calendarios de fechas de contactos y sus eventos. Se perderán todos los recordatorios o notas agregadas.</string>
|
||||||
|
<string name="settings_special_dates_paused_title">Pausado</string>
|
||||||
|
<string name="settings_special_dates_disable_type_message">Esto eliminara el calendario “%1$s” y sus eventos. Se perderán todos los recordatorios o notas agregadas.</string>
|
||||||
|
<string name="settings_special_dates_disable_confirm">Desactivar</string>
|
||||||
|
<string name="special_dates_calendar_birthday">Cumpleaños</string>
|
||||||
|
<string name="special_dates_calendar_anniversary">Aniversarios</string>
|
||||||
|
<string name="special_dates_calendar_custom">Fechas especiales</string>
|
||||||
|
<string name="special_dates_default_title_birthday">Cumpleaños de {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_anniversary">Aniversario de {name} ({year})</string>
|
||||||
|
<string name="special_dates_default_title_custom">{name}</string>
|
||||||
|
<string name="settings_special_dates_grant">Permitir acceso</string>
|
||||||
|
<string name="dialog_save">Guardar</string>
|
||||||
|
<string name="settings_font_system">Igual que el sistema</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -418,4 +418,42 @@
|
|||||||
<string name="settings_default_reminder">Promemoria standard</string>
|
<string name="settings_default_reminder">Promemoria standard</string>
|
||||||
<string name="settings_notifications_subtitle">Promemoria evento</string>
|
<string name="settings_notifications_subtitle">Promemoria evento</string>
|
||||||
<string name="shortcut_new_event_short">Nuovo evento</string>
|
<string name="shortcut_new_event_short">Nuovo evento</string>
|
||||||
|
<string name="event_edit_managed_hint">Gestito da “%1$s” - titolo, data e ripetizioni sono sincronizzate dai tuoi contatti. Promemoria e note sono modificabili dall\'utente</string>
|
||||||
|
<string name="settings_font_headings">Carattere dell\'intestazione</string>
|
||||||
|
<string name="settings_font_body">Carattere del corpo</string>
|
||||||
|
<string name="settings_font_system">Predefinito di sistema</string>
|
||||||
|
<string name="settings_font_choose_file">Scegli file…</string>
|
||||||
|
<string name="settings_font_custom_selected">Carattere personalizzato</string>
|
||||||
|
<string name="settings_font_import_failed">Impossibile leggere il file come font</string>
|
||||||
|
<string name="settings_section_views">Viste</string>
|
||||||
|
<string name="settings_quick_switch_header">Pulsante di cambio rapido</string>
|
||||||
|
<string name="settings_quick_switch_hint">Scegli attraverso quali viste scorrere mediante il pulsante in alto a destra, trascinale per riordinarle. Le viste disattivate rimangono raggiungibili dal menù di navigazione.</string>
|
||||||
|
<string name="settings_drawer_order_header">Menù di navigazione</string>
|
||||||
|
<string name="settings_drawer_order_hint">Trascina per riordinare le viste elencate nel menù di navigazione.</string>
|
||||||
|
<string name="reorder_drag_handle">Trascina per riordinare</string>
|
||||||
|
<string name="settings_views_subtitle">Ordine del pulsante di cambio rapido e del menù</string>
|
||||||
|
<string name="settings_special_dates_subtitle">Compleanni e anniversari dei contatti</string>
|
||||||
|
<string name="settings_section_special_dates">Date speciali dei contatti</string>
|
||||||
|
<string name="settings_special_dates_enable">Mostra date dei contatti</string>
|
||||||
|
<string name="settings_special_dates_enable_hint">Riporta compleanni ed altre date dei tuoi contatti nei calendari locali. Legge i contatti solo su questo dispositivo - niente viene caricato in rete ed i tuoi contatti non vengono mai modificati.</string>
|
||||||
|
<string name="settings_special_dates_type_birthday">Compleanni</string>
|
||||||
|
<string name="settings_special_dates_type_anniversary">Anniversari</string>
|
||||||
|
<string name="settings_special_dates_type_custom">Altre date</string>
|
||||||
|
<string name="settings_special_dates_template">Formato del titolo</string>
|
||||||
|
<string name="settings_special_dates_template_hint">Usa {name} per il contatto e {year} per l\'anno (l\'anno di nascita, o l\'anno di inizio di un anniversario; nascosto se sconosciuto).</string>
|
||||||
|
<string name="settings_special_dates_reminders">Promemoria</string>
|
||||||
|
<string name="settings_special_dates_show_year">Mostra anno</string>
|
||||||
|
<string name="settings_special_dates_show_year_hint">Include {year} nei titoli quando è noto</string>
|
||||||
|
<string name="settings_special_dates_sync_now">Sincronizza adesso</string>
|
||||||
|
<string name="settings_special_dates_never_synced">Ancora non sincronizzato</string>
|
||||||
|
<string name="settings_special_dates_last_synced">%1$s è un tempo relativo, per esempio \"5 minuti fa\"</string>
|
||||||
|
<string name="settings_special_dates_calendar_hint">Imposta colore e visibilità di ogni calendario nelle impostazioni dei calendari.</string>
|
||||||
|
<string name="settings_special_dates_paused_title">In pausa</string>
|
||||||
|
<string name="settings_special_dates_paused_hint">Calendula non può più leggere i tuoi contatti, quindi questi calendari non vengono aggiornati.</string>
|
||||||
|
<string name="settings_special_dates_grant">Concedi accesso</string>
|
||||||
|
<string name="settings_special_dates_disable_title">Disattivare date dei contatti?</string>
|
||||||
|
<string name="settings_special_dates_disable_all_message">Questo cancella i calendari delle date dei contatti ed i loro eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
||||||
|
<string name="settings_special_dates_disable_type_message">Questo cancella il calendario “%1$s” ed i suoi eventi. Ogni promemoria o note che erano stati aggiunti saranno persi.</string>
|
||||||
|
<string name="settings_special_dates_disable_confirm">Spegni</string>
|
||||||
|
<string name="dialog_save">Salva</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
122
app/src/main/res/values-zh-rCN/strings.xml
Normal file
122
app/src/main/res/values-zh-rCN/strings.xml
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?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">一个现代化的日历应用</string>
|
||||||
|
<string name="state_loading">加载中…</string>
|
||||||
|
<string name="state_retry">推迟</string>
|
||||||
|
<string name="state_failure_unknown">错误!</string>
|
||||||
|
<string name="state_failure_permission">需要日历权限</string>
|
||||||
|
<string name="state_failure_permission_action">授予访问权限</string>
|
||||||
|
<string name="state_failure_no_calendars">无已设置的日历</string>
|
||||||
|
<string name="state_failure_no_calendars_action">打开系统日历设置</string>
|
||||||
|
<string name="state_failure_provider">无法读取日历</string>
|
||||||
|
<string name="permission_rationale_title">优雅地查看你的所有活动</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="permission_benefit_privacy_title">无追踪器</string>
|
||||||
|
<string name="permission_benefit_privacy_body">零遥测,零分析,没有广告。</string>
|
||||||
|
<string name="permission_privacy_footnote">数据仅保留在本地设备 ·无网络权限</string>
|
||||||
|
<string name="month_prev">上个月</string>
|
||||||
|
<string name="month_next">下个月</string>
|
||||||
|
<string name="month_today_action">今天</string>
|
||||||
|
<string name="month_more_actions">更多选项</string>
|
||||||
|
<string name="month_open_menu">打开菜单</string>
|
||||||
|
<string name="month_action_settings">设置</string>
|
||||||
|
<string name="month_a11y_today_prefix">今天</string>
|
||||||
|
<string name="week_today_action">本周</string>
|
||||||
|
<string name="day_today_action">今天</string>
|
||||||
|
<string name="event_detail_back">返回</string>
|
||||||
|
<string name="event_detail_edit">编辑</string>
|
||||||
|
<string name="event_detail_delete">删除</string>
|
||||||
|
<string name="event_detail_share">分享</string>
|
||||||
|
<string name="event_share_chooser_title">分享活动</string>
|
||||||
|
<string name="event_share_failed">无法分享该活动。</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_delete_option_series">系列中的所有活动</string>
|
||||||
|
<string name="event_edit_recurring_title">编辑重复活动</string>
|
||||||
|
<string name="event_delete_failed">无法删除该活动</string>
|
||||||
|
<string name="event_delete_write_denied">Calendula需要权限读取或删除活动</string>
|
||||||
|
<string name="dialog_cancel">取消</string>
|
||||||
|
<string name="dialog_ok">确认</string>
|
||||||
|
<string name="event_edit_new_title">新活动</string>
|
||||||
|
<string name="event_edit_close">关闭</string>
|
||||||
|
<string name="event_edit_save">保存</string>
|
||||||
|
<string name="event_edit_title_hint">增加标题</string>
|
||||||
|
<string name="event_edit_managed_hint">由“%1$s”管理——标题、日期和重复内容会与你的联系人保持同步。提醒、位置和笔记由你自行编辑。</string>
|
||||||
|
<string name="event_edit_starts">开始</string>
|
||||||
|
<string name="event_edit_ends">结束</string>
|
||||||
|
<string name="event_edit_error_end_before_start">报错:结束时间早于起始时间</string>
|
||||||
|
<string name="event_edit_error_no_calendar">无可写入的日历</string>
|
||||||
|
<string name="event_edit_save_failed">无法保存该活动</string>
|
||||||
|
<string name="event_edit_write_denied">Calendula需要写入权限来创建活动</string>
|
||||||
|
<string name="event_edit_more_fields">其他领域</string>
|
||||||
|
<string name="event_edit_add">新增</string>
|
||||||
|
<string name="event_edit_add_reminder">新建提醒</string>
|
||||||
|
<string name="event_edit_remove_reminder">移除提醒</string>
|
||||||
|
<string name="event_edit_attendees">访问者</string>
|
||||||
|
<string name="event_edit_add_guest">新增访问者</string>
|
||||||
|
<string name="event_edit_add_guest_hint">通过 Email 新增一个访问者…</string>
|
||||||
|
<string name="event_edit_add_guest_from_contacts">从联系人中添加访问者</string>
|
||||||
|
<string name="event_edit_location_from_contacts">从联系人中添加地址</string>
|
||||||
|
<string name="event_edit_remove_guest">移除访问者</string>
|
||||||
|
<string name="event_edit_attendee_required">必填</string>
|
||||||
|
<string name="event_edit_attendee_optional">可选项</string>
|
||||||
|
<string name="event_edit_attendees_note_synced">Calendula 不发送邀请函。你的日历账户同步时可能会给访客发邮件。</string>
|
||||||
|
<string name="event_edit_attendees_note_local">保存在本设备,其他访客不会被提醒。</string>
|
||||||
|
<string name="event_edit_reminder_custom">自定义</string>
|
||||||
|
<string name="reminder_unit_minutes">分钟</string>
|
||||||
|
<string name="reminder_unit_hours">小时</string>
|
||||||
|
<string name="reminder_unit_days">天</string>
|
||||||
|
<string name="reminder_unit_weeks">周</string>
|
||||||
|
<string name="event_edit_availability">空闲</string>
|
||||||
|
<string name="event_edit_visibility">隐私设置</string>
|
||||||
|
<string name="event_edit_color">色彩</string>
|
||||||
|
<string name="event_edit_color_default">日历颜色</string>
|
||||||
|
<string name="event_edit_color_custom">自定义颜色</string>
|
||||||
|
<string name="event_edit_color_reset">重置</string>
|
||||||
|
<string name="event_edit_color_unsupported">该日历不可用</string>
|
||||||
|
<string name="event_edit_color_unsupported_hint">此日历未设置任何颜色集。您可以在“设置”中为此类日历启用自定义颜色。</string>
|
||||||
|
<string name="event_edit_color_sync_warning">该日历在下一次同步时可能会删除或覆盖该颜色。</string>
|
||||||
|
<string name="event_edit_conflict_title">其他设备的活动已更改</string>
|
||||||
|
<string name="event_edit_conflict_body">您在编辑时,该事件已被同步或其他应用修改。您的修改要怎么处理?</string>
|
||||||
|
<string name="event_edit_conflict_overwrite">保存我的更改</string>
|
||||||
|
<string name="event_edit_conflict_overwrite_hint">仅您编辑的字段会覆盖外部更改</string>
|
||||||
|
<string name="event_edit_conflict_discard">丢弃我的更改</string>
|
||||||
|
<string name="event_edit_conflict_discard_hint">该活动保持现状</string>
|
||||||
|
<string name="event_edit_gone_title">活动被删除</string>
|
||||||
|
<string name="event_edit_gone_body">这个事件在您编辑期间已被删除了(比如在另一台设备上)。您的修改现在无法保存。</string>
|
||||||
|
<string name="event_edit_recurrence_none">确认无需重复</string>
|
||||||
|
<string name="event_edit_recurrence_custom">自定义</string>
|
||||||
|
<string name="event_edit_recurrence_every">每</string>
|
||||||
|
<string name="recurrence_unit_days">天</string>
|
||||||
|
<string name="recurrence_unit_weeks">周</string>
|
||||||
|
<string name="recurrence_unit_months">月</string>
|
||||||
|
<string name="recurrence_unit_years">年</string>
|
||||||
|
<string name="event_edit_recurrence_ends">结束时间</string>
|
||||||
|
<string name="event_edit_recurrence_end_never">从不</string>
|
||||||
|
<string name="event_edit_recurrence_end_until">于指定日期</string>
|
||||||
|
<string name="event_edit_recurrence_end_count">重复几次后</string>
|
||||||
|
<string name="event_edit_recurrence_times">次</string>
|
||||||
|
<string name="event_edit_error_recurrence_ends_before_start">报错:结束日期早于开始日期</string>
|
||||||
|
<string name="event_availability_busy">占用中</string>
|
||||||
|
<string name="event_access_default">默认</string>
|
||||||
|
<string name="event_access_public">公开</string>
|
||||||
|
</resources>
|
||||||
@@ -22,8 +22,8 @@ class EventDetailMapperTest {
|
|||||||
eventColor: Any? = null,
|
eventColor: Any? = null,
|
||||||
eventColorKey: String? = null,
|
eventColorKey: String? = null,
|
||||||
calendarColor: Int = 0xFFAABBCC.toInt(),
|
calendarColor: Int = 0xFFAABBCC.toInt(),
|
||||||
dtstart: Long = 1_000_000_000L,
|
dtstart: Long? = 1_000_000_000L,
|
||||||
dtend: Long = 1_000_003_600L,
|
dtend: Long? = 1_000_003_600L,
|
||||||
allDay: Int = 0,
|
allDay: Int = 0,
|
||||||
location: String? = "Berlin",
|
location: String? = "Berlin",
|
||||||
calendarId: Long = 7L,
|
calendarId: Long = 7L,
|
||||||
@@ -120,9 +120,31 @@ class EventDetailMapperTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `dtend before dtstart drops detail`() {
|
fun `dtend before dtstart is clamped to a zero-length event, not dropped`() {
|
||||||
|
// A backwards DTEND is malformed, but dropping it would make the event
|
||||||
|
// un-openable (the "Something went wrong" trap, same as issue #34); keep
|
||||||
|
// it as a zero-length event so the user can still open and fix it.
|
||||||
val detail = detailReader(dtstart = 2000L, dtend = 1000L).toDetail()
|
val detail = detailReader(dtstart = 2000L, dtend = 1000L).toDetail()
|
||||||
assertThat(detail).isNull()
|
assertThat(detail).isNotNull()
|
||||||
|
assertThat(detail!!.instance.start.toEpochMilliseconds()).isEqualTo(2000L)
|
||||||
|
assertThat(detail.instance.end.toEpochMilliseconds()).isEqualTo(2000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pre-1970 negative dtstart is kept, not dropped (issue #34)`() {
|
||||||
|
// A yearly birthday/anniversary anchored before the epoch has a
|
||||||
|
// legitimately negative UTC epoch-millis DTSTART; recurring rows carry
|
||||||
|
// no DTEND (they use DURATION), so it stays end == begin.
|
||||||
|
val begin = -157_766_400_000L // 1965-01-01T00:00:00Z
|
||||||
|
val detail = detailReader(dtstart = begin, dtend = null).toDetail()
|
||||||
|
assertThat(detail).isNotNull()
|
||||||
|
assertThat(detail!!.instance.start.toEpochMilliseconds()).isEqualTo(begin)
|
||||||
|
assertThat(detail.instance.end.toEpochMilliseconds()).isEqualTo(begin)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `absent dtstart drops detail`() {
|
||||||
|
assertThat(detailReader(dtstart = null).toDetail()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.data.calendar
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class SearchMapperTest {
|
||||||
|
|
||||||
|
private fun searchReader(
|
||||||
|
id: Long = 1L,
|
||||||
|
calendarId: Long = 7L,
|
||||||
|
title: String? = "Birthday",
|
||||||
|
dtstart: Long? = 1_000_000_000L,
|
||||||
|
dtend: Long? = null,
|
||||||
|
duration: String? = "P1D",
|
||||||
|
allDay: Int = 1,
|
||||||
|
eventColor: Any? = null,
|
||||||
|
calendarColor: Int = 0xFFAABBCC.toInt(),
|
||||||
|
location: String? = null,
|
||||||
|
): MapColumnReader = MapColumnReader(
|
||||||
|
SearchProjection.IDX_ID to id,
|
||||||
|
SearchProjection.IDX_CALENDAR_ID to calendarId,
|
||||||
|
SearchProjection.IDX_TITLE to title,
|
||||||
|
SearchProjection.IDX_DTSTART to dtstart,
|
||||||
|
SearchProjection.IDX_DTEND to dtend,
|
||||||
|
SearchProjection.IDX_DURATION to duration,
|
||||||
|
SearchProjection.IDX_ALL_DAY to allDay,
|
||||||
|
SearchProjection.IDX_EVENT_COLOR to eventColor,
|
||||||
|
SearchProjection.IDX_CALENDAR_COLOR to calendarColor,
|
||||||
|
SearchProjection.IDX_LOCATION to location,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `pre-1970 negative dtstart still surfaces in search (issue #34)`() {
|
||||||
|
val begin = -157_766_400_000L // 1965-01-01T00:00:00Z
|
||||||
|
val result = searchReader(dtstart = begin, dtend = null, duration = "P1D").toSearchResult()
|
||||||
|
assertThat(result).isNotNull()
|
||||||
|
assertThat(result!!.start.toEpochMilliseconds()).isEqualTo(begin)
|
||||||
|
assertThat(result.title).isEqualTo("Birthday")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `absent dtstart drops the search hit`() {
|
||||||
|
assertThat(searchReader(dtstart = null).toSearchResult()).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package de.jeanlucmakiola.calendula.domain
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.datetime.LocalDate
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.datetime.LocalTime
|
||||||
|
import kotlinx.datetime.TimeZone
|
||||||
|
import kotlinx.datetime.atStartOfDayIn
|
||||||
|
import kotlinx.datetime.toInstant
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.time.Instant
|
||||||
|
|
||||||
|
class InsertEventFormTest {
|
||||||
|
|
||||||
|
private val zone = TimeZone.UTC
|
||||||
|
|
||||||
|
// A fixed "now" at 2026-01-15T10:20:00Z; the next full hour is 11:00.
|
||||||
|
private val now = LocalDateTime(2026, 1, 15, 10, 20).toInstant(zone)
|
||||||
|
|
||||||
|
private fun build(
|
||||||
|
beginMillis: Long? = null,
|
||||||
|
endMillis: Long? = null,
|
||||||
|
isAllDay: Boolean = false,
|
||||||
|
title: String? = null,
|
||||||
|
description: String? = null,
|
||||||
|
location: String? = null,
|
||||||
|
rrule: String? = null,
|
||||||
|
): EventForm = buildInsertEventForm(
|
||||||
|
beginMillis = beginMillis,
|
||||||
|
endMillis = endMillis,
|
||||||
|
isAllDay = isAllDay,
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
location = location,
|
||||||
|
rrule = rrule,
|
||||||
|
zone = zone,
|
||||||
|
now = now,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun millis(dateTime: LocalDateTime): Long = dateTime.toInstant(zone).toEpochMilliseconds()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `timed event maps all provided fields`() {
|
||||||
|
val start = LocalDateTime(2026, 3, 1, 14, 30)
|
||||||
|
val end = LocalDateTime(2026, 3, 1, 15, 45)
|
||||||
|
val form = build(
|
||||||
|
beginMillis = millis(start),
|
||||||
|
endMillis = millis(end),
|
||||||
|
title = "Standup",
|
||||||
|
description = "Daily",
|
||||||
|
location = "Room 1",
|
||||||
|
rrule = "FREQ=DAILY",
|
||||||
|
)
|
||||||
|
assertThat(form.calendarId).isNull() // resolves to last-used / first-writable
|
||||||
|
assertThat(form.isAllDay).isFalse()
|
||||||
|
assertThat(form.start).isEqualTo(start)
|
||||||
|
assertThat(form.end).isEqualTo(end)
|
||||||
|
assertThat(form.title).isEqualTo("Standup")
|
||||||
|
assertThat(form.description).isEqualTo("Daily")
|
||||||
|
assertThat(form.location).isEqualTo("Room 1")
|
||||||
|
assertThat(form.rrule).isEqualTo("FREQ=DAILY")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `missing times default to the next full hour and a one-hour duration`() {
|
||||||
|
val form = build(title = "Quick note")
|
||||||
|
assertThat(form.start).isEqualTo(LocalDateTime(2026, 1, 15, 11, 0))
|
||||||
|
assertThat(form.end).isEqualTo(LocalDateTime(2026, 1, 15, 12, 0))
|
||||||
|
assertThat(form.title).isEqualTo("Quick note")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `timed event with only a start gets a one-hour end`() {
|
||||||
|
val start = LocalDateTime(2026, 5, 2, 9, 0)
|
||||||
|
val form = build(beginMillis = millis(start))
|
||||||
|
assertThat(form.start).isEqualTo(start)
|
||||||
|
assertThat(form.end).isEqualTo(LocalDateTime(2026, 5, 2, 10, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `an end before the start is ignored and falls back to plus one hour`() {
|
||||||
|
val start = LocalDateTime(2026, 5, 2, 9, 0)
|
||||||
|
val badEnd = LocalDateTime(2026, 5, 2, 8, 0)
|
||||||
|
val form = build(beginMillis = millis(start), endMillis = millis(badEnd))
|
||||||
|
assertThat(form.end).isEqualTo(LocalDateTime(2026, 5, 2, 10, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all-day event uses UTC dates and decrements the exclusive end`() {
|
||||||
|
val startMillis = LocalDate(2026, 1, 1).atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
||||||
|
val endExclusive = LocalDate(2026, 1, 3).atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
||||||
|
val form = build(beginMillis = startMillis, endMillis = endExclusive, isAllDay = true, title = "Trip")
|
||||||
|
assertThat(form.isAllDay).isTrue()
|
||||||
|
assertThat(form.start.date).isEqualTo(LocalDate(2026, 1, 1))
|
||||||
|
assertThat(form.end.date).isEqualTo(LocalDate(2026, 1, 2))
|
||||||
|
// Placeholder wall-clock times survive a switch back to a timed event.
|
||||||
|
assertThat(form.start.time).isEqualTo(LocalTime(9, 0))
|
||||||
|
assertThat(form.end.time).isEqualTo(LocalTime(10, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all-day event without an end is a single day`() {
|
||||||
|
val startMillis = LocalDate(2026, 6, 10).atStartOfDayIn(TimeZone.UTC).toEpochMilliseconds()
|
||||||
|
val form = build(beginMillis = startMillis, isAllDay = true)
|
||||||
|
assertThat(form.start.date).isEqualTo(LocalDate(2026, 6, 10))
|
||||||
|
assertThat(form.end.date).isEqualTo(LocalDate(2026, 6, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `a leading RRULE prefix is stripped and a blank rule becomes null`() {
|
||||||
|
assertThat(build(rrule = "RRULE:FREQ=WEEKLY").rrule).isEqualTo("FREQ=WEEKLY")
|
||||||
|
assertThat(build(rrule = " ").rrule).isNull()
|
||||||
|
assertThat(build(rrule = null).rrule).isNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
16
fastlane/metadata/android/en-US/changelogs/21301.txt
Normal file
16
fastlane/metadata/android/en-US/changelogs/21301.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
### Added
|
||||||
|
- Create events from other apps and widgets. Calendula now registers the
|
||||||
|
standard "insert event" intent, so other apps and home-screen widgets (such as
|
||||||
|
the Todo Agenda widget) can hand off to Calendula to create a new event — it
|
||||||
|
opens the new-event form prefilled with whatever they passed (title, time,
|
||||||
|
location, description). Thanks to @dschuermann for the suggestion ([#30]).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Some recurring events could not be opened. Events in a series that started
|
||||||
|
before 1970 — for example yearly birthdays or anniversaries synced over CalDAV
|
||||||
|
— showed "Something went wrong" instead of opening. They now open normally, and
|
||||||
|
show up in search again. Thanks to @dschuermann for the report ([#34]).
|
||||||
|
- The time picker now follows your 24-hour setting. With Calendula set to
|
||||||
|
24-hour time, the dial for choosing an event's start and end time still showed
|
||||||
|
AM/PM; it now matches your setting. Thanks to @abrossimow for the report
|
||||||
|
([#27]).
|
||||||
Reference in New Issue
Block a user