Compare commits

...

17 Commits

Author SHA1 Message Date
cf3b897305 release: v2.13.1 — recurring-event open fix, 24h time picker, INSERT intent
All checks were successful
CI / ci (pull_request) Successful in 8m32s
Patch release bundling the fixes for #34 (pre-1970 recurring events could
not be opened) and #27 (time-picker dial ignored the 24h setting), plus
#30 (create events from external ACTION_INSERT launches). Bumps
versionName to 2.13.1 (versionCode 21301) and cuts the changelog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:25:17 +02:00
87a78ca924 Merge branches 'fix/recurring-event-open-and-24h-picker' and 'feat/insert-intent' into release/v2.13.1 2026-07-06 17:23:47 +02:00
13debac340 Merge pull request 'Translations update from Weblate' (!59) from weblate-bot/calendula:weblate-calendula-strings into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea release / release (push) Has been skipped
Reviewed-on: #59
2026-07-06 15:21:52 +00:00
76846420a2 fix(detail): clamp a backwards DTEND instead of dropping the event (#34)
toEventDetailCore returned null when a present DTEND preceded DTSTART, the
only remaining false-drop that surfaces as the generic "Something went
wrong." error screen — the same un-openable trap as the pre-1970 DTSTART
bug, and worse because the user can't even open the malformed event to fix
it. Clamp the end to DTSTART (a zero-length event) instead, matching how
SearchMapper already coerces its end. After this the detail mapper drops a
row only when DTSTART is genuinely absent (unrenderable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:11:51 +02:00
1a3e4f501f fix(edit): time picker dial honours the app 24h/12h setting (#27)
The event-form (and Settings) time picker seeded is24Hour from the
system TIME_12_24 override / device locale, ignoring the app's own
TimeFormatPref. So with the app set to 24h under an English locale the
dial still showed AM/PM, while every time label (which reads
LocalUse24HourFormat) showed 24h.

Seed the picker from LocalUse24HourFormat — the app-wide clock
convention already resolved once at the root from TimeFormatPref — so the
dial matches the labels. Drops the now-unused deviceUses24HourClock
helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:05:43 +02:00
974d65f619 fix(detail): open events whose series starts before 1970 (#34)
DTSTART is stored as UTC epoch millis, so a recurring series anchored
before 1970-01-01 (common for yearly birthdays/anniversaries synced over
CalDAV) has a legitimately negative DTSTART. The detail and search
mappers dropped any row with dtstart < 0, and since the detail query
reads the series-master DTSTART (the ancient anchor), every occurrence of
such a series became un-openable — surfacing as the generic
"Something went wrong." error screen — and the events vanished from
search too.

Relax the guard to reject only an *absent* DTSTART (isNull), which is the
malformed case it was meant to catch; negative epoch millis flow through
correctly (Instant/formatting and the all-day reminder decode are all
Long-based). Add regression tests for both mappers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:05:33 +02:00
Ulisse Perusin
721ca0d7e0 Translated using Weblate (Italian)
All checks were successful
Translations / check (pull_request) Successful in 5s
CI / ci (pull_request) Successful in 9m40s
Currently translated at 97.3% (399 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-05 08:52:25 +00:00
Ulisse Perusin
2556eba84b Translated using Weblate (Italian)
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 4m2s
Currently translated at 94.1% (386 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/it/
2026-07-05 07:49:35 +00:00
zh-cn
fc1ff97d22 Translated using Weblate (Chinese (Simplified Han script))
All checks were successful
Translations / check (pull_request) Successful in 27s
CI / ci (pull_request) Successful in 4m3s
Currently translated at 29.0% (119 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/zh_Hans/
2026-07-05 04:20:20 +00:00
Weblate
b7f3d421ef Update translation files
All checks were successful
Translations / check (pull_request) Successful in 28s
CI / ci (pull_request) Successful in 10m7s
Updated by "Remove blank strings" add-on in Weblate.

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/
2026-07-04 04:10:19 +00:00
zh-cn
2722b3427a Added translation using Weblate (Chinese (Simplified Han script)) 2026-07-04 04:10:19 +00:00
Weblate
e28c293d27 Translated using Weblate (Spanish)
Currently translated at 100.0% (410 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/es/
2026-07-04 04:10:19 +00:00
iker Contreras
5cb2a836ab Translated using Weblate (Spanish)
Currently translated at 100.0% (410 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/es/
2026-07-04 04:10:18 +00:00
Weblate
b5978e7a61 Translated using Weblate (German)
Currently translated at 84.3% (346 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/de/
2026-07-04 04:10:18 +00:00
c4a048adee Translated using Weblate (German)
Currently translated at 84.3% (346 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/de/
2026-07-04 04:10:18 +00:00
Weblate
2746ca5784 Translated using Weblate (German)
Currently translated at 84.3% (346 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/de/
2026-07-04 04:10:18 +00:00
fc9bd33992 Translated using Weblate (German)
Currently translated at 84.3% (346 of 410 strings)

Translation: Calendula/Strings
Translate-URL: https://weblate.dev.jeanlucmakiola.de/projects/calendula/strings/de/
2026-07-04 04:10:18 +00:00
12 changed files with 374 additions and 49 deletions

View File

@@ -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

View File

@@ -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"
} }

View File

@@ -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

View File

@@ -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))

View File

@@ -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')
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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

View File

@@ -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()
}
}

View 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]).