Commit Graph

383 Commits

Author SHA1 Message Date
f0cc35f2ce test: cover the export calendar-id plumbing
The fake now records the calendarIds it receives; two repository tests
assert exportEvents forwards a chosen subset and defaults to null (all
eligible calendars), closing the coverage gap for the per-calendar
export selector.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:34:35 +02:00
2ce79942c4 fix: broaden restore availability and stabilize the export picker
- Restore is import, not export: offer it whenever any writable, non-
  managed calendar exists (local or synced), not only when there is a
  local calendar to back up. Previously the row lived inside the
  export-gated block and vanished for users with only a writable synced
  calendar, despite import supporting that target.
- Export-picker selection now uses rememberSaveable and is no longer
  keyed on the observer-driven calendars list, so a background provider
  re-emit (sync/recolor) can't silently reset the user's de-selections,
  and the choice survives rotation.
- Shared calendar picker: restore the displayName fallback for a synced
  calendar whose account name and type are both blank (was grouping them
  under an empty header).
- Drop imports left dead by the CalendarPickerGroups extraction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:34:35 +02:00
df426bb8df fix: scope import ViewModel per-uri so a second import re-parses
ImportScreen has no nav backstack, so an unkeyed hiltViewModel() resolved
to the Activity's ViewModelStore and was retained across imports. Its
one-shot `load` guard then showed the *previous* file's parsed state on
the next import — trivially reachable now that the in-app Restore button
lets you export→restore or restore twice in one session (worst case: the
picker still holds file A, so tapping Import writes A's events after you
picked B). Keying the VM by the file uri hands each distinct file a fresh
VM (fresh Loading state); the same uri (rotation) reuses it and holds the
result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:34:15 +02:00
e967007bdc fix: default import target to the first local calendar
The pre-selected target was calendars.first() (raw provider order), which
could land on a synced calendar mid-list while the picker shows local
calendars first. Default to the first local calendar so the checkmark lines
up with the top row; fall back to the first calendar when none are local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:41:51 +02:00
9f7427e72f feat: pin import action to the top bar, fold count into title
Move the multi-event import's confirm button into the app-bar actions so
it's reachable without scrolling past a long calendar list, and put the
count in the title ('Importing 5 events') instead of a separate 'N events
in this file' line. Hoists the selected target calendar to the screen so
the top-bar action can read it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:39:54 +02:00
79d9e0eaa0 feat: in-app restore always uses the full import flow
Route by entry point, not just event count. Opening a .ics from outside
still sends a single event straight to the prefilled create form (add one
event, e.g. a ticket). The in-app 'Restore from .ics' button passes
forceMany so even a single-event backup goes through the calendar picker +
summary — its intent is 'restore a backup', not 'add this event'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:29:04 +02:00
a98dd654a6 polish: explain skipped duplicates on the import-complete screen
The import de-dups by UID against the target calendar (idempotent restore),
so re-importing events already present shows a low 'Added' count. Add a note
under the title when any were skipped so the outcome doesn't read as broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:07:56 +02:00
a138e179dd feat: per-calendar export selector
Tapping Export with more than one exportable calendar now opens a picker
to choose which local calendars to include (all selected by default); a
single calendar exports straight through as before. Threads an optional
calendarIds filter through exportEvents/exportableEvents (null = all
eligible), so the auto-backup path is unaffected. The backup section is
now gated on there being at least one exportable (non-managed) calendar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:46:24 +02:00
94e3887345 fix: exclude managed special-dates calendars from import targets
Symmetric with the export change: the contact-derived, editor-locked
special-dates mirror calendars aren't a valid import destination, so drop
them from the target-calendar picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:39:54 +02:00
f440d385fa fix: exclude managed special-dates calendars from .ics export
The contact special-dates mirror calendars (birthdays/anniversaries) are
derived from contacts and re-materialise from the contact sync, so backing
them up only duplicates events on restore. Skip managed calendars in
exportableEvents — covers both the manual export and the auto-backup, which
share this path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:38:21 +02:00
8fb4767888 feat: spice up the import-complete screen
Replace the plain centered text list with an M3 Expressive success state:
a tonal check badge that springs in, the headline, and big-number tonal
stat tiles for added / duplicate-skipped counts, with a full-width Done
button. Stat tiles carry the full-sentence plurals as accessibility
labels so TalkBack still reads 'Imported N events'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:32:20 +02:00
993d74502f chore: stop tracking local floret-kit scratch dir (added by mistake)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:25:20 +02:00
4a11c951ae refactor: one shared calendar picker with settings-page category headers
Extract CalendarPickerGroups into ui/common: the calendar-manager screen's
grouped-card system (device chip for local calendars, the owning app's
launcher icon per synced account, colour chip + check per calendar) as a
single reusable picker. Use it in both the event editor and the .ics import
screen so all 'which calendar' lists match.

Moves LeadingAvatar/SourceLogo/curatedSourcePackage out of CalendarsScreen
into common as the shared source of truth. Drops the redundant 'Add to
calendar' caption from the import picker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:23:39 +02:00
f25f308326 fix: import target picker uses the standard grouped-list format
The 'Add to calendar' picker rendered bare OptionCards with no calendar
colour and no account grouping. Reuse the same account-grouped GroupedRow
layout as the event editor's calendar picker — coloured chip per calendar,
account sub-headers, a check on the selected row — so it matches the rest
of the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 18:11:45 +02:00
bff683a403 feat: restore events from .ics file in backup section (#32)
Add a 'Restore from .ics file' row to the Calendars backup section, next
to Export. It opens a SAF document picker and routes the picked Uri into
the existing import flow (parse, dedup by UID, target-calendar picker,
summary) via CalendarHost's importUri — the same path an externally
opened .ics already takes, so no new import machinery is needed.

Closes #32.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:55:35 +02:00
bb7954d026 Remove superpowers planning docs; ignore CLAUDE.md
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 6s
Release — F-Droid repo + Gitea release / release (push) Successful in 7m3s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v2.13.1
2026-07-06 17:43:22 +02:00
d9f4239729 Merge pull request 'release: v2.13.1 — recurring-event open fix, 24h time picker, INSERT intent' (!60) from release/v2.13.1 into main
Some checks failed
Release — F-Droid repo + Gitea release / detect (push) Successful in 4s
Release — F-Droid repo + Gitea release / release (push) Has been cancelled
Reviewed-on: #60
2026-07-06 15:36:42 +00:00
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
65bd6c4254 feat(intent): create events from external ACTION_INSERT launches (#30)
Register an intent-filter for ACTION_INSERT on the events dir mime type
(vnd.android.cursor.dir/event), the way the AOSP calendar accepts one, so
other apps and widgets (e.g. the Todo Agenda widget) can launch Calendula
to create a new event.

MainActivity.insertFormOrNull parses the standard CalendarContract extras
(EXTRA_EVENT_BEGIN_TIME/END_TIME/ALL_DAY, Events.TITLE/DESCRIPTION/
EVENT_LOCATION/RRULE) into a prefilled EventForm via the pure, unit-tested
buildInsertEventForm — omitted fields fall back to the same defaults the
in-app "new event" uses (next full hour, +1h). The form is routed through
the existing single-event prefill channel (RootScreen → CalendarHost →
the create form for review), with calendarId left null so it resolves to
the last-used / first-writable calendar. No new permission is needed
(WRITE_CALENDAR is already held), and the user still explicitly saves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 16:12:56 +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
2239c38ecc Merge pull request 'release: v2.13.0 — special dates, custom fonts, quick-switch, es/it translations' (!58) from release/v2.13.0 into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 5s
Release — F-Droid repo + Gitea release / release (push) Successful in 11m16s
Renovate / renovate (push) Successful in 1m12s
Reviewed-on: #58
v2.13.0
2026-07-03 14:47:42 +00:00
79ad70e0c0 release: cut v2.13.0 changelog and fastlane notes
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 9m3s
Move the accumulated [Unreleased] section under [2.13.0] — 2026-07-03,
adding the entries that were still missing: Spanish + Italian community
translations (thanks kikerw and corrent via Weblate), custom fonts (#19),
configurable quick-switch cycle + drawer order (#24), and the scrollable
event-editor calendar picker (#29). Regenerate fastlane/.../changelogs/
21300.txt via scripts/sync_changelog_to_fastlane.sh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:25:39 +02:00
f168021f51 release: bump version to 2.13.0 (21300)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:25:39 +02:00
a2b1e7792b chore(i18n): drop stale reminder_custom_with_value from es/it
The source key was removed on this branch; the Weblate translations
merged from main still carried it, tripping check_translations.py and
fatal ExtraTranslation lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:10:31 +02:00
c6ce0830b1 Merge remote-tracking branch 'origin/main' into release/v2.13.0 2026-07-03 16:09:51 +02:00
be13f97802 feat(i18n): surface Spanish and Italian in the language pickers
Community translations for es and it reached ~85% coverage (Weblate
PR #52 into main). Adding their locale entries makes them selectable in
the in-app picker and Android 13+ per-app language settings once that
PR is merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:05:22 +02:00
f6d40de832 chore(i18n): strip hand-edited German strings — German returns via Weblate
Weblate owns all translation files including values-de; devs only touch
the English base. Reverts the hand-added special-dates strings and the
permission_rationale_body rewording to main's version (texts recoverable
from 1ff4115, to be re-added through the Weblate API). Keeps the removal
of the stale reminder_custom_with_value key, whose source string was
deleted on this branch (ExtraTranslation is fatal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:05:22 +02:00
fdee311658 Merge pull request 'Translations update from Weblate' (!52) from weblate-bot/calendula:weblate-calendula-strings into main
All checks were successful
Release — F-Droid repo + Gitea release / detect (push) Successful in 4s
Release — F-Droid repo + Gitea release / release (push) Has been skipped
Reviewed-on: #52
2026-07-03 14:04:16 +00:00
1ff41153e9 Merge pull request 'feat(contacts): mirror contact special dates into local calendars (#15)' (!57) from feat/contact-special-dates into release/v2.13.0
Reviewed-on: #57
2026-07-03 13:44:46 +00:00
49d04bcee0 Merge remote-tracking branch 'origin/release/v2.13.0' into feat/contact-special-dates
All checks were successful
Translations / check (pull_request) Successful in 26s
CI / ci (pull_request) Successful in 5m19s
# Conflicts:
#	app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt
#	app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt
#	app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt
2026-07-03 15:41:38 +02:00
5e470baf4e Merge pull request 'fix(edit): scrollable, grouped, pinned-header calendar picker (#29)' (!56) from fix/calendar-picker-scrollable into release/v2.13.0
Reviewed-on: #56
2026-07-03 13:35:48 +00:00
c8a4f90404 Merge remote-tracking branch 'origin/release/v2.13.0' into fix/calendar-picker-scrollable
All checks were successful
CI / ci (pull_request) Successful in 5m16s
2026-07-03 15:22:38 +02:00
0a89ff9d0e fix(edit): pin the picker title instead of a large collapsing header
Full-screen selection pickers opened with the LargeTopAppBar expanded, so the
title started below the bar and only settled into it after a scroll — a tall
empty header above a short option list. Give CollapsingScaffold a largeTopBar
flag and have FullScreenPicker use a pinned single-line TopAppBar, so every
picker (calendar, theme, week start, language, reminder, agenda range) shows
its title in the bar from the start. Settings and the calendar manager keep
the large collapsing header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:22:18 +02:00
0718afd5a6 fix(edit): make the new-event calendar picker a scrollable grouped list
The calendar picker was an AlertDialog holding a non-scrolling Column, so
accounts with many calendars overflowed the dialog's fixed height and the
entries past ~9 were unreachable (Codeberg #29).

Replace it with the app's FullScreenPicker (collapsing scaffold + scrollable
content), rendering calendars as connected GroupedRows grouped under their
owning account — the same grouping the visibility filter and calendar manager
use — with a colour chip per row and a check on the current selection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:13:27 +02:00
84e6402031 fix(contacts): serialize sync/teardown and stop retrying on lost permission
- The periodic job and the immediate 'Sync now'/foreground runs had no mutual
  exclusion, so two overlapping reconciles could each see no managed calendar
  and both create one (duplicate 'Birthdays', doubled events); likewise a
  disable-teardown racing an in-flight sync got its calendars recreated right
  after deletion. A lifecycle Mutex now makes sync() and teardown() atomic, and
  sync() re-reads the enabled flag inside the lock so a teardown always wins.

- The foreground resume trigger shared a unique work name with enable/'Sync
  now' under ExistingWorkPolicy.REPLACE, so a debounced foreground enqueue could
  cancel-and-swallow a pending enable sync (feature on, no calendars for up to a
  day). It now uses its own work name.

- doWork() mapped every exception to retry(), so a revoked calendar permission
  retried with backoff forever and never surfaced. A SecurityException now parks
  the feature in the stalled state instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:04:26 +02:00