From 25ff7ce7caad4a2e1c8ad5da975eec717e94dee6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 30 Jun 2026 14:58:14 +0200 Subject: [PATCH 01/49] docs(design): contact special-dates calendars + per-calendar multiple reminders Capture the full design for surfacing contact birthdays/anniversaries/custom dates as auto-updating local calendars (Codeberg #15), plus the #14 prerequisite (per-calendar multiple default reminders). One local calendar per type to reuse existing per-calendar color/visibility/reminder infra; one-way mirror with field-level managed events keyed on contact LOOKUP_KEY. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/contact-special-dates.md | 229 +++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 docs/design/contact-special-dates.md diff --git a/docs/design/contact-special-dates.md b/docs/design/contact-special-dates.md new file mode 100644 index 0000000..65bebb1 --- /dev/null +++ b/docs/design/contact-special-dates.md @@ -0,0 +1,229 @@ +# Design: Contact special-dates calendars (+ per-calendar multiple reminders) + +Status: **design / not yet implemented** +Date: 2026-06-30 +Tracking: Codeberg #15 (feature), Codeberg #14 (prerequisite) + +This document captures the full design for surfacing contact birthdays — +and, by extension, anniversaries and custom dates — as auto-updating local +calendars in Calendula. It also specifies #14 (per-calendar *multiple* +default reminders), which is a hard prerequisite and ships first. + +The two pieces are deliberately split across **two branches / two +shipments**: + +- `feat/per-calendar-multi-reminders` — #14, ships first. +- `feat/contact-special-dates` — #15, builds on #14. + +They *may* release together depending on timing, but #14 is independently +useful and is the foundation the special-dates calendars lean on. + +--- + +## Why this is worth doing carefully + +Calendula's identity is privacy: **no INTERNET permission**, and historically +**no `READ_CONTACTS`** — guest-picking uses a one-shot `ACTION_PICK` so the app +never holds the contacts permission (`EventEditScreen.kt:446-460`). + +This feature **declares `READ_CONTACTS`** for the first time. That is a +deliberate, owner-approved shift, mitigated by: + +- The permission is **opt-in and feature-gated** — requested only when the user + enables the feature, never at startup. +- Everything stays **offline**. Contacts are read locally and mirrored into a + local calendar; nothing leaves the device. +- The mirror is **one-way** (contacts → calendar). Calendula never writes to + Contacts. + +Store listing / About copy will need to explain the optional permission. + +--- + +## Prerequisite — #14: per-calendar *multiple* default reminders + +### Today + +- Events already support **multiple reminders** — `EventForm.reminders: + List` (`EventForm.kt:24`), each written as a separate + `CalendarContract.Reminders` row (`CalendarDataSource.kt:620-630`), each + firing independently. The runtime/notification path is done. +- The **defaults** layer is single-valued, though: + - Global: `defaultReminderMinutes` / `defaultAllDayReminderMinutes` — single + `Int` (`SettingsPrefs.kt:287,302`). + - Per-calendar override: `perCalendarReminderOverride: Map` and + the all-day variant (`SettingsPrefs.kt:350-381`), resolved by + `resolveDefaultReminder()` (`SettingsPrefs.kt:509-524`). + - UI: expandable per-calendar override section + (`SettingsScreen.kt:879-953`); `ReminderDefaultPicker` (`:956+`). + +### The change + +Widen the **defaults**, not the runtime, from one reminder to a list: + +1. Resolution model `Int` → `List` in global defaults + per-calendar + override + `resolveDefaultReminder()`. The DataStore string format already + parses `id=value;id=value` (`SettingsPrefs.kt:576-577`); `value` becomes a + comma-separated list. Keep the `Inherit / None / Minutes` override states + (`CalendarReminderOverride`, `SettingsPrefs.kt:493-500`) — `Minutes` carries + a list. +2. `ReminderDefaultPicker` → multi-select. Mirror the event-form multi-reminder + UI rather than inventing new interaction. +3. New events seed `EventForm.reminders` from the **resolved list** instead of + a single value. + +No new permissions, no provider changes, no scheduling changes. Type widening +plus one picker upgrade. + +### Why it gates #15 + +Each special-dates calendar is a normal local calendar, so it inherits the +**per-calendar reminder default** for free. A birthdays calendar is useless +without lead time (the reporter literally asked for "a week before *and* on the +day") — that requires *multiple* defaults, i.e. #14. + +--- + +## #15: Contact special-dates calendars + +### Core model: one-way mirror, one local calendar per type + +Contacts → **separate local calendars per type**: + +- "Birthdays" +- "Anniversaries" +- "Custom dates" (everything else / `TYPE_OTHER` / `TYPE_CUSTOM`) + +Separate-per-type is the key architectural decision: each is a normal local +calendar (`createLocalCalendar()`, `CalendarDataSource.kt:238-260`), so the +**existing per-calendar infra applies for free**: + +- **Color** → existing per-calendar color. +- **Show/hide** → existing calendar visibility. +- **Default reminders** → per-calendar override from #14. + +This shrinks the new surface dramatically: anniversaries and custom dates are +near copy-paste of birthdays (same engine, different `ContactsContract` data +kind, one extra local calendar each), and the settings sub-page does **not** +need to reinvent color/visibility/reminders. + +### Events: field-level managed, not read-only + +Calendula manages a few fields; the user owns the rest. + +- **Managed** (overwritten on every sync, disabled in the editor for these + events): title, start date, recurrence (`FREQ=YEARLY`), existence. +- **User-owned** (seeded once on insert, never touched again by sync): + location, reminders, notes, busy/free. + +The rule that makes this safe: sync performs a **targeted column update** +(managed columns only), **never delete-and-reinsert**. Reminders live in +separate rows, so once seeded they are simply never re-touched. A user can move +a birthday reminder to "2 weeks before" or add a location and neither is +clobbered. + +In the editor, when an event belongs to a managed calendar: disable the +title/date/recurrence fields; allow the rest. + +### Reconciliation identity + +Stamp each event with the contact's `LOOKUP_KEY` in `Events.SYNC_DATA1` (ours, +since it's a local calendar). Sync becomes an idempotent diff keyed on it: + +- contact added → insert +- birthday/date changed → targeted update of managed columns +- contact or date removed → delete the event + +Getting this wrong = duplicate birthdays on every sync, so the identity key is +non-negotiable. + +### Data source + +`ContactsContract.Data` rows of `Event.CONTENT_ITEM_TYPE`, split by +`Event.TYPE`: + +- `TYPE_BIRTHDAY` → Birthdays calendar +- `TYPE_ANNIVERSARY` → Anniversaries calendar +- `TYPE_OTHER` / `TYPE_CUSTOM` (+ label) → Custom dates calendar + +### Dates: year-less + age + +Many contacts store `--MM-DD` (no year). Handle both: + +- **Year present:** anchor `DTSTART` at that year; `FREQ=YEARLY`. We can show + age ("Jane turns 30") when the show-age option is on. +- **Year absent:** `FREQ=YEARLY` from an arbitrary anchor year; no age shown. + +Recurrence support is already there: `FREQ=YEARLY` + `UNTIL`/`COUNT`/`INTERVAL` +(`Recurrence.kt:32-174`). + +### Auto-update cadence + +Birthdays change rarely, so nothing live/expensive: + +- Sync **on enable**, **on app foreground**, and a **daily WorkManager** job + (catches new/edited/deleted contacts even if the app isn't opened). +- **No `ContentObserver`** — needs the process alive and buys almost nothing + for once-a-year events. + +### Permission + lifecycle + +- **Enable:** request `READ_CONTACTS` (feature-gated, contextual — mirror the + contextual `WRITE_CALENDAR` pattern at `EventEditScreen.kt:216-231`). On + grant, create the enabled calendars and run the first sync. +- **Disable:** delete the managed calendars + their events (with a confirm). +- **Permission revoked in system settings:** detect on next sync; surface that + the feature is stalled rather than failing silently. + +### Configurable settings sub-page + +Enabling the feature reveals a **dedicated sub-page** (not inline rows). Because +color/visibility/reminders already live in normal calendar settings, the +sub-page only carries the genuinely new knobs: + +- Master enable toggle. +- Per-type toggles: Birthdays / Anniversaries / Custom dates. +- Title/display template, e.g. `"{name}'s birthday"` (new translatable string). +- Show-age toggle (only meaningful when the year is known). +- (Possibly) sync-cadence / "sync now". +- Pointer to the normal calendar settings for color, visibility, reminders. + +The owner wants this broadly configurable — keep the sub-page the home for +display/behaviour options, and lean on existing per-calendar settings for the +rest. + +--- + +## Open questions / deferred + +- Title template: free-form vs a small set of presets. Start with one good + localized default; revisit configurability. +- Whether "Custom dates" is one bucket or split further by label. Start: one + bucket. +- Contact scope: all contacts vs per-account/group filtering. Start: all; + filtering is a later refinement. + +## Task outline + +### #14 — per-calendar multiple default reminders (`feat/per-calendar-multi-reminders`) +- [ ] Widen global + per-calendar reminder defaults `Int` → `List` + (`SettingsPrefs.kt`), keep `Inherit/None/Minutes(list)`. +- [ ] `resolveDefaultReminder()` returns a list. +- [ ] `ReminderDefaultPicker` → multi-select (mirror event-form UI). +- [ ] Seed new events' `EventForm.reminders` from the resolved list. +- [ ] Migration / parse compatibility for existing single-value stored prefs. +- [ ] Tests: resolution, parse round-trip, all-day variant. + +### #15 — contact special-dates (`feat/contact-special-dates`) +- [ ] Declare `READ_CONTACTS`; feature-gated contextual request on enable. +- [ ] Per-type local calendars (create on enable, delete on disable+confirm). +- [ ] Contacts reader: `Event` rows by `TYPE`, year-less handling. +- [ ] Sync engine: idempotent diff keyed on `LOOKUP_KEY` in `SYNC_DATA1`; + targeted managed-column updates; seed user-owned fields on insert only. +- [ ] `FREQ=YEARLY` event generation + age (when year known). +- [ ] Auto-update: on enable, on foreground, daily WorkManager. +- [ ] Editor: disable managed fields for managed-calendar events. +- [ ] Revoked-permission detection + stalled-state surface. +- [ ] Settings sub-page: enable, per-type toggles, title template, show-age. +- [ ] Translatable strings (titles, sub-page). +- [ ] About / store copy for the optional `READ_CONTACTS` permission. -- 2.49.1 From 2ae4c818ba7d84296745ea44270bce0cae3e53d4 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 30 Jun 2026 15:19:25 +0200 Subject: [PATCH 02/49] feat(reminders): allow multiple default reminders per calendar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codeberg #14. Event-level multi-reminders already worked; this widens the *defaults* layer from a single Int to a List so a calendar's default can carry several lead times (e.g. a birthday calendar: one week before *and* on the day). - SettingsPrefs: global + per-calendar timed/all-day defaults become List; CalendarReminderOverride.Minutes(List); resolveDefaultReminder returns a list. Storage stays backward-compatible — a legacy single value ("30") parses to [30], comma-joined for multiples, "none"/empty for no reminder. - ReminderDefaultPicker is now multi-select: M3 Checkbox rows over the existing grouped-tonal idiom, an exclusive "use default" group for per-calendar overrides, and a Custom row that adds an arbitrary lead time to the set. Optimistic local state so quick successive toggles don't race the settings flow round-trip. - New events seed their reminder list from the resolved default. Tests cover none/single/multiple round-trips, legacy single-value parsing, and per-calendar list overrides. lint + test + assembleDebug green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/data/prefs/SettingsPrefs.kt | 110 +++++++++------- .../calendula/ui/common/Picker.kt | 122 ++++++++++-------- .../calendula/ui/edit/EventEditViewModel.kt | 11 +- .../calendula/ui/settings/SettingsScreen.kt | 36 +++--- .../calendula/ui/settings/SettingsUiState.kt | 16 +-- .../ui/settings/SettingsViewModel.kt | 12 +- app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - .../calendula/data/prefs/SettingsPrefsTest.kt | 86 +++++++----- 9 files changed, 220 insertions(+), 175 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 1a1f1d5..4fed5ad 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -276,35 +276,36 @@ class SettingsPrefs @Inject constructor( } /** - * The default reminder lead time (minutes before start) prefilled on new - * **timed** events. `null` = no default reminder — the prior behaviour, kept - * as the factory default so existing users aren't surprised by reminders they - * never asked for. Stored as a string so "none" is distinct from a numeric - * value (and from an unset key, which is also "none"). Per-calendar overrides - * in [perCalendarReminderOverride] take precedence; all-day events instead use + * The default reminder lead times (minutes before start) prefilled on new + * **timed** events. The empty list = no default reminder — the prior + * behaviour, kept as the factory default so existing users aren't surprised by + * reminders they never asked for. Stored as a comma-joined list of minutes, or + * "none"/empty for no reminder (also the unset state). A legacy single value + * ("30") parses transparently to a one-element list. Per-calendar overrides in + * [perCalendarReminderOverride] take precedence; all-day events instead use * [defaultAllDayReminderMinutes]. Resolve with [resolveDefaultReminder]. */ - val defaultReminderMinutes: Flow = store.data.map { prefs -> - prefs[DEFAULT_REMINDER_KEY].toReminderMinutes() + val defaultReminderMinutes: Flow> = store.data.map { prefs -> + prefs[DEFAULT_REMINDER_KEY].toReminderList() } - suspend fun setDefaultReminderMinutes(minutes: Int?) { - store.edit { it[DEFAULT_REMINDER_KEY] = minutes?.toString() ?: NONE } + suspend fun setDefaultReminderMinutes(minutes: List) { + store.edit { it[DEFAULT_REMINDER_KEY] = minutes.toStoredReminders() } } /** - * The default reminder lead time prefilled on new **all-day** events, in + * The default reminder lead times prefilled on new **all-day** events, in * minutes before the start of the day. All-day events want day-scale lead * times ("1 day before"), so they have their own default rather than reusing - * the timed one. `null` = no default. Per-calendar overrides do **not** apply - * to all-day events — they always use this global value. + * the timed one. Empty list = no default. Per-calendar overrides do **not** + * apply to all-day events — they always use this global value. */ - val defaultAllDayReminderMinutes: Flow = store.data.map { prefs -> - prefs[DEFAULT_ALLDAY_REMINDER_KEY].toReminderMinutes() + val defaultAllDayReminderMinutes: Flow> = store.data.map { prefs -> + prefs[DEFAULT_ALLDAY_REMINDER_KEY].toReminderList() } - suspend fun setDefaultAllDayReminderMinutes(minutes: Int?) { - store.edit { it[DEFAULT_ALLDAY_REMINDER_KEY] = minutes?.toString() ?: NONE } + suspend fun setDefaultAllDayReminderMinutes(minutes: List) { + store.edit { it[DEFAULT_ALLDAY_REMINDER_KEY] = minutes.toStoredReminders() } } /** @@ -341,13 +342,13 @@ class SettingsPrefs @Inject constructor( /** * Per-calendar overrides of [defaultReminderMinutes] for **timed** events, * keyed by calendar id. A calendar **present** in the map overrides the global - * timed default for its new events: a `null` value means "no reminder", an int - * means that lead time. A calendar **absent** from the map inherits the global - * default. Serialised as `id=value;id=value`, with `none` for an explicit - * no-reminder override. (All-day events ignore this and use - * [defaultAllDayReminderMinutes].) + * timed default for its new events: an **empty** list means "no reminder", a + * non-empty list means those lead times. A calendar **absent** from the map + * inherits the global default. Serialised as `id=value;id=value`, where a value + * is a comma-joined minute list or `none` for an explicit no-reminder override. + * (All-day events ignore this and use [defaultAllDayReminderMinutes].) */ - val perCalendarReminderOverride: Flow> = store.data.map { prefs -> + val perCalendarReminderOverride: Flow>> = store.data.map { prefs -> parseReminderOverrides(prefs[CALENDAR_REMINDER_OVERRIDE_KEY]) } @@ -364,7 +365,7 @@ class SettingsPrefs @Inject constructor( * events, with the same semantics as [perCalendarReminderOverride] (absent = * inherit the global all-day default; present null = no reminder). */ - val perCalendarAllDayReminderOverride: Flow> = store.data.map { prefs -> + val perCalendarAllDayReminderOverride: Flow>> = store.data.map { prefs -> parseReminderOverrides(prefs[CALENDAR_ALLDAY_REMINDER_OVERRIDE_KEY]) } @@ -495,43 +496,46 @@ sealed interface CalendarReminderOverride { data object Inherit : CalendarReminderOverride /** Explicit "no reminder" for this calendar, regardless of the global default. */ data object None : CalendarReminderOverride - /** A specific lead time in minutes before the event start. */ - data class Minutes(val minutes: Int) : CalendarReminderOverride + /** Specific lead times in minutes before the event start (non-empty). */ + data class Minutes(val minutes: List) : CalendarReminderOverride } /** - * The lead time to prefill on a new event: the matching per-calendar override + * The lead times to prefill on a new event: the matching per-calendar override * if [calendarId] has one for this event kind, otherwise the global default for * that kind. All-day events consult [allDayOverrides] / [allDayGlobal]; timed - * events consult [timedOverrides] / [timedGlobal]. `null` = no reminder. Pure so - * it can be unit-tested. + * events consult [timedOverrides] / [timedGlobal]. The empty list = no reminder. + * Pure so it can be unit-tested. */ fun resolveDefaultReminder( - timedGlobal: Int?, - allDayGlobal: Int?, - timedOverrides: Map, - allDayOverrides: Map, + timedGlobal: List, + allDayGlobal: List, + timedOverrides: Map>, + allDayOverrides: Map>, calendarId: Long?, isAllDay: Boolean, -): Int? { +): List { val overrides = if (isAllDay) allDayOverrides else timedOverrides val global = if (isAllDay) allDayGlobal else timedGlobal return if (calendarId != null && overrides.containsKey(calendarId)) { - overrides[calendarId] + overrides.getValue(calendarId) } else { global } } -/** Apply a [CalendarReminderOverride] to an override map ([Inherit] removes the key). */ -private fun MutableMap.applyOverride( +/** + * Apply a [CalendarReminderOverride] to an override map ([Inherit] removes the + * key; [None] and an empty [Minutes] both store the empty list). + */ +private fun MutableMap>.applyOverride( calendarId: Long, override: CalendarReminderOverride, ) { when (override) { CalendarReminderOverride.Inherit -> remove(calendarId) - CalendarReminderOverride.None -> put(calendarId, null) - is CalendarReminderOverride.Minutes -> put(calendarId, override.minutes) + CalendarReminderOverride.None -> put(calendarId, emptyList()) + is CalendarReminderOverride.Minutes -> put(calendarId, override.minutes.normalizeReminders()) } } @@ -557,24 +561,36 @@ private fun parseWeekStart(stored: String?): WeekStartPref = when (stored) { private const val NONE = "none" private const val ENTRY_SEP = ";" private const val KEY_VALUE_SEP = "=" +private const val LIST_SEP = "," -private fun String?.toReminderMinutes(): Int? = when (this) { - null, "", NONE -> null - else -> toIntOrNull() +/** Distinct, ascending lead times — the canonical form stored and resolved. */ +private fun List.normalizeReminders(): List = distinct().sorted() + +/** + * Parse a stored reminder value into lead times. `null`/empty/"none" → empty + * list; a comma-joined list → its minutes; a legacy single value ("30") → a + * one-element list. Non-numeric parts are dropped defensively. + */ +private fun String?.toReminderList(): List = when { + this == null || isEmpty() || this == NONE -> emptyList() + else -> split(LIST_SEP).mapNotNull { it.trim().toIntOrNull() }.normalizeReminders() } -private fun parseReminderOverrides(stored: String?): Map { +/** Serialise lead times for storage: "none" when empty, else comma-joined. */ +private fun List.toStoredReminders(): String = + if (isEmpty()) NONE else normalizeReminders().joinToString(LIST_SEP) { it.toString() } + +private fun parseReminderOverrides(stored: String?): Map> { if (stored.isNullOrBlank()) return emptyMap() return stored.split(ENTRY_SEP).mapNotNull { entry -> val parts = entry.split(KEY_VALUE_SEP).takeIf { it.size == 2 } ?: return@mapNotNull null val id = parts[0].toLongOrNull() ?: return@mapNotNull null - val value = if (parts[1] == NONE) null else parts[1].toIntOrNull() ?: return@mapNotNull null - id to value + id to parts[1].toReminderList() }.toMap() } -private fun serializeReminderOverrides(map: Map): String = - map.entries.joinToString(ENTRY_SEP) { (id, minutes) -> "$id$KEY_VALUE_SEP${minutes ?: NONE}" } +private fun serializeReminderOverrides(map: Map>): String = + map.entries.joinToString(ENTRY_SEP) { (id, minutes) -> "$id$KEY_VALUE_SEP${minutes.toStoredReminders()}" } private inline fun > String?.toEnum(default: E): E = this?.let { stored -> enumValues().firstOrNull { it.name == stored } } ?: default diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index ce6abae..847b5a1 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Checkbox import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -26,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -111,10 +113,15 @@ fun OptionPicker( } /** - * Reminder-default picker, full-screen: the grouped list (with an optional "Use - * default reminder" row and a "None" row), the [presets] as lead-time rows, and - * a "Custom" row that expands an inline number field plus a segmented unit - * selector. Returns the choice as a [CalendarReminderOverride]. + * Reminder-default picker, full-screen and **multi-select**: each [presets] + * lead time (plus any chosen custom value) is a checkbox row that toggles + * independently, so a default can carry several reminders ("1 week before" *and* + * "on the day"). A per-calendar picker ([allowInherit]) gains an exclusive "Use + * default reminder" row above the list; choosing it defers to the global default + * and clears the checked times. With nothing checked and inherit off, the choice + * is [CalendarReminderOverride.None] (an explicit no-reminder). A "Custom" row + * expands an inline number field plus a unit selector to add an arbitrary lead + * time to the set. Changes apply live via [onSelect]; the user leaves via back. */ @Composable fun ReminderDefaultPicker( @@ -125,57 +132,74 @@ fun ReminderDefaultPicker( onSelect: (CalendarReminderOverride) -> Unit, onDismiss: () -> Unit, ) { - val selectedMinutes = (selected as? CalendarReminderOverride.Minutes)?.minutes - val customSelected = selectedMinutes != null && selectedMinutes !in presets - val seed = decomposeReminder(selectedMinutes?.takeIf { customSelected }) + // Optimistic local state: the chosen override is authoritative while the + // picker is open, so quick successive toggles compose on each other instead + // of racing the round-trip through the settings flow (which would drop a + // toggle made before the previous write echoes back). Seeded once on open; + // the picker leaves composition on dismiss, so reopening re-reads [selected]. + var current by remember { mutableStateOf(selected) } + val inherits = current is CalendarReminderOverride.Inherit + val selectedMinutes = (current as? CalendarReminderOverride.Minutes)?.minutes.orEmpty() + // Presets plus any selected custom (non-preset) values, each as its own row. + val rows = presets + selectedMinutes.filter { it !in presets }.sorted() var customExpanded by rememberSaveable { mutableStateOf(false) } - var amountText by rememberSaveable { mutableStateOf(seed.first) } - var unit by rememberSaveable { mutableStateOf(seed.second) } + var amountText by rememberSaveable { mutableStateOf("") } + var unit by rememberSaveable { mutableStateOf(ReminderUnit.Minutes) } - val options = buildList { - if (allowInherit) add(CalendarReminderOverride.Inherit) - add(CalendarReminderOverride.None) - presets.forEach { add(CalendarReminderOverride.Minutes(it)) } + fun apply(override: CalendarReminderOverride) { + current = override + onSelect(override) } - val rowCount = options.size + 1 // + the custom row + // Emit the override for a new set of minutes: empty collapses to None, which + // (with inherit unchosen) reads as an explicit "no reminder". + fun emit(minutes: List) { + val norm = minutes.distinct().sorted() + apply( + if (norm.isEmpty()) { + CalendarReminderOverride.None + } else { + CalendarReminderOverride.Minutes(norm) + }, + ) + } + fun toggle(minute: Int) = + emit(if (minute in selectedMinutes) selectedMinutes - minute else selectedMinutes + minute) FullScreenPicker(title = title, onDismiss = onDismiss) { - options.forEachIndexed { index, option -> - val isSelected = option == selected + // Exclusive "use default" choice (per-calendar only): its own group above + // the multi-select list, since inheriting is mutually exclusive with + // picking specific times. + if (allowInherit) { GroupedRow( - title = reminderOverrideLabel(option), - position = positionOf(index, rowCount), - selected = isSelected, - trailing = if (isSelected) { + title = stringResource(R.string.reminder_use_default), + position = Position.Alone, + selected = inherits, + trailing = if (inherits) { { SelectedCheck() } } else { null }, - onClick = { - onSelect(option) - onDismiss() - }, + onClick = { apply(CalendarReminderOverride.Inherit) }, + ) + Spacer(Modifier.height(24.dp)) + } + val rowCount = rows.size + 1 // + the custom row + rows.forEachIndexed { index, minute -> + val checked = minute in selectedMinutes + GroupedRow( + title = reminderLeadTimeLabel(minute), + position = positionOf(index, rowCount), + selected = checked, + trailing = { Checkbox(checked = checked, onCheckedChange = { toggle(minute) }) }, + onClick = { toggle(minute) }, ) } // When expanded, the Custom row connects downward into the editor card // so the two read as one grouped container (the per-calendar pattern). GroupedRow( - title = if (customSelected) { - stringResource( - R.string.reminder_custom_with_value, - reminderLeadTimeLabel(selectedMinutes!!), - ) - } else { - stringResource(R.string.event_edit_reminder_custom) - }, - position = if (customExpanded) Position.Top else positionOf(options.size, rowCount), - selected = customSelected, - trailing = if (customSelected) { - { SelectedCheck() } - } else { - null - }, + title = stringResource(R.string.event_edit_reminder_custom), + position = if (customExpanded) Position.Top else positionOf(rows.size, rowCount), onClick = { customExpanded = !customExpanded }, ) AnimatedVisibility( @@ -189,8 +213,9 @@ fun ReminderDefaultPicker( unit = unit, onUnitChange = { unit = it }, onConfirm = { minutes -> - onSelect(CalendarReminderOverride.Minutes(minutes)) - onDismiss() + emit(selectedMinutes + minutes) + amountText = "" + customExpanded = false }, ) } @@ -428,18 +453,3 @@ fun agendaRangeLabel(range: AgendaRange): String = when (range) { is AgendaRange.Custom -> pluralStringResource(R.plurals.agenda_range_days, range.days, range.days) } -@Composable -private fun reminderOverrideLabel(override: CalendarReminderOverride): String = when (override) { - CalendarReminderOverride.Inherit -> stringResource(R.string.reminder_use_default) - CalendarReminderOverride.None -> stringResource(R.string.reminder_none) - is CalendarReminderOverride.Minutes -> reminderLeadTimeLabel(override.minutes) -} - -/** Seed the custom editor: the largest exact unit for [minutes] (null → empty). */ -private fun decomposeReminder(minutes: Int?): Pair = when { - minutes == null -> "" to ReminderUnit.Minutes - minutes % 10_080 == 0 -> (minutes / 10_080).toString() to ReminderUnit.Weeks - minutes % 1_440 == 0 -> (minutes / 1_440).toString() to ReminderUnit.Days - minutes % 60 == 0 -> (minutes / 60).toString() to ReminderUnit.Hours - else -> minutes.toString() to ReminderUnit.Minutes -} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt index 50a2f7d..7190beb 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt @@ -108,10 +108,10 @@ class EventEditViewModel @Inject constructor( ) private data class ReminderDefaults( - val timed: Int?, - val allDay: Int?, - val timedOverrides: Map, - val allDayOverrides: Map, + val timed: List, + val allDay: List, + val timedOverrides: Map>, + val allDayOverrides: Map>, ) private data class ExternalInputs( @@ -260,7 +260,7 @@ class EventEditViewModel @Inject constructor( // Re-check after suspending: bail if the form closed or the user edited. val form = _form.value ?: return@launch if (_editTarget.value != null || _remindersTouched.value) return@launch - val default = resolveDefaultReminder( + val reminders = resolveDefaultReminder( timedGlobal = defaults.timed, allDayGlobal = defaults.allDay, timedOverrides = defaults.timedOverrides, @@ -268,7 +268,6 @@ class EventEditViewModel @Inject constructor( calendarId = targetId, isAllDay = form.isAllDay, ) - val reminders = listOfNotNull(default) _form.value = form.copy(reminders = reminders) // Surface the section so an auto-applied default is visible and // removable, even when Reminders isn't a default-shown field. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index d3fc450..cbc5048 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -969,7 +969,7 @@ private fun NotificationsScreen( presets = REMINDER_PRESETS, selected = state.defaultReminderMinutes.toReminderChoice(), allowInherit = false, - onSelect = { viewModel.setDefaultReminderMinutes(it.toMinutesOrNull()) }, + onSelect = { viewModel.setDefaultReminderMinutes(it.toMinutesList()) }, onDismiss = { showDefaultReminder = false }, ) } @@ -979,7 +979,7 @@ private fun NotificationsScreen( presets = ALLDAY_REMINDER_PRESETS, selected = state.defaultAllDayReminderMinutes.toReminderChoice(), allowInherit = false, - onSelect = { viewModel.setDefaultAllDayReminderMinutes(it.toMinutesOrNull()) }, + onSelect = { viewModel.setDefaultAllDayReminderMinutes(it.toMinutesList()) }, onDismiss = { showAllDayReminder = false }, ) } @@ -1028,13 +1028,13 @@ private fun NotificationsScreen( /** Which calendar + event kind a per-calendar reminder-override dialog targets. */ private data class OverrideTarget(val calendarId: Long, val isAllDay: Boolean) -/** A global default (null = none) as a picker choice for selection highlighting. */ -private fun Int?.toReminderChoice(): CalendarReminderOverride = - if (this == null) CalendarReminderOverride.None else CalendarReminderOverride.Minutes(this) +/** A global default (empty = none) as a picker choice for selection highlighting. */ +private fun List.toReminderChoice(): CalendarReminderOverride = + if (isEmpty()) CalendarReminderOverride.None else CalendarReminderOverride.Minutes(this) /** A picked choice as global-default minutes (Inherit isn't offered for globals). */ -private fun CalendarReminderOverride.toMinutesOrNull(): Int? = - (this as? CalendarReminderOverride.Minutes)?.minutes +private fun CalendarReminderOverride.toMinutesList(): List = + (this as? CalendarReminderOverride.Minutes)?.minutes ?: emptyList() /** * Whether Calendula is exempt from battery optimisation, re-read on every @@ -1109,27 +1109,33 @@ private fun formatTimeOfDay(context: Context, minutesOfDay: Int): String { } /** The stored override for [calendarId], as a picker choice (absent → inherit). */ -private fun Map.choiceFor(calendarId: Long): CalendarReminderOverride = when { +private fun Map>.choiceFor(calendarId: Long): CalendarReminderOverride = when { !containsKey(calendarId) -> CalendarReminderOverride.Inherit - this[calendarId] == null -> CalendarReminderOverride.None - else -> CalendarReminderOverride.Minutes(this.getValue(calendarId)!!) + this.getValue(calendarId).isEmpty() -> CalendarReminderOverride.None + else -> CalendarReminderOverride.Minutes(this.getValue(calendarId)) } -/** Label for a global-default choice: null → "None", else the lead time. */ +/** Label for a global-default choice: empty → "None", else the lead times joined. */ @Composable -private fun reminderChoiceLabel(minutes: Int?): String = - if (minutes == null) stringResource(R.string.reminder_none) else reminderLeadTimeLabel(minutes) +private fun reminderChoiceLabel(minutes: List): String { + if (minutes.isEmpty()) return stringResource(R.string.reminder_none) + // reminderLeadTimeLabel is @Composable, so resolve each part in a loop rather + // than a (non-composable) joinToString transform. + val parts = ArrayList(minutes.size) + for (m in minutes) parts.add(reminderLeadTimeLabel(m)) + return parts.joinToString(", ") +} /** Row summary for a calendar: its override, or the inherited global default. */ @Composable private fun calendarOverrideSummary( choice: CalendarReminderOverride, - globalDefault: Int?, + globalDefault: List, ): String = when (choice) { CalendarReminderOverride.Inherit -> stringResource(R.string.settings_calendar_reminder_inherits, reminderChoiceLabel(globalDefault)) CalendarReminderOverride.None -> stringResource(R.string.reminder_none) - is CalendarReminderOverride.Minutes -> reminderLeadTimeLabel(choice.minutes) + is CalendarReminderOverride.Minutes -> reminderChoiceLabel(choice.minutes) } // --------------------------------------------------------------------------- diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index 52c23ae..4ad49a7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -44,24 +44,24 @@ data class SettingsUiState( /** Whether Calendula posts reminder notifications (v1.4). */ val remindersEnabled: Boolean = true, /** - * The default reminder lead time (minutes) prefilled on new timed events; - * null = no default reminder. Per-calendar overrides take precedence. + * The default reminder lead times (minutes) prefilled on new timed events; + * empty = no default reminder. Per-calendar overrides take precedence. */ - val defaultReminderMinutes: Int? = null, - /** The default reminder lead time prefilled on new all-day events; null = none. */ - val defaultAllDayReminderMinutes: Int? = null, + val defaultReminderMinutes: List = emptyList(), + /** The default reminder lead times prefilled on new all-day events; empty = none. */ + val defaultAllDayReminderMinutes: List = emptyList(), /** Wall-clock time (minutes from midnight) all-day reminders fire at; default 09:00. */ val allDayReminderTimeMinutes: Int = SettingsPrefs.DEFAULT_ALLDAY_REMINDER_TIME, /** How long the notification "Snooze" action defers a reminder; default 10 min. */ val snoozeMinutes: Int = SettingsPrefs.DEFAULT_SNOOZE_MINUTES, /** * Per-calendar overrides of [defaultReminderMinutes] for timed events: a - * calendar present in the map overrides the global default (null value = no + * calendar present in the map overrides the global default (empty value = no * reminder); absent = inherit the global default. */ - val perCalendarReminderOverride: Map = emptyMap(), + val perCalendarReminderOverride: Map> = emptyMap(), /** Per-calendar overrides of [defaultAllDayReminderMinutes] for all-day events. */ - val perCalendarAllDayReminderOverride: Map = emptyMap(), + val perCalendarAllDayReminderOverride: Map> = emptyMap(), /** Writable calendars, shown as per-calendar reminder-override rows. */ val writableCalendars: List = emptyList(), /** diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 557ab21..ae33454 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -139,15 +139,15 @@ class SettingsViewModel @Inject constructor( private data class ReminderDefaults( val allowColor: Boolean, - val defaultReminder: Int?, - val allDayReminder: Int?, + val defaultReminder: List, + val allDayReminder: List, val allDayReminderTime: Int, val snoozeMinutes: Int, ) private data class ReminderOverrides( - val timed: Map, - val allDay: Map, + val timed: Map>, + val allDay: Map>, val calendars: List, ) @@ -255,11 +255,11 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { prefs.setAutofocusEventTitle(enabled) } } - fun setDefaultReminderMinutes(minutes: Int?) { + fun setDefaultReminderMinutes(minutes: List) { viewModelScope.launch { prefs.setDefaultReminderMinutes(minutes) } } - fun setDefaultAllDayReminderMinutes(minutes: Int?) { + fun setDefaultAllDayReminderMinutes(minutes: List) { viewModelScope.launch { prefs.setDefaultAllDayReminderMinutes(minutes) } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d6c9ab0..d4f6945 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -316,7 +316,6 @@ Keine Standard-Erinnerung verwenden Anzahl - Benutzerdefiniert (%1$s) Übernehmen Standard pro Kalender überschreiben — getrennt für Termine mit Uhrzeit und ganztägige Termine. Ein Kalender kann den Standard übernehmen, weglassen oder einen eigenen festlegen. Standard (%1$s) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d121f90..1f96a98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -324,7 +324,6 @@ None Use default reminder Amount - Custom (%1$s) Set Per-calendar reminders Override the default per calendar — separately for timed and all-day events. A calendar can keep the default, drop it, or set its own. diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt index eec1075..4ae1c9b 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt @@ -183,22 +183,38 @@ class SettingsPrefsTest { } @Test - fun `default reminder is none until set`(@TempDir tempDir: Path) = runTest { + fun `default reminder is empty until set`(@TempDir tempDir: Path) = runTest { val prefs = SettingsPrefs(newDataStore(tempDir)) - assertThat(prefs.defaultReminderMinutes.first()).isNull() + assertThat(prefs.defaultReminderMinutes.first()).isEmpty() } @Test - fun `default reminder round-trips, including none`(@TempDir tempDir: Path) = runTest { + fun `default reminder round-trips none, single, and multiple lead times`(@TempDir tempDir: Path) = runTest { val prefs = SettingsPrefs(newDataStore(tempDir)) - prefs.setDefaultReminderMinutes(30) - assertThat(prefs.defaultReminderMinutes.first()).isEqualTo(30) - prefs.setDefaultReminderMinutes(null) - assertThat(prefs.defaultReminderMinutes.first()).isNull() + prefs.setDefaultReminderMinutes(listOf(30)) + assertThat(prefs.defaultReminderMinutes.first()).containsExactly(30) + // Several lead times persist, normalised to distinct ascending order. + prefs.setDefaultReminderMinutes(listOf(10_080, 0, 30, 30)) + assertThat(prefs.defaultReminderMinutes.first()).containsExactly(0, 30, 10_080).inOrder() + prefs.setDefaultReminderMinutes(emptyList()) + assertThat(prefs.defaultReminderMinutes.first()).isEmpty() } @Test - fun `garbage stored default reminder reads as none`(@TempDir tempDir: Path) = runTest { + fun `legacy single-value default reminder parses as a one-element list`(@TempDir tempDir: Path) = runTest { + val store = newDataStore(tempDir) + val prefs = SettingsPrefs(store) + // The pre-multi-reminder format stored a bare integer string. + store.updateData { p -> + val m = p.toMutablePreferences() + m[SettingsPrefs.DEFAULT_REMINDER_KEY] = "30" + m + } + assertThat(prefs.defaultReminderMinutes.first()).containsExactly(30) + } + + @Test + fun `garbage stored default reminder reads as empty`(@TempDir tempDir: Path) = runTest { val store = newDataStore(tempDir) val prefs = SettingsPrefs(store) store.updateData { p -> @@ -206,7 +222,7 @@ class SettingsPrefsTest { m[SettingsPrefs.DEFAULT_REMINDER_KEY] = "soon" m } - assertThat(prefs.defaultReminderMinutes.first()).isNull() + assertThat(prefs.defaultReminderMinutes.first()).isEmpty() } @Test @@ -214,16 +230,16 @@ class SettingsPrefsTest { val prefs = SettingsPrefs(newDataStore(tempDir)) assertThat(prefs.perCalendarReminderOverride.first()).isEmpty() - prefs.setCalendarReminderOverride(7L, CalendarReminderOverride.Minutes(15)) + prefs.setCalendarReminderOverride(7L, CalendarReminderOverride.Minutes(listOf(15, 10_080))) prefs.setCalendarReminderOverride(9L, CalendarReminderOverride.None) prefs.perCalendarReminderOverride.first().let { map -> - assertThat(map).containsExactly(7L, 15, 9L, null) + assertThat(map).containsExactly(7L, listOf(15, 10_080), 9L, emptyList()) } - // Inherit drops the override entirely (absent != null value). + // Inherit drops the override entirely (absent != an empty-list value). prefs.setCalendarReminderOverride(9L, CalendarReminderOverride.Inherit) prefs.perCalendarReminderOverride.first().let { map -> - assertThat(map).containsExactly(7L, 15) + assertThat(map).containsExactly(7L, listOf(15, 10_080)) assertThat(map.containsKey(9L)).isFalse() } } @@ -231,48 +247,48 @@ class SettingsPrefsTest { @Test fun `all-day default round-trips, including none`(@TempDir tempDir: Path) = runTest { val prefs = SettingsPrefs(newDataStore(tempDir)) - assertThat(prefs.defaultAllDayReminderMinutes.first()).isNull() - prefs.setDefaultAllDayReminderMinutes(1_440) - assertThat(prefs.defaultAllDayReminderMinutes.first()).isEqualTo(1_440) - prefs.setDefaultAllDayReminderMinutes(null) - assertThat(prefs.defaultAllDayReminderMinutes.first()).isNull() + assertThat(prefs.defaultAllDayReminderMinutes.first()).isEmpty() + prefs.setDefaultAllDayReminderMinutes(listOf(1_440)) + assertThat(prefs.defaultAllDayReminderMinutes.first()).containsExactly(1_440) + prefs.setDefaultAllDayReminderMinutes(emptyList()) + assertThat(prefs.defaultAllDayReminderMinutes.first()).isEmpty() } @Test fun `per-calendar all-day override round-trips independently of the timed one`(@TempDir tempDir: Path) = runTest { val prefs = SettingsPrefs(newDataStore(tempDir)) - prefs.setCalendarReminderOverride(7L, CalendarReminderOverride.Minutes(15)) - prefs.setCalendarAllDayReminderOverride(7L, CalendarReminderOverride.Minutes(1_440)) - assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(7L, 15) - assertThat(prefs.perCalendarAllDayReminderOverride.first()).containsExactly(7L, 1_440) + prefs.setCalendarReminderOverride(7L, CalendarReminderOverride.Minutes(listOf(15))) + prefs.setCalendarAllDayReminderOverride(7L, CalendarReminderOverride.Minutes(listOf(1_440))) + assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(7L, listOf(15)) + assertThat(prefs.perCalendarAllDayReminderOverride.first()).containsExactly(7L, listOf(1_440)) // Clearing the all-day override leaves the timed one untouched. prefs.setCalendarAllDayReminderOverride(7L, CalendarReminderOverride.Inherit) assertThat(prefs.perCalendarAllDayReminderOverride.first()).isEmpty() - assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(7L, 15) + assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(7L, listOf(15)) } @Test fun `resolveDefaultReminder picks the kind-matching override or global`() { - val timed = mapOf(7L to 15, 9L to null) - val allDay = mapOf(7L to 2_880) + val timed = mapOf(7L to listOf(15, 10_080), 9L to emptyList()) + val allDay = mapOf(7L to listOf(2_880)) fun resolve(calendarId: Long?, isAllDay: Boolean) = resolveDefaultReminder( - timedGlobal = 30, - allDayGlobal = 1_440, + timedGlobal = listOf(30), + allDayGlobal = listOf(1_440), timedOverrides = timed, allDayOverrides = allDay, calendarId = calendarId, isAllDay = isAllDay, ) - // Timed: minutes override, explicit none, inherit global, no calendar. - assertThat(resolve(7L, isAllDay = false)).isEqualTo(15) - assertThat(resolve(9L, isAllDay = false)).isNull() - assertThat(resolve(5L, isAllDay = false)).isEqualTo(30) - assertThat(resolve(null, isAllDay = false)).isEqualTo(30) + // Timed: multi-value override, explicit none, inherit global, no calendar. + assertThat(resolve(7L, isAllDay = false)).containsExactly(15, 10_080).inOrder() + assertThat(resolve(9L, isAllDay = false)).isEmpty() + assertThat(resolve(5L, isAllDay = false)).containsExactly(30) + assertThat(resolve(null, isAllDay = false)).containsExactly(30) // All-day: its own override wins; absent → all-day global; a timed-only // override (cal 9) does not bleed into all-day. - assertThat(resolve(7L, isAllDay = true)).isEqualTo(2_880) - assertThat(resolve(9L, isAllDay = true)).isEqualTo(1_440) - assertThat(resolve(5L, isAllDay = true)).isEqualTo(1_440) + assertThat(resolve(7L, isAllDay = true)).containsExactly(2_880) + assertThat(resolve(9L, isAllDay = true)).containsExactly(1_440) + assertThat(resolve(5L, isAllDay = true)).containsExactly(1_440) } @Test -- 2.49.1 From c2d88e744ee5ece0ffbb7e740b2c04207e69fcf6 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 30 Jun 2026 15:06:58 +0200 Subject: [PATCH 03/49] fix(edit): write occurrence exceptions with DURATION, not DTEND MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Saving "only this event" on a recurring event inserts a modified-occurrence exception at Events.CONTENT_EXCEPTION_URI. buildOccurrenceExceptionValues set the occurrence's end as DTEND, but the provider rejects that outright — CalendarProvider2.checkAllowedInException throws IllegalArgumentException: Exceptions can't overwrite dtend so the insert failed, the save surfaced as SaveUiState.Failed, and the edit screen reappeared with no change applied. "This and following" and "all events" never go through the exception insert, which is why only "only this event" broke (Codeberg #16, verified on a Pixel 10 / Android 16). An exception is a single instance whose end the provider derives from DTSTART + DURATION (clearing the inherited RRULE itself), so carry the length as DURATION and drop DTEND — the same shape AOSP Calendar/Etar use. Verified on-device: editing one occurrence now applies to just that occurrence and leaves the rest of the series untouched. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/data/calendar/EventWriteMapper.kt | 12 +++++++++--- .../calendula/data/calendar/EventWriteMapperTest.kt | 9 +++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt index 0f50930..2c3928c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt @@ -118,8 +118,14 @@ internal fun buildEventUpdateValues( * provider clone the series row and apply these on top. Unlike the series * update there is no dirty check — the exception is a fresh row, so every * form-backed column is written (empty optionals as explicit NULLs, since the - * clone starts from the parent's values). An exception is a single event: - * DTEND, never RRULE/DURATION. + * clone starts from the parent's values). + * + * The occurrence's length travels as DURATION, never DTEND: the provider + * rejects DTEND on an exception outright (`CalendarProvider2`: + * "Exceptions can't overwrite dtend") and derives the instance end from + * DTSTART + DURATION itself, clearing the inherited RRULE in the process. This + * matches how AOSP Calendar/Etar write exceptions; sending DTEND is what made + * "only this event" fail on-device (Codeberg #16). */ internal fun buildOccurrenceExceptionValues( form: EventForm, @@ -131,7 +137,7 @@ internal fun buildOccurrenceExceptionValues( put(CalendarContract.Events.TITLE, form.title.trim()) put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0) put(CalendarContract.Events.DTSTART, times.dtStartMillis) - put(CalendarContract.Events.DTEND, times.dtEndMillis) + put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay)) put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone) put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue()) put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue()) diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt index 40aac33..f1be42b 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapperTest.kt @@ -199,11 +199,12 @@ class EventWriteMapperTest { assertThat(values[CalendarContract.Events.TITLE]).isEqualTo("Moved") assertThat(values[CalendarContract.Events.EVENT_LOCATION]).isEqualTo("Berlin") assertThat(values[CalendarContract.Events.DTSTART]).isEqualTo(1_781_164_800_000L) - assertThat(values[CalendarContract.Events.DTEND]) - .isEqualTo(1_781_164_800_000L + 5_400_000L) - // A single occurrence never carries its own rule. + // The occurrence's length travels as DURATION, never DTEND — the provider + // rejects DTEND on an exception ("Exceptions can't overwrite dtend") and + // derives the end from DTSTART + DURATION, clearing the inherited RRULE. + assertThat(values[CalendarContract.Events.DURATION]).isEqualTo("P5400S") + assertThat(values).doesNotContainKey(CalendarContract.Events.DTEND) assertThat(values).doesNotContainKey(CalendarContract.Events.RRULE) - assertThat(values).doesNotContainKey(CalendarContract.Events.DURATION) } @Test -- 2.49.1 From f2fb3d62795c8e9860659b9d949ffe7e6063ca7c Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 30 Jun 2026 16:18:47 +0200 Subject: [PATCH 04/49] feat(debug): mark debug builds with a distinct icon, label, and ribbon Make a debug install unmistakable next to the production app, Flutter-style: - src/debug resource overrides: "Calendula Debug" label and a burnt-orange launcher-icon background (production stays slate). The .debug applicationId suffix already lets both install side by side. - DebugRibbon: a "DEBUG" corner ribbon overlaid in MainActivity, gated on BuildConfig.DEBUG so release/releaseTest never show it. Enables the buildConfig feature for the flag. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle.kts | 2 + app/src/debug/res/values/colors.xml | 10 ++++ app/src/debug/res/values/strings.xml | 10 ++++ .../jeanlucmakiola/calendula/MainActivity.kt | 33 +++++++----- .../calendula/ui/common/DebugRibbon.kt | 50 +++++++++++++++++++ 5 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 app/src/debug/res/values/colors.xml create mode 100644 app/src/debug/res/values/strings.xml create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b20a171..72977e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -90,6 +90,8 @@ android { buildFeatures { compose = true + // BuildConfig.DEBUG gates the in-app debug ribbon (see DebugRibbon). + buildConfig = true } // Don't embed AGP's dependency-metadata block in the APK signing block. It's diff --git a/app/src/debug/res/values/colors.xml b/app/src/debug/res/values/colors.xml new file mode 100644 index 0000000..87de5a9 --- /dev/null +++ b/app/src/debug/res/values/colors.xml @@ -0,0 +1,10 @@ + + + + #FFB23B00 + diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml new file mode 100644 index 0000000..5dad449 --- /dev/null +++ b/app/src/debug/res/values/strings.xml @@ -0,0 +1,10 @@ + + + + Calendula Debug + diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index a2de42c..ede492a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -8,6 +8,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -29,6 +30,7 @@ import de.jeanlucmakiola.calendula.ui.common.LocalShowHourLines import de.jeanlucmakiola.calendula.ui.common.LocalUse24HourFormat import de.jeanlucmakiola.calendula.ui.WidgetNavRequest import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.DebugRibbon import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport @@ -101,19 +103,24 @@ class MainActivity : AppCompatActivity() { darkTheme = darkTheme, dynamicColor = settings.dynamicColor, ) { - CompositionLocalProvider( - LocalUse24HourFormat provides use24Hour, - LocalShowHourLines provides settings.showHourLines, - ) { - RootScreen( - modifier = Modifier.fillMaxSize(), - requestedDetailKey = requestedDetailKey, - onDetailKeyConsumed = { requestedDetailKey = null }, - widgetNavRequest = requestedNav, - onWidgetNavConsumed = { requestedNav = null }, - requestedImportUri = requestedImportUri, - onImportConsumed = { requestedImportUri = null }, - ) + Box(modifier = Modifier.fillMaxSize()) { + CompositionLocalProvider( + LocalUse24HourFormat provides use24Hour, + LocalShowHourLines provides settings.showHourLines, + ) { + RootScreen( + modifier = Modifier.fillMaxSize(), + requestedDetailKey = requestedDetailKey, + onDetailKeyConsumed = { requestedDetailKey = null }, + widgetNavRequest = requestedNav, + onWidgetNavConsumed = { requestedNav = null }, + requestedImportUri = requestedImportUri, + onImportConsumed = { requestedImportUri = null }, + ) + } + // A persistent corner marker so a debug build is never + // mistaken for the production app; compiled out of release. + if (BuildConfig.DEBUG) DebugRibbon() } pendingCrashReport?.let { report -> CrashReportDialog( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt new file mode 100644 index 0000000..3ca1381 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/DebugRibbon.kt @@ -0,0 +1,50 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex + +/** + * A Flutter-style "DEBUG" corner ribbon, drawn across the top-right corner of + * the app. Deliberately a stark, un-themed marker (not a product component) so a + * debug build is unmistakable at a glance — gate it on `BuildConfig.DEBUG` at the + * call site so it never reaches a release build. Non-interactive: it's a plain + * label with no pointer handler, so taps fall through to whatever is beneath it. + * + * Drop it in as the last child of a full-screen [androidx.compose.foundation.layout.Box] + * so it overlays the UI. + */ +@Composable +fun BoxScope.DebugRibbon() { + Text( + text = "DEBUG", + color = Color.White, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.TopEnd) + .zIndex(1f) + // Push the band out so its midline crosses the very corner, then + // rotate it to the classic 45° ribbon. + .offset(x = 36.dp, y = 24.dp) + .rotate(45f) + .background(Color(0xFFB23B00)) + .width(140.dp) + .padding(vertical = 2.dp), + ) +} -- 2.49.1 From 048f407ba1ff95be05cf0ef75d7a0b741803ad56 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 00:40:46 +0200 Subject: [PATCH 05/49] docs(changelog): record v2.13.0 unreleased changes Add an [Unreleased] section covering the two user-facing changes in release/v2.13.0: multiple default reminders per calendar (Codeberg #14, @moonj) and the fix for editing a single occurrence of a recurring event (Codeberg #16). The debug-build ribbon is dev-only and intentionally omitted. Define the [#14] and [#16] links. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab8105..61779f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Set more than one default reminder per calendar. A calendar's default + reminders — and the global defaults under Settings → Notifications — can now + hold several lead times instead of just one, so new events can start with, say, + a reminder a week before *and* one on the day. The reminder pickers are now + multi-select; per-calendar overrides can still inherit the global default or + turn reminders off entirely. Thanks to @moonj for the suggestion ([#14]). + +### Fixed +- Editing a single occurrence of a recurring event works again. Choosing **Only + this event** and saving a change to one event in a repeating series silently + did nothing — the change was rejected and the edit form simply reappeared with + nothing applied. The edited occurrence is now stored correctly, so the change + lands on just that one event and leaves the rest of the series untouched + ([#16]). + ## [2.12.0] — 2026-06-28 ### Added @@ -729,3 +747,5 @@ automatically, with zero telemetry and no internet permission. [#10]: https://codeberg.org/jlmakiola/calendula/issues/10 [#12]: https://codeberg.org/jlmakiola/calendula/issues/12 [#13]: https://codeberg.org/jlmakiola/calendula/issues/13 +[#14]: https://codeberg.org/jlmakiola/calendula/issues/14 +[#16]: https://codeberg.org/jlmakiola/calendula/issues/16 -- 2.49.1 From 5d887524b14aea5fc07cda0cce038a6430923e51 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 00:43:56 +0200 Subject: [PATCH 06/49] fix(reminders): suppress notifications for disabled calendars Reminders fired for every due CalendarAlerts row regardless of whether the event's calendar had been disabled in-app, even though disabled calendars are hidden from every other surface. Carry the calendar id through the alert and filter disabled calendars before posting, while still marking all due alerts fired so the provider stops re-broadcasting the suppressed ones. Closes #17 Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 5 +++++ .../data/reminders/EventReminderReceiver.kt | 10 +++++++++- .../data/reminders/ReminderActionReceiver.kt | 3 +++ .../calendula/data/reminders/ReminderAlertStore.kt | 13 ++++++++----- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61779f7..6d973ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 turn reminders off entirely. Thanks to @moonj for the suggestion ([#14]). ### Fixed +- Disabled calendars no longer notify. Reminders for events in a calendar you + have disabled (Settings → Calendars) are now suppressed instead of still + popping up — matching how a disabled calendar's events already stay hidden + everywhere else in the app ([#17]). - Editing a single occurrence of a recurring event works again. Choosing **Only this event** and saving a change to one event in a repeating series silently did nothing — the change was rejected and the edit form simply reappeared with @@ -749,3 +753,4 @@ automatically, with zero telemetry and no internet permission. [#13]: https://codeberg.org/jlmakiola/calendula/issues/13 [#14]: https://codeberg.org/jlmakiola/calendula/issues/14 [#16]: https://codeberg.org/jlmakiola/calendula/issues/16 +[#17]: https://codeberg.org/jlmakiola/calendula/issues/17 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt index 71af6a3..62078c9 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt @@ -8,6 +8,7 @@ import android.content.pm.PackageManager import android.provider.CalendarContract import androidx.core.content.ContextCompat import dagger.hilt.android.AndroidEntryPoint +import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -32,6 +33,7 @@ class EventReminderReceiver : BroadcastReceiver() { @Inject lateinit var alertStore: ReminderAlertStore @Inject lateinit var notifier: ReminderNotifier @Inject lateinit var settingsPrefs: SettingsPrefs + @Inject lateinit var calendarPrefs: CalendarPrefs override fun onReceive(context: Context, intent: Intent) { if (intent.action != CalendarContract.ACTION_EVENT_REMINDER) return @@ -46,7 +48,13 @@ class EventReminderReceiver : BroadcastReceiver() { if (settingsPrefs.remindersEnabled.first()) { val now = System.currentTimeMillis() val due = alertStore.dueAlerts(now) - due.forEach { notifier.post(it) } + // Suppress reminders for calendars the user disabled in-app + // (mirrors the event filtering in CalendarRepositoryImpl), but + // still mark every due alert fired so the provider stops + // re-broadcasting the suppressed ones. + val disabled = calendarPrefs.disabledCalendarIds.first() + due.filterNot { it.calendarId in disabled } + .forEach { notifier.post(it) } alertStore.markFired(due.map { it.alertId }, now) } } finally { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt index b0a57fc..5a25f8c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderActionReceiver.kt @@ -75,6 +75,7 @@ class ReminderActionReceiver : BroadcastReceiver() { private const val EXTRA_ALERT_ID = "alert_id" private const val EXTRA_EVENT_ID = "event_id" + private const val EXTRA_CALENDAR_ID = "calendar_id" private const val EXTRA_BEGIN = "begin" private const val EXTRA_END = "end" private const val EXTRA_TITLE = "title" @@ -87,6 +88,7 @@ class ReminderActionReceiver : BroadcastReceiver() { this.action = action putExtra(EXTRA_ALERT_ID, alert.alertId) putExtra(EXTRA_EVENT_ID, alert.eventId) + putExtra(EXTRA_CALENDAR_ID, alert.calendarId) putExtra(EXTRA_BEGIN, alert.beginMillis) putExtra(EXTRA_END, alert.endMillis) putExtra(EXTRA_TITLE, alert.title) @@ -113,6 +115,7 @@ class ReminderActionReceiver : BroadcastReceiver() { return ReminderAlert( alertId = intent.getLongExtra(EXTRA_ALERT_ID, 0L), eventId = intent.getLongExtra(EXTRA_EVENT_ID, 0L), + calendarId = intent.getLongExtra(EXTRA_CALENDAR_ID, 0L), beginMillis = intent.getLongExtra(EXTRA_BEGIN, 0L), endMillis = intent.getLongExtra(EXTRA_END, 0L), title = intent.getStringExtra(EXTRA_TITLE).orEmpty(), diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlertStore.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlertStore.kt index 5d14579..54f68dc 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlertStore.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderAlertStore.kt @@ -16,6 +16,7 @@ import javax.inject.Singleton data class ReminderAlert( val alertId: Long, val eventId: Long, + val calendarId: Long, val beginMillis: Long, val endMillis: Long, /** Raw event title; may be blank — the notifier substitutes "(no title)". */ @@ -66,11 +67,12 @@ class AndroidReminderAlertStore @Inject constructor( ReminderAlert( alertId = c.getLong(0), eventId = c.getLong(1), - beginMillis = c.getLong(2), - endMillis = c.getLong(3), - title = c.getString(4).orEmpty(), - location = c.getString(5)?.takeIf { it.isNotBlank() }, - isAllDay = c.getInt(6) == 1, + calendarId = c.getLong(2), + beginMillis = c.getLong(3), + endMillis = c.getLong(4), + title = c.getString(5).orEmpty(), + location = c.getString(6)?.takeIf { it.isNotBlank() }, + isAllDay = c.getInt(7) == 1, ), ) } @@ -102,6 +104,7 @@ class AndroidReminderAlertStore @Inject constructor( val PROJECTION = arrayOf( CalendarContract.CalendarAlerts._ID, CalendarContract.CalendarAlerts.EVENT_ID, + CalendarContract.CalendarAlerts.CALENDAR_ID, CalendarContract.CalendarAlerts.BEGIN, CalendarContract.CalendarAlerts.END, CalendarContract.CalendarAlerts.TITLE, -- 2.49.1 From 225f4c34912150c673d03572c5fb8a9a9e2be6ff Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 00:46:20 +0200 Subject: [PATCH 07/49] test(reminders): cover disabled-calendar alert filtering Extract the disabled-calendar filtering from EventReminderReceiver into a pure postableAlerts() function and unit-test it, since the receiver flow itself is not reachable from a plain JUnit test. Co-Authored-By: Claude Opus 4.8 --- .../data/reminders/EventReminderReceiver.kt | 21 +++++-- .../data/reminders/PostableAlertsTest.kt | 55 +++++++++++++++++++ 2 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt index 62078c9..dc6871f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt @@ -17,6 +17,17 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject +/** + * The due alerts that should actually surface as notifications: everything + * except alerts whose calendar the user has disabled in-app (mirroring the + * event filtering in CalendarRepositoryImpl). The caller still marks the full + * due set fired, so suppressed alerts are not re-broadcast by the provider. + */ +internal fun postableAlerts( + due: List, + disabledCalendarIds: Set, +): List = due.filterNot { it.calendarId in disabledCalendarIds } + /** * Becomes the app that turns the calendar provider's reminder alarms into * visible notifications (the Etar model — the provider broadcasts @@ -48,13 +59,11 @@ class EventReminderReceiver : BroadcastReceiver() { if (settingsPrefs.remindersEnabled.first()) { val now = System.currentTimeMillis() val due = alertStore.dueAlerts(now) - // Suppress reminders for calendars the user disabled in-app - // (mirrors the event filtering in CalendarRepositoryImpl), but - // still mark every due alert fired so the provider stops - // re-broadcasting the suppressed ones. val disabled = calendarPrefs.disabledCalendarIds.first() - due.filterNot { it.calendarId in disabled } - .forEach { notifier.post(it) } + // Suppress reminders for disabled calendars, but still mark + // every due alert fired so the provider stops re-broadcasting + // the suppressed ones. + postableAlerts(due, disabled).forEach { notifier.post(it) } alertStore.markFired(due.map { it.alertId }, now) } } finally { diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt new file mode 100644 index 0000000..222281e --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt @@ -0,0 +1,55 @@ +package de.jeanlucmakiola.calendula.data.reminders + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class PostableAlertsTest { + + private fun alert(alertId: Long, calendarId: Long) = ReminderAlert( + alertId = alertId, + eventId = alertId * 10, + calendarId = calendarId, + beginMillis = 0L, + endMillis = 0L, + title = "Event $alertId", + location = null, + isAllDay = false, + ) + + @Test + fun `keeps alerts when no calendar is disabled`() { + val due = listOf(alert(1, calendarId = 100), alert(2, calendarId = 200)) + + val postable = postableAlerts(due, disabledCalendarIds = emptySet()) + + assertThat(postable).isEqualTo(due) + } + + @Test + fun `drops alerts for a disabled calendar`() { + val keep = alert(1, calendarId = 100) + val drop = alert(2, calendarId = 200) + + val postable = postableAlerts(listOf(keep, drop), disabledCalendarIds = setOf(200)) + + assertThat(postable).containsExactly(keep) + } + + @Test + fun `drops every alert when all their calendars are disabled`() { + val due = listOf(alert(1, calendarId = 100), alert(2, calendarId = 100)) + + val postable = postableAlerts(due, disabledCalendarIds = setOf(100)) + + assertThat(postable).isEmpty() + } + + @Test + fun `keeps multiple alerts from the same enabled calendar`() { + val due = listOf(alert(1, calendarId = 100), alert(2, calendarId = 100)) + + val postable = postableAlerts(due, disabledCalendarIds = setOf(999)) + + assertThat(postable).isEqualTo(due) + } +} -- 2.49.1 From 93ca38d974dd7664231ee0dea148d48e5d5764ed Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 10:19:23 +0200 Subject: [PATCH 08/49] feat(contacts): declare READ_CONTACTS + read contact special-dates Codeberg #15, foundation for the contact special-dates calendars. Declares the optional, feature-gated READ_CONTACTS permission (never requested at startup) and adds the offline, read-only contacts reader. - AndroidManifest: READ_CONTACTS with a comment documenting the opt-in/offline one-way-mirror contract. - domain/contacts: SpecialDateType + ContactSpecialDate model and a pure parseContactEventDate covering full (yyyy-MM-dd), year-less (--MM-dd) and compact (yyyyMMdd) shapes, with Feb-29 handling via a leap anchor. - data/contacts: ContactSpecialDatesDataSource querying ContactsContract.Data Event rows, split by TYPE, deduped per (contact, type); returns empty without the permission so sync can degrade to a stalled state. Hilt-bound. - Unit tests for the date parser (full/year-less/compact/Feb-29/malformed). Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src/main/AndroidManifest.xml | 9 ++ .../contacts/ContactSpecialDatesDataSource.kt | 98 ++++++++++++++ .../calendula/data/di/DataModule.kt | 8 ++ .../domain/contacts/ContactSpecialDate.kt | 122 ++++++++++++++++++ .../domain/contacts/ContactSpecialDateTest.kt | 64 +++++++++ 5 files changed, 301 insertions(+) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/ContactSpecialDate.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/ContactSpecialDateTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 29f9c5e..8ad7750 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,15 @@ + + + Birthdays + Anniversaries + Special dates + {name}\'s birthday ({age}) + {name}\'s anniversary ({age}) + {name} diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt index b2832b8..edbff7f 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt @@ -86,6 +86,8 @@ internal class FakeCalendarDataSource : CalendarDataSource { override fun deleteCalendar(id: Long) { writeError?.let { throw it } deletedCalendarIds += id + managedCalendarRows.removeAll { it.id == id } + managedEvents.remove(id) } /** All-day reminder fire-time minute-of-day passed into the last write. */ @@ -147,9 +149,9 @@ internal class FakeCalendarDataSource : CalendarDataSource { // --- Managed special-dates surface (stateful, so a re-sync sees inserts) --- - var findManagedCalendarsResult: List = emptyList() var nextManagedEventId: Long = 1_000L private var managedCalendarSeq: Long = 700L + private val managedCalendarRows = mutableListOf() private val managedEvents = mutableMapOf>() data class CreatedManagedCalendar(val displayName: String, val color: Int, val type: SpecialDateType) @@ -157,15 +159,22 @@ internal class FakeCalendarDataSource : CalendarDataSource { val insertedManagedEvents = mutableListOf>() val updatedManagedFields = mutableListOf>>() + /** Preset an orphan managed calendar (e.g. to test adoption after a prefs wipe). */ + fun presetManagedCalendar(id: Long, type: SpecialDateType) { + managedCalendarRows += ManagedCalendarRow(id, type) + managedEvents.getOrPut(id) { mutableListOf() } + } + override fun createManagedCalendar(displayName: String, color: Int, type: SpecialDateType): Long { writeError?.let { throw it } createdManagedCalendars += CreatedManagedCalendar(displayName, color, type) val id = managedCalendarSeq++ + managedCalendarRows += ManagedCalendarRow(id, type) managedEvents.getOrPut(id) { mutableListOf() } return id } - override fun findManagedCalendars(): List = findManagedCalendarsResult + override fun findManagedCalendars(): List = managedCalendarRows.toList() override fun queryManagedEvents(calendarId: Long): List = managedEvents[calendarId]?.toList() ?: emptyList() diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt new file mode 100644 index 0000000..c24a411 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt @@ -0,0 +1,196 @@ +package de.jeanlucmakiola.calendula.data.contacts + +import android.provider.CalendarContract +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.calendula.data.calendar.FakeCalendarDataSource +import de.jeanlucmakiola.calendula.data.calendar.ManagedEventRow +import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs +import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate +import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +class SpecialDatesSyncEngineTest { + + private val today = LocalDate(2026, 3, 1) + + private class FakeContacts( + var permission: Boolean = true, + var dates: List = emptyList(), + ) : ContactSpecialDatesDataSource { + override fun hasPermission() = permission + override fun readSpecialDates() = dates + } + + private class FakeSpec : SpecialDatesCalendarSpec { + override fun displayName(type: SpecialDateType) = type.name + override fun color(type: SpecialDateType) = 0xFF112233.toInt() + override fun defaultTitleTemplate(type: SpecialDateType) = when (type) { + SpecialDateType.Birthday -> "{name}'s birthday ({age})" + SpecialDateType.Anniversary -> "{name}'s anniversary" + SpecialDateType.Custom -> "{name}" + } + } + + private fun prefs(tempDir: Path): SettingsPrefs { + val store: DataStore = PreferenceDataStoreFactory.create( + produceFile = { tempDir.resolve("settings_test.preferences_pb").toFile() }, + ) + return SettingsPrefs(store) + } + + private fun birthday(key: String, name: String, month: Int, day: Int, year: Int?) = + ContactSpecialDate(key, name, SpecialDateType.Birthday, month, day, year) + + private suspend fun engineWith( + tempDir: Path, + contacts: FakeContacts, + calendars: FakeCalendarDataSource = FakeCalendarDataSource(), + enabled: Boolean = true, + ): Pair { + val settings = prefs(tempDir) + if (enabled) settings.setSpecialDatesEnabled(true) + return SpecialDatesSyncEngine(contacts, calendars, settings, FakeSpec()) to settings + } + + @Test + fun `disabled feature is a no-op`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val (engine, _) = engineWith(tempDir, FakeContacts(), calendars, enabled = false) + assertThat(engine.sync(today)).isEqualTo(SpecialDatesSyncResult.Disabled) + assertThat(calendars.createdManagedCalendars).isEmpty() + } + + @Test + fun `missing permission reports stalled without touching calendars`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val (engine, _) = engineWith(tempDir, FakeContacts(permission = false), calendars) + assertThat(engine.sync(today)).isEqualTo(SpecialDatesSyncResult.PermissionMissing) + assertThat(calendars.createdManagedCalendars).isEmpty() + } + + @Test + fun `first sync creates calendars and inserts one event per contact`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val contacts = FakeContacts( + dates = listOf( + birthday("a", "Jane", 5, 14, 1990), + birthday("b", "Bob", 12, 25, null), + ), + ) + val (engine, settings) = engineWith(tempDir, contacts, calendars) + + assertThat(engine.sync(today)).isEqualTo(SpecialDatesSyncResult.Success) + // All three type calendars exist; the birthday one holds both contacts. + assertThat(calendars.createdManagedCalendars.map { it.type }) + .containsExactlyElementsIn(SpecialDateType.entries) + assertThat(calendars.insertedManagedEvents).hasSize(2) + assertThat(calendars.insertedManagedEvents.map { it.second }) + .containsExactly("contact-birthday:a@calendula", "contact-birthday:b@calendula") + // Year-less contact drops the age; year-known keeps it. + val titles = calendars.insertedManagedEvents.map { it.first.title } + assertThat(titles).contains("Jane's birthday (36)") + assertThat(titles).contains("Bob's birthday") + // Calendar ids were persisted for the editor / next sync. + assertThat(settings.specialDatesCalendars.first()).containsKey(SpecialDateType.Birthday) + } + + @Test + fun `a second sync with no changes writes nothing`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) + val (engine, _) = engineWith(tempDir, contacts, calendars) + + engine.sync(today) + val insertsAfterFirst = calendars.insertedManagedEvents.size + val calendarsAfterFirst = calendars.createdManagedCalendars.size + engine.sync(today) + + assertThat(calendars.insertedManagedEvents).hasSize(insertsAfterFirst) + assertThat(calendars.createdManagedCalendars).hasSize(calendarsAfterFirst) + assertThat(calendars.updatedManagedFields).isEmpty() + assertThat(calendars.deletedEventIds).isEmpty() + } + + @Test + fun `a renamed contact triggers a title-only managed update`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) + val (engine, _) = engineWith(tempDir, contacts, calendars) + engine.sync(today) + + contacts.dates = listOf(birthday("a", "Janet", 5, 14, 1990)) + engine.sync(today) + + assertThat(calendars.updatedManagedFields).hasSize(1) + val (_, columns) = calendars.updatedManagedFields.single() + // Only managed columns are ever written — never reminders/location. + assertThat(columns.keys).containsExactly(CalendarContract.Events.TITLE) + assertThat(columns[CalendarContract.Events.TITLE]).isEqualTo("Janet's birthday (36)") + } + + @Test + fun `a removed contact deletes its event`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) + val (engine, _) = engineWith(tempDir, contacts, calendars) + engine.sync(today) + + contacts.dates = emptyList() + engine.sync(today) + + assertThat(calendars.deletedEventIds).hasSize(1) + } + + @Test + fun `disabling a type deletes its calendar on the next sync`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val (engine, settings) = engineWith(tempDir, FakeContacts(), calendars) + engine.sync(today) + val anniversaryId = settings.specialDatesCalendars.first().getValue(SpecialDateType.Anniversary) + + settings.setSpecialDateTypeEnabled(SpecialDateType.Anniversary, false) + engine.sync(today) + + assertThat(calendars.deletedCalendarIds).contains(anniversaryId) + assertThat(settings.specialDatesCalendars.first()).doesNotContainKey(SpecialDateType.Anniversary) + } + + // --- pure diff --- + + private fun desired(uid: String, title: String, dt: Long, rrule: String? = "FREQ=YEARLY") = + ManagedEventDesired(uid, title, dt, rrule) + + private fun row(id: Long, uid: String, title: String, dt: Long, rrule: String? = "FREQ=YEARLY") = + ManagedEventRow(id, uid, title, dt, rrule) + + @Test + fun `diff inserts new, deletes gone, and leaves unchanged alone`() { + val diff = diffManagedEvents( + desired = listOf(desired("keep", "Jane", 100), desired("new", "Ada", 200)), + existing = listOf(row(1, "keep", "Jane", 100), row(2, "gone", "Old", 300)), + ) + assertThat(diff.insertUids).containsExactly("new") + assertThat(diff.deleteEventIds).containsExactly(2L) + assertThat(diff.updates).isEmpty() + } + + @Test + fun `diff emits only the changed managed columns`() { + val diff = diffManagedEvents( + desired = listOf(desired("a", "Janet", 150)), + existing = listOf(row(9, "a", "Jane", 100)), + ) + val update = diff.updates.single() + assertThat(update.eventId).isEqualTo(9L) + assertThat(update.columns.keys) + .containsExactly(CalendarContract.Events.TITLE, CalendarContract.Events.DTSTART) + } +} diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt new file mode 100644 index 0000000..393ccbe --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt @@ -0,0 +1,69 @@ +package de.jeanlucmakiola.calendula.domain.contacts + +import com.google.common.truth.Truth.assertThat +import kotlinx.datetime.LocalDate +import org.junit.jupiter.api.Test + +class SpecialDateFormattingTest { + + private fun birthday(month: Int, day: Int, year: Int?) = ContactSpecialDate( + lookupKey = "k", + displayName = "Jane", + type = SpecialDateType.Birthday, + month = month, + day = day, + year = year, + ) + + @Test + fun `uid is deterministic and namespaced by type`() { + assertThat(managedEventUid(SpecialDateType.Birthday, "abc")) + .isEqualTo("contact-birthday:abc@calendula") + assertThat(managedEventUid(SpecialDateType.Anniversary, "abc")) + .isEqualTo("contact-anniversary:abc@calendula") + } + + @Test + fun `anchor date uses the known year`() { + assertThat(birthday(5, 14, 1990).anchorDate()).isEqualTo(LocalDate(1990, 5, 14)) + } + + @Test + fun `anchor date falls back to a leap year when year-less`() { + assertThat(birthday(2, 29, null).anchorDate()).isEqualTo(LocalDate(YEARLESS_ANCHOR_YEAR, 2, 29)) + } + + @Test + fun `age is null when the year is unknown`() { + assertThat(birthday(5, 14, null).ageAtNextOccurrence(LocalDate(2026, 1, 1))).isNull() + } + + @Test + fun `age counts the upcoming birthday when it is still ahead this year`() { + // Born 1990-05-14; today 2026-03-01 → next birthday 2026 → turns 36. + assertThat(birthday(5, 14, 1990).ageAtNextOccurrence(LocalDate(2026, 3, 1))).isEqualTo(36) + } + + @Test + fun `age rolls to next year once the birthday has passed`() { + // Born 1990-05-14; today 2026-06-01 → next birthday 2027 → turns 37. + assertThat(birthday(5, 14, 1990).ageAtNextOccurrence(LocalDate(2026, 6, 1))).isEqualTo(37) + } + + @Test + fun `age counts today's birthday as this year`() { + assertThat(birthday(5, 14, 1990).ageAtNextOccurrence(LocalDate(2026, 5, 14))).isEqualTo(36) + } + + @Test + fun `title substitutes name and age`() { + assertThat(renderSpecialDateTitle("{name}'s birthday ({age})", "Jane", 30)) + .isEqualTo("Jane's birthday (30)") + } + + @Test + fun `title drops an empty age parenthesis and tidies spacing`() { + assertThat(renderSpecialDateTitle("{name}'s birthday ({age})", "Jane", null)) + .isEqualTo("Jane's birthday") + } +} -- 2.49.1 From 4540faf2a0d9c4954e08d70e25894e0cbe609a92 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 11:01:29 +0200 Subject: [PATCH 12/49] feat(contacts): schedule + lifecycle for the special-dates mirror Mirrors the auto-backup infrastructure. A daily periodic WorkManager job plus an immediate run on enable/"Sync now" and a debounced foreground run keep the mirror fresh without a ContentObserver. - SpecialDatesScheduler.apply/runNow + SpecialDatesSyncWorker (EntryPoint): respects the toggle, debounces foreground runs (4h), and parks the feature in a PermissionRevoked stalled state (recorded for the settings banner) instead of retrying forever when READ_CONTACTS is gone. - CalendulaApp.onCreate reconciles the schedule on every launch. - RootScreen ON_RESUME enqueues a debounced foreground sync, gated on the READ_CONTACTS grant so opted-out users never enqueue it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../jeanlucmakiola/calendula/CalendulaApp.kt | 20 +++ .../data/contacts/SpecialDatesScheduler.kt | 123 ++++++++++++++++++ .../jeanlucmakiola/calendula/ui/RootScreen.kt | 10 ++ 3 files changed, 153 insertions(+) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt b/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt index ac300d1..4ce0165 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/CalendulaApp.kt @@ -5,6 +5,8 @@ import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.HiltAndroidApp import de.jeanlucmakiola.calendula.data.backup.BackupScheduler import de.jeanlucmakiola.calendula.data.backup.BackupWorker +import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler +import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncWorker import de.jeanlucmakiola.calendula.data.crash.CrashReporter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -25,6 +27,7 @@ class CalendulaApp : Application() { // respecting, on-device; the user submits the report by hand). CrashReporter.install(this) reconcileAutoBackup() + reconcileSpecialDates() } /** @@ -44,4 +47,21 @@ class CalendulaApp : Application() { ) } } + + /** + * Re-arm (or cancel) the daily special-dates reconcile from the saved + * settings on every launch — like [reconcileAutoBackup], this cancels + * orphaned work after the feature is turned off and re-schedules after a + * reinstall. The foreground trigger (RootScreen ON_RESUME) does the on-open + * refresh, so no immediate run is needed here. + */ + private fun reconcileSpecialDates() { + val deps = EntryPointAccessors.fromApplication(this, SpecialDatesSyncWorker.Deps::class.java) + CoroutineScope(SupervisorJob() + Dispatchers.Default).launch { + SpecialDatesScheduler.apply( + context = this@CalendulaApp, + enabled = deps.settingsPrefs().specialDatesEnabled.first(), + ) + } + } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt new file mode 100644 index 0000000..f5237b7 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt @@ -0,0 +1,123 @@ +package de.jeanlucmakiola.calendula.data.contacts + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.Data +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent +import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs +import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason +import kotlinx.coroutines.flow.first +import kotlinx.datetime.TimeZone +import kotlinx.datetime.todayIn +import java.util.concurrent.TimeUnit +import kotlin.time.Clock + +/** + * Schedules the contact special-dates mirror. Birthdays change rarely, so this + * is deliberately cheap: a **daily** periodic reconcile, plus an immediate run + * on enable / "Sync now" and a debounced one when the app is foregrounded. No + * ContentObserver — it would need the process alive and buys almost nothing for + * once-a-year events. Everything stays offline (local contacts → local calendar). + */ +object SpecialDatesScheduler { + + private const val WORK_NAME = "special-dates-sync" + private const val WORK_NAME_NOW = "special-dates-sync-now" + private const val KEY_FOREGROUND = "foreground" + + /** Enqueue (or cancel) the daily periodic reconcile to match [enabled]. */ + fun apply(context: Context, enabled: Boolean) { + val workManager = WorkManager.getInstance(context) + if (!enabled) { + workManager.cancelUniqueWork(WORK_NAME) + workManager.cancelUniqueWork(WORK_NAME_NOW) + return + } + val request = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + // Delay the first periodic run so it never overlaps an immediate run. + .setInitialDelay(1, TimeUnit.DAYS) + .build() + workManager.enqueueUniquePeriodicWork(WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, request) + } + + /** + * Run one reconcile immediately. [foreground] runs are debounced (the app can + * resume often); enable/"Sync now" runs ([foreground] false) always sync. + */ + fun runNow(context: Context, foreground: Boolean = false) { + val request = OneTimeWorkRequestBuilder() + .setInputData(Data.Builder().putBoolean(KEY_FOREGROUND, foreground).build()) + .build() + WorkManager.getInstance(context) + .enqueueUniqueWork(WORK_NAME_NOW, ExistingWorkPolicy.REPLACE, request) + } + + internal const val INPUT_FOREGROUND = KEY_FOREGROUND +} + +/** + * Runs the [SpecialDatesSyncEngine]. Pulls its collaborators through a Hilt + * [EntryPoint] so it works under WorkManager's default worker factory. Records + * the run for the settings status line; a missing permission parks the feature + * in a stalled state (surfaced in settings) rather than retrying forever. + */ +class SpecialDatesSyncWorker( + appContext: Context, + params: WorkerParameters, +) : CoroutineWorker(appContext, params) { + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface Deps { + fun settingsPrefs(): SettingsPrefs + fun syncEngine(): SpecialDatesSyncEngine + } + + override suspend fun doWork(): Result { + val deps = EntryPointAccessors.fromApplication(applicationContext, Deps::class.java) + val prefs = deps.settingsPrefs() + // Respect the toggle even for already-queued work; never revive a + // disabled feature. + if (!prefs.specialDatesEnabled.first()) return Result.success() + + val now = System.currentTimeMillis() + // A foreground resume can fire often — skip if we synced recently. + val foreground = inputData.getBoolean(SpecialDatesScheduler.INPUT_FOREGROUND, false) + if (foreground && now - prefs.specialDatesLastForegroundSync.first() < FOREGROUND_DEBOUNCE_MILLIS) { + return Result.success() + } + + val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) + return try { + val result = deps.syncEngine().sync(today) + val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) { + SpecialDatesStalledReason.PermissionRevoked + } else { + null + } + prefs.recordSpecialDatesRun(now, stalled) + if (foreground) prefs.setSpecialDatesLastForegroundSync(now) + Result.success() + } catch (e: Exception) { + Log.w(TAG, "Special-dates sync failed", e) + Result.retry() + } + } + + companion object { + private const val TAG = "SpecialDatesSync" + + /** Skip a foreground-triggered sync if one ran within this window (4h). */ + private const val FOREGROUND_DEBOUNCE_MILLIS = 4L * 60 * 60 * 1000 + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt index 1ab1f0f..d3da90d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler import de.jeanlucmakiola.calendula.ui.permission.PermissionScreen import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingScreen import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingViewModel @@ -49,6 +50,15 @@ fun RootScreen( hasPermission = ContextCompat.checkSelfPermission( context, Manifest.permission.READ_CALENDAR ) == PackageManager.PERMISSION_GRANTED + // Refresh the contact special-dates mirror on foreground (the + // worker is debounced and no-ops when the feature is off). Gated + // on the permission so users without the opt-in never enqueue it. + if (ContextCompat.checkSelfPermission( + context, Manifest.permission.READ_CONTACTS + ) == PackageManager.PERMISSION_GRANTED + ) { + SpecialDatesScheduler.runNow(context, foreground = true) + } } } lifecycle.addObserver(obs) -- 2.49.1 From 4da9abe05bec11e117493535846fac70b91d2bb0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 11:06:00 +0200 Subject: [PATCH 13/49] feat(contacts): lock managed fields in the event editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When editing an event in a special-dates calendar, the title, all-day/date and recurrence are locked (the sync overwrites them) while reminders, location, notes and colour stay editable — the field-level "managed" contract from the design. - EventEditUiState.isManaged (calendar id ∈ managedCalendarIds while editing), wired through the ViewModel's state combine. - InlineTextField/InlineField gain an `enabled` flag (dims + goes read-only); ScheduleRow dims and disables its date/time taps; the all-day switch and the recurrence card are disabled. - A one-line "Managed by …" hint under the title explains what's editable. The calendar picker needs no change — it's already create-only, and managed events are always edits. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/common/InlineTextField.kt | 11 +++-- .../calendula/ui/edit/EventEditScreen.kt | 44 +++++++++++++++---- .../calendula/ui/edit/EventEditUiState.kt | 6 +++ .../calendula/ui/edit/EventEditViewModel.kt | 5 ++- app/src/main/res/values/strings.xml | 2 + 5 files changed, 54 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt index 6a541f1..5bf2f56 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/InlineTextField.kt @@ -36,6 +36,7 @@ fun InlineTextField( textStyle: TextStyle = MaterialTheme.typography.titleMedium, singleLine: Boolean = true, minLines: Int = 1, + enabled: Boolean = true, keyboardType: KeyboardType = KeyboardType.Text, capitalization: KeyboardCapitalization = KeyboardCapitalization.None, imeAction: ImeAction = ImeAction.Default, @@ -43,15 +44,17 @@ fun InlineTextField( onImeAction: (() -> Unit)? = null, ) { val resolvedStyle = textStyle.copy( - color = if (textStyle.color.isSpecified) { - textStyle.color - } else { - MaterialTheme.colorScheme.onSurface + color = when { + // A disabled field reads as dimmed, like a locked value. + !enabled -> MaterialTheme.colorScheme.onSurfaceVariant + textStyle.color.isSpecified -> textStyle.color + else -> MaterialTheme.colorScheme.onSurface }, ) BasicTextField( value = value, onValueChange = onValueChange, + enabled = enabled, textStyle = resolvedStyle, singleLine = singleLine, minLines = minLines, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 166c0fd..2fd6830 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -435,6 +435,10 @@ private fun EventEditContent( val form = state.form val locale = currentLocale() val dark = isSystemInDarkTheme() + // The title, date and recurrence are managed by the special-dates sync, so + // they're locked here; everything else (reminders, location, notes) is the + // user's to edit. + val locked = state.isManaged var picker by remember { mutableStateOf(null) } var showCalendarPicker by rememberSaveable { mutableStateOf(false) } var showReminderPicker by rememberSaveable { mutableStateOf(false) } @@ -519,6 +523,7 @@ private fun EventEditContent( placeholder = stringResource(R.string.event_edit_title_hint), textStyle = MaterialTheme.typography.headlineMedium .copy(fontWeight = FontWeight.SemiBold), + enabled = !locked, modifier = Modifier .fillMaxWidth() .padding(vertical = 4.dp) @@ -531,6 +536,17 @@ private fun EventEditContent( .height(3.dp) .background(accent, RoundedCornerShape(2.dp)), ) + if (locked) { + Spacer(Modifier.height(10.dp)) + Text( + text = stringResource( + R.string.event_edit_managed_hint, + selectedCalendar?.displayName.orEmpty(), + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } Spacer(Modifier.height(20.dp)) @@ -549,7 +565,11 @@ private fun EventEditContent( style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f), ) - Switch(checked = form.isAllDay, onCheckedChange = viewModel::setAllDay) + Switch( + checked = form.isAllDay, + onCheckedChange = viewModel::setAllDay, + enabled = !locked, + ) } }, ) { @@ -561,6 +581,7 @@ private fun EventEditContent( locale = locale, onDateClick = { picker = PickerTarget.StartDate }, onTimeClick = { picker = PickerTarget.StartTime }, + enabled = !locked, ) ScheduleRow( label = stringResource(R.string.event_edit_ends), @@ -570,6 +591,7 @@ private fun EventEditContent( onDateClick = { picker = PickerTarget.EndDate }, onTimeClick = { picker = PickerTarget.EndTime }, isError = EventFormProblem.EndBeforeStart in state.problems, + enabled = !locked, ) if (EventFormProblem.EndBeforeStart in state.problems) { Spacer(Modifier.height(2.dp)) @@ -700,7 +722,7 @@ private fun EventEditContent( EditCard( icon = Icons.Default.Repeat, iconContentDescription = null, - onClick = { showRecurrencePicker = true }, + onClick = if (locked) null else ({ showRecurrencePicker = true }), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -1858,6 +1880,7 @@ private fun InlineField( textStyle: TextStyle = MaterialTheme.typography.titleMedium, singleLine: Boolean = true, minLines: Int = 1, + enabled: Boolean = true, keyboardType: KeyboardType = KeyboardType.Text, modifier: Modifier = Modifier .fillMaxWidth() @@ -1871,6 +1894,7 @@ private fun InlineField( textStyle = textStyle, singleLine = singleLine, minLines = minLines, + enabled = enabled, keyboardType = keyboardType, ) } @@ -1885,6 +1909,7 @@ private fun ScheduleRow( onDateClick: () -> Unit, onTimeClick: () -> Unit, isError: Boolean = false, + enabled: Boolean = true, ) { val dateFormat = remember(locale) { DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) @@ -1894,11 +1919,12 @@ private fun ScheduleRow( timeOfDayFormatter(use24Hour, locale) } // Tappable values read as links (primary), like the location on the - // detail screen; errors flip them to the error colour. - val valueColor = if (isError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary + // detail screen; errors flip them to the error colour; a locked (managed) + // row dims to a plain, non-tappable value. + val valueColor = when { + !enabled -> MaterialTheme.colorScheme.onSurfaceVariant + isError -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.primary } Row( verticalAlignment = Alignment.CenterVertically, @@ -1915,7 +1941,7 @@ private fun ScheduleRow( style = MaterialTheme.typography.titleMedium, color = valueColor, modifier = Modifier - .clickable(onClick = onDateClick) + .clickable(enabled = enabled, onClick = onDateClick) .padding(vertical = 8.dp, horizontal = 6.dp), ) if (!isAllDay) { @@ -1924,7 +1950,7 @@ private fun ScheduleRow( style = MaterialTheme.typography.titleMedium, color = valueColor, modifier = Modifier - .clickable(onClick = onTimeClick) + .clickable(enabled = enabled, onClick = onTimeClick) .padding(vertical = 8.dp, horizontal = 6.dp), ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt index 04b063d..934b792 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditUiState.kt @@ -29,6 +29,12 @@ data class EventEditUiState( val hiddenFields: List = emptyList(), /** True while editing an existing event (the calendar is then fixed). */ val isEditing: Boolean = false, + /** + * True when the event's calendar is a contact special-dates calendar the app + * manages. While editing such an event the title, date and recurrence are + * locked (sync overwrites them); reminders, location and notes stay editable. + */ + val isManaged: Boolean = false, /** * Whether to focus the title and raise the keyboard when this is a fresh * create form (issue #10). Mirrors the settings flag; the screen only acts diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt index 7190beb..673501a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt @@ -165,7 +165,8 @@ class EventEditViewModel @Inject constructor( ::ExternalInputs, ).flowOn(io), colorPalette, - ) { local, external, palette -> + settingsPrefs.managedCalendarIds, + ) { local, external, palette, managedIds -> val form = local.form ?: return@combine null val resolvedId = form.calendarId ?: external.lastUsed?.takeIf { id -> external.writable.any { it.id == id } } @@ -180,6 +181,8 @@ class EventEditViewModel @Inject constructor( visibleFields = visibleFields, hiddenFields = (EventFormField.entries.toSet() - visibleFields).sorted(), isEditing = local.editTarget != null, + // Editing an event in a managed calendar locks its synced fields. + isManaged = local.editTarget != null && resolvedId != null && resolvedId in managedIds, autofocusTitle = external.autofocusTitle, // A modified-occurrence exception can't carry its own rule, so // the scope dialog drops "only this event" after a rule change. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46446bd..e1218ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,6 +68,8 @@ Close Save Add title + + Managed by “%1$s” — the title, date and repeat are kept in sync from your contacts. Reminders, location and notes are yours to edit. Starts Ends Ends before it starts -- 2.49.1 From 22bbfef3e2cf78f2ce7d55a151e39f1cce1b88d3 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 11:12:08 +0200 Subject: [PATCH 14/49] feat(contacts): settings sub-page for contact special dates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dedicated sub-page under Settings drives the mirror: - master enable toggle — requests READ_CONTACTS contextually (only here, never at startup), runs an immediate sync on enable, and on disable confirms then tears the managed calendars down; - per-type toggles (Birthdays / Anniversaries / Other), each confirm-guarded since turning one off deletes its calendar; - an editable title template ({name}/{age}) and a Show-age toggle; - a "Sync now" row with the last-run time, a paused/permission banner with a re-grant button, and a pointer to per-calendar colour/visibility/reminders. Exposed via a separate SpecialDatesUiState flow (the main settings combine is already at capacity); the ViewModel gained the engine/scheduler wiring. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/settings/SettingsScreen.kt | 269 +++++++++++++++++- .../calendula/ui/settings/SettingsUiState.kt | 18 ++ .../ui/settings/SettingsViewModel.kt | 82 ++++++ app/src/main/res/values/strings.xml | 27 ++ 4 files changed, 395 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index cbc5048..92220c5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Cake import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Dashboard import androidx.compose.material.icons.filled.Check @@ -91,6 +92,7 @@ import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref import de.jeanlucmakiola.calendula.domain.EventFormField +import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.qs.NewEventTileService import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker @@ -121,7 +123,7 @@ import java.time.format.TextStyle as JavaTextStyle import java.util.Calendar /** The settings sub-screens reached from the hub's category rows. */ -private enum class SettingsSection { Appearance, EventForm, Notifications } +private enum class SettingsSection { Appearance, EventForm, Notifications, SpecialDates } /** * Token-based accent for a leading icon chip (container / on-container pair). @@ -180,6 +182,13 @@ fun SettingsScreen( ) { NotificationsScreen(state = state, viewModel = viewModel, onBack = { section = null }) } + AnimatedVisibility( + visible = section == SettingsSection.SpecialDates, + enter = slideInHorizontally(slideSpec) { it } + fadeIn(), + exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), + ) { + SpecialDatesScreen(viewModel = viewModel, onBack = { section = null }) + } } } @@ -225,6 +234,13 @@ private fun SettingsHub( leading = { CategoryIcon(Icons.Default.CalendarMonth, ChipAccent.Tertiary) }, onClick = onManageCalendars, ) + GroupedRow( + title = stringResource(R.string.settings_section_special_dates), + summary = stringResource(R.string.settings_special_dates_subtitle), + position = Position.Middle, + leading = { CategoryIcon(Icons.Default.Cake, ChipAccent.Tertiary) }, + onClick = { onOpenSection(SettingsSection.SpecialDates) }, + ) LanguageRow(position = Position.Middle) // One-tap add of the "New event" Quick Settings tile. The system prompt // is API 33+; on older versions the tile is still addable manually from @@ -1025,6 +1041,257 @@ private fun NotificationsScreen( } } +// --------------------------------------------------------------------------- +// Contact special dates (issue #15) +// --------------------------------------------------------------------------- + +@Composable +private fun SpecialDatesScreen( + viewModel: SettingsViewModel, + onBack: () -> Unit, +) { + val state by viewModel.specialDatesState.collectAsStateWithLifecycle() + val context = LocalContext.current + + // READ_CONTACTS is requested only here, on enable — never at startup. On a + // grant we either enable (if turning on) or just re-sync (clearing a stalled + // banner after the permission was re-granted). + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { granted -> + if (granted) { + if (!state.enabled) viewModel.setSpecialDatesEnabled(true) else viewModel.syncSpecialDatesNow() + } + } + val requestOrEnable: () -> Unit = { + if (hasContactsPermission(context)) { + viewModel.setSpecialDatesEnabled(true) + } else { + permissionLauncher.launch(Manifest.permission.READ_CONTACTS) + } + } + + var confirmDisableAll by remember { mutableStateOf(false) } + var confirmDisableType by remember { mutableStateOf(null) } + var editTemplate by remember { mutableStateOf(null) } + + CollapsingScaffold( + title = stringResource(R.string.settings_section_special_dates), + onBack = onBack, + ) { + // Paused banner: the permission was revoked after enabling. + if (state.enabled && state.stalledPermission) { + Surface( + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.errorContainer, + modifier = Modifier.fillMaxWidth(), + ) { + Column(Modifier.padding(16.dp)) { + Text( + text = stringResource(R.string.settings_special_dates_paused_title), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + Text( + text = stringResource(R.string.settings_special_dates_paused_hint), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + ) + Spacer(Modifier.height(8.dp)) + FilledTonalButton( + onClick = { permissionLauncher.launch(Manifest.permission.READ_CONTACTS) }, + ) { Text(stringResource(R.string.settings_special_dates_grant)) } + } + } + Spacer(Modifier.height(16.dp)) + } + + GroupedRow( + title = stringResource(R.string.settings_special_dates_enable), + summary = stringResource(R.string.settings_special_dates_enable_hint), + position = Position.Alone, + trailing = { + Switch( + checked = state.enabled, + onCheckedChange = { want -> if (want) requestOrEnable() else confirmDisableAll = true }, + ) + }, + onClick = { if (state.enabled) confirmDisableAll = true else requestOrEnable() }, + ) + + if (state.enabled) { + Spacer(Modifier.height(24.dp)) + // Per-type toggles + their title templates. + SpecialDateType.entries.forEachIndexed { index, type -> + val on = type in state.types + GroupedRow( + title = stringResource(specialDateTypeLabel(type)), + position = if (index == 0) Position.Top else Position.Middle, + trailing = { + Switch( + checked = on, + onCheckedChange = { want -> + if (want) viewModel.setSpecialDateTypeEnabled(type, true) + else confirmDisableType = type + }, + ) + }, + onClick = { + if (on) confirmDisableType = type else viewModel.setSpecialDateTypeEnabled(type, true) + }, + ) + } + GroupedRow( + title = stringResource(R.string.settings_special_dates_template), + summary = state.titleTemplates[SpecialDateType.Birthday].orEmpty(), + position = Position.Bottom, + onClick = { editTemplate = SpecialDateType.Birthday }, + ) + + Spacer(Modifier.height(24.dp)) + GroupedRow( + title = stringResource(R.string.settings_special_dates_show_age), + summary = stringResource(R.string.settings_special_dates_show_age_hint), + position = Position.Top, + trailing = { + Switch( + checked = state.showAge, + onCheckedChange = viewModel::setSpecialDatesShowAge, + ) + }, + onClick = { viewModel.setSpecialDatesShowAge(!state.showAge) }, + ) + GroupedRow( + title = stringResource(R.string.settings_special_dates_sync_now), + summary = specialDatesLastRunLabel(context, state.lastRun), + position = Position.Bottom, + onClick = viewModel::syncSpecialDatesNow, + ) + + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(R.string.settings_special_dates_calendar_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + + if (confirmDisableAll) { + SpecialDatesDisableDialog( + message = stringResource(R.string.settings_special_dates_disable_all_message), + onConfirm = { viewModel.setSpecialDatesEnabled(false); confirmDisableAll = false }, + onDismiss = { confirmDisableAll = false }, + ) + } + confirmDisableType?.let { type -> + SpecialDatesDisableDialog( + message = stringResource( + R.string.settings_special_dates_disable_type_message, + stringResource(specialDateTypeLabel(type)), + ), + onConfirm = { viewModel.setSpecialDateTypeEnabled(type, false); confirmDisableType = null }, + onDismiss = { confirmDisableType = null }, + ) + } + editTemplate?.let { type -> + SpecialDatesTemplateDialog( + initial = state.titleTemplates[type].orEmpty(), + onConfirm = { viewModel.setSpecialDatesTitleTemplate(type, it); editTemplate = null }, + onDismiss = { editTemplate = null }, + ) + } +} + +@Composable +private fun SpecialDatesDisableDialog( + message: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + androidx.compose.material3.AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.settings_special_dates_disable_title)) }, + text = { Text(message) }, + confirmButton = { + androidx.compose.material3.TextButton(onClick = onConfirm) { + Text(stringResource(R.string.settings_special_dates_disable_confirm)) + } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = onDismiss) { + Text(stringResource(R.string.dialog_cancel)) + } + }, + ) +} + +@Composable +private fun SpecialDatesTemplateDialog( + initial: String, + onConfirm: (String) -> Unit, + onDismiss: () -> Unit, +) { + var text by rememberSaveable { mutableStateOf(initial) } + androidx.compose.material3.AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.settings_special_dates_template)) }, + text = { + Column { + androidx.compose.material3.OutlinedTextField( + value = text, + onValueChange = { text = it }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.settings_special_dates_template_hint), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + }, + confirmButton = { + androidx.compose.material3.TextButton( + onClick = { onConfirm(text) }, + enabled = text.isNotBlank(), + ) { Text(stringResource(R.string.dialog_save)) } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = onDismiss) { + Text(stringResource(R.string.dialog_cancel)) + } + }, + ) +} + +private fun specialDateTypeLabel(type: SpecialDateType): Int = when (type) { + SpecialDateType.Birthday -> R.string.settings_special_dates_type_birthday + SpecialDateType.Anniversary -> R.string.settings_special_dates_type_anniversary + SpecialDateType.Custom -> R.string.settings_special_dates_type_custom +} + +private fun hasContactsPermission(context: Context): Boolean = + ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == + PackageManager.PERMISSION_GRANTED + +@Composable +private fun specialDatesLastRunLabel(context: Context, lastRun: Long): String = + if (lastRun <= 0L) { + stringResource(R.string.settings_special_dates_never_synced) + } else { + stringResource( + R.string.settings_special_dates_last_synced, + android.text.format.DateUtils.getRelativeTimeSpanString( + lastRun, + System.currentTimeMillis(), + android.text.format.DateUtils.MINUTE_IN_MILLIS, + ).toString(), + ) + } + /** Which calendar + event kind a per-calendar reminder-override dialog targets. */ private data class OverrideTarget(val calendarId: Long, val isAllDay: Boolean) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index 4ad49a7..ad153ce 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -7,6 +7,7 @@ import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventFormField +import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.common.CalendarView @@ -70,3 +71,20 @@ data class SettingsUiState( */ val allowColorOnUnsupportedCalendars: Boolean = false, ) + +/** + * State for the contact special-dates sub-page (issue #15). Exposed as its own + * flow rather than folded into [SettingsUiState] (whose combine is already full). + * [titleTemplates] is always populated — a blank stored template resolves to the + * localized default so the editor never shows an empty field. + */ +data class SpecialDatesUiState( + val enabled: Boolean = false, + val types: Set = SpecialDateType.entries.toSet(), + val titleTemplates: Map = emptyMap(), + val showAge: Boolean = true, + /** True when READ_CONTACTS was revoked after enabling — the mirror is paused. */ + val stalledPermission: Boolean = false, + /** Epoch millis of the last sync, or 0 if it has never run. */ + val lastRun: Long = 0L, +) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index ae33454..9e80d2e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -10,14 +10,22 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository +import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesCalendarSpec +import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler +import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncEngine +import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs +import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventFormField +import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.storageValue import de.jeanlucmakiola.calendula.ui.common.CalendarView @@ -41,6 +49,9 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val prefs: SettingsPrefs, repository: CalendarRepository, + private val specialDatesEngine: SpecialDatesSyncEngine, + specialDatesSpec: SpecialDatesCalendarSpec, + @IoDispatcher private val io: CoroutineDispatcher, @ApplicationContext private val appContext: Context, ) : ViewModel() { @@ -166,6 +177,77 @@ class SettingsViewModel @Inject constructor( val dimCompletedEvents: Boolean, ) + /** Contact special-dates sub-page (issue #15); its own flow — the main state is full. */ + val specialDatesState: StateFlow = combine( + prefs.specialDatesEnabled, + prefs.specialDatesTypes, + prefs.specialDatesTitleTemplates, + prefs.specialDatesShowAge, + prefs.specialDatesStatus, + ) { enabled, types, templates, showAge, status -> + SpecialDatesUiState( + enabled = enabled, + types = types, + // A blank stored template resolves to the localized default so the + // editor always shows something sensible. + titleTemplates = SpecialDateType.entries.associateWith { type -> + templates[type]?.takeIf { it.isNotBlank() } + ?: specialDatesSpec.defaultTitleTemplate(type) + }, + showAge = showAge, + stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked, + lastRun = status.lastRun, + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = SpecialDatesUiState(), + ) + + /** + * Turn the mirror on or off. Enabling schedules the daily job and runs an + * immediate sync (which creates the calendars); disabling cancels the job and + * tears the managed calendars down. The caller must have READ_CONTACTS granted + * before enabling. + */ + fun setSpecialDatesEnabled(enabled: Boolean) { + viewModelScope.launch { + prefs.setSpecialDatesEnabled(enabled) + if (enabled) { + SpecialDatesScheduler.apply(appContext, enabled = true) + SpecialDatesScheduler.runNow(appContext) + } else { + SpecialDatesScheduler.apply(appContext, enabled = false) + withContext(io) { specialDatesEngine.teardown() } + } + } + } + + fun setSpecialDateTypeEnabled(type: SpecialDateType, enabled: Boolean) { + viewModelScope.launch { + prefs.setSpecialDateTypeEnabled(type, enabled) + // Reconcile now: create the new type's calendar, or delete a disabled one. + SpecialDatesScheduler.runNow(appContext) + } + } + + fun setSpecialDatesTitleTemplate(type: SpecialDateType, template: String) { + viewModelScope.launch { + prefs.setSpecialDatesTitleTemplate(type, template.trim()) + SpecialDatesScheduler.runNow(appContext) + } + } + + fun setSpecialDatesShowAge(enabled: Boolean) { + viewModelScope.launch { + prefs.setSpecialDatesShowAge(enabled) + SpecialDatesScheduler.runNow(appContext) + } + } + + /** Kick off an immediate reconcile (the "Sync now" action). */ + fun syncSpecialDatesNow() = SpecialDatesScheduler.runNow(appContext) + fun setThemeMode(mode: ThemeMode) { viewModelScope.launch { prefs.setThemeMode(mode) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1218ea..48b6856 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,6 +346,33 @@ Theme, default view, week start Default fields for new events Event reminders + Contact birthdays & anniversaries + + + Contact special dates + Show contact dates + Mirror your contacts\' birthdays and other dates into local calendars. Reads contacts on this device only — nothing is uploaded, and your contacts are never changed. + Birthdays + Anniversaries + Other dates + Title format + Use {name} for the contact and {age} for their age (hidden when the year is unknown). + Show age + Include {age} in titles when the birth year is known + Sync now + Not synced yet + + Last synced %1$s + Set each calendar\'s colour, visibility and reminders in Calendars settings. + Paused + Calendula can no longer read your contacts, so these calendars aren\'t updating. + Grant access + Turn off contact dates? + This deletes the contact-date calendars and their events. Any reminders or notes you added to them will be lost. + + This deletes the “%1$s” calendar and its events. Any reminders or notes you added to them will be lost. + Turn off + Save About License MIT -- 2.49.1 From e67960d3527b5f8f496135b4eea875cfaaa3cef2 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 11:16:11 +0200 Subject: [PATCH 15/49] docs(contacts): translations, store copy + changelog for special dates - German translations for the special-dates sub-page, the managed-field editor hint, and the calendar/title strings. - Store listing (en/de): a paragraph on the optional, on-device, one-way contact special-dates feature. - Soften the calendar-permission rationale ("all it asks for up front") now that an optional contacts permission exists, without weakening the privacy promise. - CHANGELOG entry for #15. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 12 +++++++ app/src/main/res/values-de/strings.xml | 34 ++++++++++++++++++- app/src/main/res/values/strings.xml | 2 +- .../android/de-DE/full_description.txt | 6 ++++ .../android/en-US/full_description.txt | 5 +++ 5 files changed, 57 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d973ab..32f6971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- See your contacts' birthdays and anniversaries in your calendar. A new, + **optional** feature (Settings → Contact special dates) mirrors your contacts' + birthdays, anniversaries and other dates into local "Birthdays", + "Anniversaries" and "Other dates" calendars that stay in sync as your contacts + change. Each is a normal local calendar, so you set its colour, visibility and + reminders the usual way — new birthdays even start with a reminder a week before + *and* on the day. The title format is yours to customise (`{name}`, `{age}`). + This is the first feature to use the contacts permission: it is requested only + when you turn the feature on, everything stays on your device (Calendula has no + internet access), and your contacts are only ever read, never changed. Thanks + to @moonj for the suggestion ([#15]). - Set more than one default reminder per calendar. A calendar's default reminders — and the global defaults under Settings → Notifications — can now hold several lead times instead of just one, so new events can start with, say, @@ -752,5 +763,6 @@ automatically, with zero telemetry and no internet permission. [#12]: https://codeberg.org/jlmakiola/calendula/issues/12 [#13]: https://codeberg.org/jlmakiola/calendula/issues/13 [#14]: https://codeberg.org/jlmakiola/calendula/issues/14 +[#15]: https://codeberg.org/jlmakiola/calendula/issues/15 [#16]: https://codeberg.org/jlmakiola/calendula/issues/16 [#17]: https://codeberg.org/jlmakiola/calendula/issues/17 diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d4f6945..9d00942 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -13,7 +13,7 @@ Alle Termine, schön im Blick - Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App nie. + Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App zu Beginn nicht — und nichts verlässt je dein Gerät. Kalender-Zugriff erlauben Kalender-Zugriff abgelehnt Ohne Kalender-Zugriff kann Calendula keine Termine anzeigen. Du kannst den Zugriff in den System-Einstellungen wieder erlauben. @@ -410,4 +410,36 @@ Der Issue-Tracker konnte nicht geöffnet werden. Der Bericht ist in deiner Zwischenablage. 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 _(Der Bericht war zu lang für diesen Link — füge ihn aus deiner Zwischenablage hier ein.)_ + + + Geburtstage + Jahrestage + Besondere Tage + {name}s Geburtstag ({age}) + {name}s Jahrestag ({age}) + {name} + Verwaltet von „%1$s“ — Titel, Datum und Wiederholung werden aus deinen Kontakten synchron gehalten. Erinnerungen, Ort und Notizen kannst du selbst bearbeiten. + Geburtstage & Jahrestage aus Kontakten + Besondere Kontakttage + Kontakttage anzeigen + Spiegelt Geburtstage und weitere Daten deiner Kontakte in lokale Kalender. Liest Kontakte nur auf diesem Gerät — nichts wird hochgeladen, und deine Kontakte werden nie verändert. + Geburtstage + Jahrestage + Weitere Tage + Titelformat + Verwende {name} für den Kontakt und {age} für das Alter (ausgeblendet, wenn das Jahr unbekannt ist). + Alter anzeigen + {age} in Titeln anzeigen, wenn das Geburtsjahr bekannt ist + Jetzt synchronisieren + Noch nicht synchronisiert + Zuletzt synchronisiert %1$s + Farbe, Sichtbarkeit und Erinnerungen jedes Kalenders legst du in den Kalender-Einstellungen fest. + Pausiert + Calendula kann deine Kontakte nicht mehr lesen, daher werden diese Kalender nicht aktualisiert. + Zugriff erlauben + Kontakttage deaktivieren? + Dadurch werden die Kontakttage-Kalender und ihre Termine gelöscht. Hinzugefügte Erinnerungen oder Notizen gehen verloren. + Dadurch werden der Kalender „%1$s“ und seine Termine gelöscht. Hinzugefügte Erinnerungen oder Notizen gehen verloren. + Deaktivieren + Speichern diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48b6856..1a93edd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ See all your events, beautifully - Calendula needs access to your calendar to show and manage your events. That\'s the only thing it ever asks for. + Calendula needs access to your calendar to show and manage your events. That\'s all it asks for up front — and nothing ever leaves your device. Grant calendar access Calendar access denied Calendula cannot show events without calendar access. You can grant it again in the system settings. diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index 5055210..96ec0e3 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -10,6 +10,12 @@ Serie) und einem einfachen Wiederholungs-Picker. Erinnerungen stellt Calendula selbst als Benachrichtigung zu — ein Tipp darauf öffnet den Termin. +Optional lassen sich Geburtstage, Jahrestage und weitere besondere Tage +deiner Kontakte im Kalender anzeigen. Du schaltest das selbst ein; Kontakte +werden nur auf deinem Gerät gelesen, nie hochgeladen und nie verändert — +Calendula spiegelt die Daten lediglich in lokale Kalender, die du voll +kontrollierst. + Der Unterschied liegt im Design: echtes Material 3 Expressive durchgehend, mit Dynamic Color, expressiven Animationen und neuen Shape-Sprachen. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index f59f242..2ebf536 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -8,6 +8,11 @@ changes (only this event / this and all following / the whole series) and a simple repeat picker. Calendula also delivers your event reminders as notifications — tap one and you're on the event. +Optionally, show your contacts' birthdays, anniversaries and other special +dates in your calendar. You turn this on yourself; contacts are read only on +your device, are never uploaded, and are never changed — Calendula only +mirrors the dates into local calendars you fully control. + The differentiator is the design: real Material 3 Expressive throughout, with dynamic color, expressive motion, and expressive shapes. -- 2.49.1 From 030c3e6ce10e226bf06c5065a1c9272a28b847b7 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 11:17:23 +0200 Subject: [PATCH 16/49] docs(design): mark contact special-dates implemented + note deviations Records the UID_2445 (vs SYNC_DATA1) identity decision, the sync-time age snapshot, and the seeded reminder default; checks off the #14/#15 task lists. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/design/contact-special-dates.md | 57 ++++++++++++++++++---------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/design/contact-special-dates.md b/docs/design/contact-special-dates.md index 65bebb1..6af36a4 100644 --- a/docs/design/contact-special-dates.md +++ b/docs/design/contact-special-dates.md @@ -1,9 +1,25 @@ # Design: Contact special-dates calendars (+ per-calendar multiple reminders) -Status: **design / not yet implemented** -Date: 2026-06-30 +Status: **implemented** on `feat/contact-special-dates` (#14 prerequisite merged in) +Date: 2026-06-30 (implemented 2026-07-01) Tracking: Codeberg #15 (feature), Codeberg #14 (prerequisite) +Implementation notes / deviations from this design: + +- **Event identity key is a deterministic `Events.UID_2445`** + (`contact-:@calendula`), not `SYNC_DATA1`. On a LOCAL calendar + the provider drops `_sync` columns unless written through a sync-adapter URI; + `UID_2445` is written/read through the normal event URIs the app already uses + (and doubles as a stable iCal UID). The managed **calendar** still carries a + `CAL_SYNC2` marker for re-adoption (calendar-row writes already use the + sync-adapter URI). +- **Age is a sync-time snapshot.** A `FREQ=YEARLY` row has one static title, so + `{age}` reflects the upcoming birthday at the last sync (refreshed daily / on + foreground), not a per-occurrence value. +- New managed calendars seed a per-calendar all-day reminder default of + on-the-day + one week before, so birthdays get lead time out of the box. +- Title template is user-editable from day one (`{name}`, `{age}`). + This document captures the full design for surfacing contact birthdays — and, by extension, anniversaries and custom dates — as auto-updating local calendars in Calendula. It also specifies #14 (per-calendar *multiple* @@ -206,24 +222,25 @@ rest. ## Task outline ### #14 — per-calendar multiple default reminders (`feat/per-calendar-multi-reminders`) -- [ ] Widen global + per-calendar reminder defaults `Int` → `List` +- [x] Widen global + per-calendar reminder defaults `Int` → `List` (`SettingsPrefs.kt`), keep `Inherit/None/Minutes(list)`. -- [ ] `resolveDefaultReminder()` returns a list. -- [ ] `ReminderDefaultPicker` → multi-select (mirror event-form UI). -- [ ] Seed new events' `EventForm.reminders` from the resolved list. -- [ ] Migration / parse compatibility for existing single-value stored prefs. -- [ ] Tests: resolution, parse round-trip, all-day variant. +- [x] `resolveDefaultReminder()` returns a list. +- [x] `ReminderDefaultPicker` → multi-select (mirror event-form UI). +- [x] Seed new events' `EventForm.reminders` from the resolved list. +- [x] Migration / parse compatibility for existing single-value stored prefs. +- [x] Tests: resolution, parse round-trip, all-day variant. ### #15 — contact special-dates (`feat/contact-special-dates`) -- [ ] Declare `READ_CONTACTS`; feature-gated contextual request on enable. -- [ ] Per-type local calendars (create on enable, delete on disable+confirm). -- [ ] Contacts reader: `Event` rows by `TYPE`, year-less handling. -- [ ] Sync engine: idempotent diff keyed on `LOOKUP_KEY` in `SYNC_DATA1`; - targeted managed-column updates; seed user-owned fields on insert only. -- [ ] `FREQ=YEARLY` event generation + age (when year known). -- [ ] Auto-update: on enable, on foreground, daily WorkManager. -- [ ] Editor: disable managed fields for managed-calendar events. -- [ ] Revoked-permission detection + stalled-state surface. -- [ ] Settings sub-page: enable, per-type toggles, title template, show-age. -- [ ] Translatable strings (titles, sub-page). -- [ ] About / store copy for the optional `READ_CONTACTS` permission. +- [x] Declare `READ_CONTACTS`; feature-gated contextual request on enable. +- [x] Per-type local calendars (create on enable, delete on disable+confirm). +- [x] Contacts reader: `Event` rows by `TYPE`, year-less handling. +- [x] Sync engine: idempotent diff keyed on the deterministic `UID_2445` + (see notes; not `SYNC_DATA1`); targeted managed-column updates; seed + user-owned fields on insert only. +- [x] `FREQ=YEARLY` event generation + age (when year known; sync-time snapshot). +- [x] Auto-update: on enable, on foreground, daily WorkManager. +- [x] Editor: disable managed fields for managed-calendar events. +- [x] Revoked-permission detection + stalled-state surface. +- [x] Settings sub-page: enable, per-type toggles, title template, show-age. +- [x] Translatable strings (titles, sub-page). +- [x] About / store copy for the optional `READ_CONTACTS` permission. -- 2.49.1 From 0fd579b19b41819d99c685e7af66fb508c904c09 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 11:51:42 +0200 Subject: [PATCH 17/49] feat(contacts): drop {age} from default titles, disclaim it when added A FREQ=YEARLY event has one static title, so {age} is only a sync-time snapshot and can look wrong on far-future occurrences. Keep it out of the default templates ({name}'s birthday / anniversary), and when a user does add {age} in the title-format editor, show a short disclaimer about the snapshot behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/settings/SettingsScreen.kt | 10 ++++++++++ app/src/main/res/values-de/strings.xml | 5 +++-- app/src/main/res/values/strings.xml | 5 +++-- docs/design/contact-special-dates.md | 4 +++- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 92220c5..9e096fd 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -1251,6 +1251,16 @@ private fun SpecialDatesTemplateDialog( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) + // Only warn about {age}'s snapshot behaviour once the user has + // actually put it in the template. + if (text.contains("{age}")) { + Spacer(Modifier.height(8.dp)) + Text( + text = stringResource(R.string.settings_special_dates_template_age_note), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.tertiary, + ) + } } }, confirmButton = { diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9d00942..d8246bb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -415,8 +415,8 @@ Geburtstage Jahrestage Besondere Tage - {name}s Geburtstag ({age}) - {name}s Jahrestag ({age}) + {name}s Geburtstag + {name}s Jahrestag {name} Verwaltet von „%1$s“ — Titel, Datum und Wiederholung werden aus deinen Kontakten synchron gehalten. Erinnerungen, Ort und Notizen kannst du selbst bearbeiten. Geburtstage & Jahrestage aus Kontakten @@ -428,6 +428,7 @@ Weitere Tage Titelformat Verwende {name} für den Kontakt und {age} für das Alter (ausgeblendet, wenn das Jahr unbekannt ist). + Hinweis: {age} ist das Alter zum nächsten Termin beim letzten Sync. Da ein jährlicher Termin nur einen Titel hat, kann es bei weit entfernten Terminen unpassend wirken. Alter anzeigen {age} in Titeln anzeigen, wenn das Geburtsjahr bekannt ist Jetzt synchronisieren diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a93edd..b4c8f0c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,6 +357,7 @@ Other dates Title format Use {name} for the contact and {age} for their age (hidden when the year is unknown). + Note: {age} is the age at the next date as of the last sync. Because a yearly event has one title, it can look off on occurrences far in the future. Show age Include {age} in titles when the birth year is known Sync now @@ -497,7 +498,7 @@ Birthdays Anniversaries Special dates - {name}\'s birthday ({age}) - {name}\'s anniversary ({age}) + {name}\'s birthday + {name}\'s anniversary {name} diff --git a/docs/design/contact-special-dates.md b/docs/design/contact-special-dates.md index 6af36a4..2b27c88 100644 --- a/docs/design/contact-special-dates.md +++ b/docs/design/contact-special-dates.md @@ -15,7 +15,9 @@ Implementation notes / deviations from this design: sync-adapter URI). - **Age is a sync-time snapshot.** A `FREQ=YEARLY` row has one static title, so `{age}` reflects the upcoming birthday at the last sync (refreshed daily / on - foreground), not a per-occurrence value. + foreground), not a per-occurrence value. Because of this, `{age}` is **kept out + of the default templates**; users can add it, and the title-format editor shows + a disclaimer once they do. - New managed calendars seed a per-calendar all-day reminder default of on-the-day + one week before, so birthdays get lead time out of the box. - Title template is user-editable from day one (`{name}`, `{age}`). -- 2.49.1 From 14b19cd902651773b28781c71a398ef11a34f13e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:02:14 +0200 Subject: [PATCH 18/49] fix(widget): keep Glance ActionCallback classes under R8 (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The month widget's prev/next/today controls and the agenda widget's refresh run through Glance `actionRunCallback()`, which persists the callback's class name into the click PendingIntent and instantiates it reflectively when tapped. Under R8 full mode (AGP 9 default) those callback classes — only ever referenced reflectively — were renamed and lost their no-arg constructor, so the lookup failed silently and the arrows did nothing on release builds (while actionStartActivity taps, which need no reflection, kept working). Keep every ActionCallback's name and constructor. Verified against the release mapping: ShiftMonthAction/ResetMonthAction/RefreshAgendaAction now retain their original fully-qualified names. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/proguard-rules.pro | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index fd9c2c8..8b1a5d3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -16,6 +16,18 @@ -keep class * extends androidx.room.RoomDatabase { *; } -dontwarn androidx.room.paging.** +# Glance runs an @Composable's `actionRunCallback()` by persisting the +# callback's fully-qualified class name into the click PendingIntent, then +# reflectively instantiating it (Class.forName(name).newInstance()) when the tap +# fires. Under R8 full mode (AGP 9 default) these ActionCallback classes — only +# ever referenced reflectively — get renamed or have their no-arg constructor +# stripped, so the lookup fails silently and the tap does nothing. In the month +# and agenda widgets that broke every run-callback control (the prev/next/today +# month arrows and the agenda refresh) in release builds while actionStartActivity +# taps, which ride a PendingIntent and need no reflection, kept working. Keep +# every ActionCallback's name and constructor intact. +-keep class * implements androidx.glance.appwidget.action.ActionCallback { (...); } + # WorkManager instantiates an InputMerger reflectively (Class.newInstance) from # the fully-qualified class name persisted in the WorkSpec, so the class must # keep both its name and a no-arg constructor. Glance renders every widget -- 2.49.1 From 40d70d9f2b8c149149e816b97bb2e9010cd4f445 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:02:14 +0200 Subject: [PATCH 19/49] feat(widget): open the app from the widget headers (#18, #20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping a widget header now opens the app on a sensible top-level view, via a shared WidgetNavRequest.OpenView(view?) — a concrete view roots there over the default home, a null view resolves to the default home: - Month widget: the month/year title opens the month view (#18). Its "today" button now snaps the grid back to the current month in place (the reset that used to sit on the title), so paging + jump-to-today are both reachable and the title is free to open the app. - Agenda widget: the "Upcoming" title opens the default view (#20), so users whose home view is Week/Month can reach it in one tap instead of drilling through a day or event. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 13 ++++++++++ .../jeanlucmakiola/calendula/MainActivity.kt | 24 +++++++++++++++++++ .../calendula/ui/CalendarHost.kt | 11 +++++++++ .../calendula/ui/WidgetNavRequest.kt | 9 +++++++ .../calendula/widget/agenda/AgendaWidget.kt | 8 ++++++- .../calendula/widget/month/MonthWidget.kt | 15 +++++++----- 6 files changed, 73 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d973ab..5e20ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 a reminder a week before *and* one on the day. The reminder pickers are now multi-select; per-calendar overrides can still inherit the global default or turn reminders off entirely. Thanks to @moonj for the suggestion ([#14]). +- Widget headers now open the app. Tapping the month/year title on the month + widget opens the app on the month view, and tapping the "Upcoming" title on + the agenda widget opens it on your default view — so there's a one-tap way + back into the app that lands where you'd expect, instead of only through a day + or event. The month widget's "today" button now snaps the grid back to the + current month in place. Thanks to @rgz46vic and @ptab for the suggestions + ([#18], [#20]). ### Fixed +- Month widget arrows and "today" button work again. On release builds the + prev/next-month arrows and the jump-to-today control on the month widget did + nothing when tapped — code shrinking had stripped the tap handlers behind + them. They respond again. Thanks to @rgz46vic for the report ([#18]). - Disabled calendars no longer notify. Reminders for events in a calendar you have disabled (Settings → Calendars) are now suppressed instead of still popping up — matching how a disabled calendar's events already stay hidden @@ -754,3 +765,5 @@ automatically, with zero telemetry and no internet permission. [#14]: https://codeberg.org/jlmakiola/calendula/issues/14 [#16]: https://codeberg.org/jlmakiola/calendula/issues/16 [#17]: https://codeberg.org/jlmakiola/calendula/issues/17 +[#18]: https://codeberg.org/jlmakiola/calendula/issues/18 +[#20]: https://codeberg.org/jlmakiola/calendula/issues/20 diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index ede492a..b74872a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -193,6 +193,12 @@ class MainActivity : AppCompatActivity() { // An external date tap (launcher/clock) has no widget source, so it opens // the day view rooted over the default home view (OpenDate source = null). calendarTimeDateOrNull()?.let { return WidgetNavRequest.OpenDate(it.toString(), source = null) } + // A widget header tap: open a top-level view with no date drill-in. The + // empty string carried by [openViewIntent] means "the default home view". + if (hasExtra(EXTRA_OPEN_VIEW)) { + val name = getStringExtra(EXTRA_OPEN_VIEW).orEmpty() + return WidgetNavRequest.OpenView(CalendarView.entries.firstOrNull { it.name == name }) + } val source = sourceViewOrNull() val eventId = getLongExtra(EXTRA_EVENT_ID, -1L) return when { @@ -244,6 +250,10 @@ class MainActivity : AppCompatActivity() { private const val EXTRA_DATE_ISO = "de.jeanlucmakiola.calendula.extra.DATE_ISO" private const val EXTRA_CREATE = "de.jeanlucmakiola.calendula.extra.CREATE" + // A widget header tap asking to open a top-level view (no date drill-in). + // Its value is the target [CalendarView] name, or "" for the default view. + private const val EXTRA_OPEN_VIEW = "de.jeanlucmakiola.calendula.extra.OPEN_VIEW" + // The [CalendarView] (by name) of the widget a launch came from. Roots the // in-app back stack in that view; absent for non-widget launches (reminders). private const val EXTRA_SOURCE_VIEW = "de.jeanlucmakiola.calendula.extra.SOURCE_VIEW" @@ -307,5 +317,19 @@ class MainActivity : AppCompatActivity() { putExtra(EXTRA_DATE_ISO, date.toString()) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } + + /** + * Open the app on a top-level [view] with no date drill-in — a widget + * header tap. A null [view] opens the user's default home view (the agenda + * widget's "Upcoming" title); a concrete view roots there over the default + * home (the month widget's month/year title → [CalendarView.Month]). The + * per-view data URI keeps distinct headers' PendingIntents from collapsing. + */ + fun openViewIntent(context: Context, view: CalendarView?): Intent = + Intent(context, MainActivity::class.java).apply { + data = "calendula://view/${view?.name ?: "default"}".toUri() + putExtra(EXTRA_OPEN_VIEW, view?.name ?: "") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 50ee186..dc2cc4f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -212,6 +212,17 @@ fun CalendarHost( detailKey = key onWidgetNavConsumed() } + is WidgetNavRequest.OpenView -> { + // A widget header tap: land on a top-level view with no date + // drill-in. Reveal it by dropping any covering overlay, then root + // the stack on the target (null → the default home) over the + // default home — so backing out returns to the default, then exits. + dismissCoveringOverlays() + createDateIso = null + pendingDayIso = null + viewStack = viewBaseStack(defaultView, req.view ?: defaultView) + onWidgetNavConsumed() + } is WidgetNavRequest.Create -> { // External "new event" entries (QS tile / launcher shortcut / // widget) must land on top of whatever is open — the form overlay diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt index 6588d10..985806b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/WidgetNavRequest.kt @@ -31,4 +31,13 @@ sealed interface WidgetNavRequest { /** Open the create-event form prefilled for [dateIso] (today when null). */ data class Create(val dateIso: String?) : WidgetNavRequest + + /** + * Open the app rooted on a top-level [view] with no date drill-in — a widget + * header tap. A null [view] means "the user's default home view" (the agenda + * widget's "Upcoming" title, issue #20); a concrete view roots there over the + * default home (the month widget's month/year title → [CalendarView.Month], + * issue #18), so backing out returns to the default view, then exits. + */ + data class OpenView(val view: CalendarView?) : WidgetNavRequest } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index 19c06c5..0174eed 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -193,6 +193,8 @@ private fun AgendaHeader() { modifier = GlanceModifier.fillMaxWidth().padding(horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { + // Tap the "Upcoming" title to open the app on the user's default view + // (issue #20) — a null target resolves to the default home in the host. Text( text = context.getString(R.string.widget_agenda_title), style = TextStyle( @@ -200,7 +202,11 @@ private fun AgendaHeader() { fontSize = 16.sp, fontWeight = FontWeight.Medium, ), - modifier = GlanceModifier.defaultWeight(), + modifier = GlanceModifier + .defaultWeight() + .clickable( + actionStartActivity(MainActivity.openViewIntent(context, view = null)), + ), ) IconButton( resId = R.drawable.ic_widget_refresh, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index 1799bd5..d08e65b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -194,6 +194,8 @@ private fun MonthHeader(label: String) { ), ), ) + // Tap the month/year title to open the app on the month view (issue #18), + // rooted over the default home so backing out returns there. Text( text = label, style = TextStyle( @@ -204,16 +206,17 @@ private fun MonthHeader(label: String) { ), modifier = GlanceModifier .defaultWeight() - .clickable(actionRunCallback()), + .clickable( + actionStartActivity( + MainActivity.openViewIntent(context, CalendarView.Month), + ), + ), ) + // The "today" button snaps the grid back to the current month in place. HeaderIcon( resId = R.drawable.ic_widget_today, contentDescription = context.getString(R.string.widget_today), - onClick = GlanceModifier.clickable( - actionStartActivity( - MainActivity.openDateIntent(context, today(systemZone()), CalendarView.Month), - ), - ), + onClick = GlanceModifier.clickable(actionRunCallback()), ) HeaderIcon( resId = R.drawable.ic_widget_chevron_right, -- 2.49.1 From b9e800e9fc452a6826eddbbba0cc8e952785d736 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:19:50 +0200 Subject: [PATCH 20/49] feat(views): customizable quick-switch cycle + drawer order (#24) Add a "Views" settings section that lets users pick which views the top-bar quick-switch button cycles through and drag to reorder them, plus an independent drag-to-reorder for the navigation drawer's view list. Two separate configs: a view turned off in the quick-switch cycle is still reachable from the drawer, which always lists every view. - QuickSwitchConfig (order + enabled set) and a drawer order persisted in SettingsPrefs (comma-joined enum names; "!" marks a disabled view). Missing views append enabled and unknown names drop, so a future view defaults into both lists. - The pill cycles through the configured enabled views in order; the drawer renders CalendarDrawer from the drawer order. Both threaded from CalendarHost via CalendarHostViewModel. - New ReorderableColumn: dependency-free, measurement-driven drag reordering for the short grouped-card settings lists (no LazyColumn, since settings are a single verticalScroll column). Commits one write per gesture. - The switch needs two targets, so the last two enabled views can't be turned off. Co-Authored-By: Claude Opus 4.8 --- .../calendula/data/prefs/SettingsPrefs.kt | 66 ++++++++ .../calendula/ui/CalendarHost.kt | 13 ++ .../calendula/ui/CalendarHostViewModel.kt | 19 +++ .../calendula/ui/agenda/AgendaScreen.kt | 6 +- .../calendula/ui/common/CalendarDrawer.kt | 5 +- .../calendula/ui/common/CalendarView.kt | 30 +++- .../calendula/ui/common/ReorderableColumn.kt | 103 +++++++++++++ .../calendula/ui/day/DayScreen.kt | 6 +- .../calendula/ui/month/MonthScreen.kt | 6 +- .../calendula/ui/settings/SettingsScreen.kt | 145 +++++++++++++++++- .../calendula/ui/settings/SettingsUiState.kt | 6 + .../ui/settings/SettingsViewModel.kt | 27 +++- .../calendula/ui/week/WeekScreen.kt | 6 +- app/src/main/res/values/strings.xml | 7 + .../calendula/data/prefs/SettingsPrefsTest.kt | 75 +++++++++ .../calendula/ui/common/ViewBackStackTest.kt | 29 ++++ 16 files changed, 539 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 1a1f1d5..a6a6ee6 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -12,6 +12,8 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange import de.jeanlucmakiola.calendula.ui.agenda.storageValue import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS +import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.datetime.DayOfWeek @@ -202,6 +204,34 @@ class SettingsPrefs @Inject constructor( store.edit { it[DEFAULT_VIEW_KEY] = view.name } } + /** + * Quick-switch button customisation (#24): the ordered full view list plus + * which views are enabled in the cycle. Stored comma-joined by enum name, a + * "!" prefix marking a disabled view (e.g. "Month,Week,!Day,Agenda"). Missing + * views are appended enabled and unknown names dropped, so a future view + * defaults into the cycle. An absent key means [QuickSwitchConfig.Default]. + */ + val quickSwitchConfig: Flow = store.data.map { prefs -> + parseQuickSwitch(prefs[QUICK_SWITCH_VIEWS_KEY]) + } + + suspend fun setQuickSwitchConfig(config: QuickSwitchConfig) { + store.edit { it[QUICK_SWITCH_VIEWS_KEY] = serializeQuickSwitch(config) } + } + + /** + * Navigation-drawer view order (#24). Comma-joined enum names; missing views + * are appended in default order and unknown names dropped. Absent key means + * [IMPLEMENTED_VIEWS] (the historical fixed order). + */ + val drawerViewOrder: Flow> = store.data.map { prefs -> + parseViewOrder(prefs[DRAWER_VIEW_ORDER_KEY]) + } + + suspend fun setDrawerViewOrder(order: List) { + store.edit { it[DRAWER_VIEW_ORDER_KEY] = order.joinToString(",") { view -> view.name } } + } + /** * Optional event-form fields shown by default (the rest hide behind * "more fields"). Stored comma-joined by enum name: an absent key means @@ -433,6 +463,40 @@ class SettingsPrefs @Inject constructor( .toSet() } + /** Parse a plain comma-joined view order, completed to every implemented view. */ + private fun parseViewOrder(stored: String?): List = + completeViewOrder( + stored?.split(',').orEmpty() + .mapNotNull { name -> CalendarView.entries.firstOrNull { it.name == name.trim() } }, + ) + + /** Parse the quick-switch config; "!"-prefixed names are disabled. */ + private fun parseQuickSwitch(stored: String?): QuickSwitchConfig { + if (stored == null) return QuickSwitchConfig.Default + val parsed = stored.split(',').mapNotNull { raw -> + val token = raw.trim() + val disabled = token.startsWith("!") + val name = if (disabled) token.drop(1) else token + CalendarView.entries.firstOrNull { it.name == name }?.let { it to disabled } + } + val order = completeViewOrder(parsed.map { it.first }) + // Only views explicitly stored disabled are excluded; anything appended + // (a view added in a later release) defaults into the cycle. + val disabled = parsed.filter { it.second }.map { it.first }.toSet() + return QuickSwitchConfig(order, order.filterNot { it in disabled }.toSet()) + } + + private fun serializeQuickSwitch(config: QuickSwitchConfig): String = + completeViewOrder(config.order).joinToString(",") { view -> + if (view in config.enabled) view.name else "!${view.name}" + } + + /** Keep the given order (de-duplicated), then append any views it omits. */ + private fun completeViewOrder(seen: List): List { + val ordered = seen.distinct() + return ordered + IMPLEMENTED_VIEWS.filterNot { it in ordered } + } + companion object { internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode") internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color") @@ -445,6 +509,8 @@ class SettingsPrefs @Inject constructor( internal val PAST_EVENT_DISPLAY_KEY = stringPreferencesKey("agenda_past_event_display") internal val DIM_COMPLETED_EVENTS_KEY = booleanPreferencesKey("dim_completed_events") internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view") + internal val QUICK_SWITCH_VIEWS_KEY = stringPreferencesKey("quick_switch_views") + internal val DRAWER_VIEW_ORDER_KEY = stringPreferencesKey("drawer_view_order") internal val FORM_FIELDS_KEY = stringPreferencesKey("event_form_default_fields") internal val AUTOFOCUS_EVENT_TITLE_KEY = booleanPreferencesKey("autofocus_event_title") internal val REMINDERS_ENABLED_KEY = booleanPreferencesKey("reminders_enabled") diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 50ee186..6cddce2 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -79,6 +79,11 @@ fun CalendarHost( // correcting it. Brief blank first frame, matching the onboarding gate above. val defaultView = viewModel.defaultView.collectAsStateWithLifecycle().value ?: return + // View customisation (#24): the quick-switch cycle and drawer order. Both have + // sensible non-empty initial values, so they're ready before the first frame. + val quickSwitchViews = viewModel.quickSwitchViews.collectAsStateWithLifecycle().value + val drawerViewOrder = viewModel.drawerViewOrder.collectAsStateWithLifecycle().value + var viewStack by rememberSaveable(stateSaver = viewStackSaver) { mutableStateOf(listOf(defaultView)) } @@ -260,6 +265,8 @@ fun CalendarHost( onOpenSettings = onOpenSettings, onOpenSearch = onOpenSearch, onCreateEvent = onCreateEvent, + quickSwitchViews = quickSwitchViews, + drawerViewOrder = drawerViewOrder, ) CalendarView.Day -> DayScreen( selectedView = currentView, @@ -269,6 +276,8 @@ fun CalendarHost( onOpenSearch = onOpenSearch, onCreateEvent = onCreateEvent, initialDateIso = pendingDayIso, + quickSwitchViews = quickSwitchViews, + drawerViewOrder = drawerViewOrder, ) CalendarView.Month -> MonthScreen( selectedView = currentView, @@ -277,6 +286,8 @@ fun CalendarHost( onOpenSettings = onOpenSettings, onOpenSearch = onOpenSearch, onCreateEvent = onCreateEvent, + quickSwitchViews = quickSwitchViews, + drawerViewOrder = drawerViewOrder, ) CalendarView.Agenda -> AgendaScreen( selectedView = currentView, @@ -285,6 +296,8 @@ fun CalendarHost( onOpenSettings = onOpenSettings, onOpenSearch = onOpenSearch, onCreateEvent = onCreateEvent, + quickSwitchViews = quickSwitchViews, + drawerViewOrder = drawerViewOrder, ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt index 9a9c2b7..773f76d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt @@ -5,8 +5,10 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import javax.inject.Inject @@ -26,4 +28,21 @@ class CalendarHostViewModel @Inject constructor( started = SharingStarted.WhileSubscribed(5_000L), initialValue = null, ) + + /** Views the top-bar quick-switch pill cycles through, in the user's order (#24). */ + val quickSwitchViews: StateFlow> = prefs.quickSwitchConfig + .map { it.cycle } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = IMPLEMENTED_VIEWS, + ) + + /** Order of the views in the navigation drawer (#24); every view always shown. */ + val drawerViewOrder: StateFlow> = prefs.drawerViewOrder + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = IMPLEMENTED_VIEWS, + ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt index 4db125c..8286ee8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt @@ -64,6 +64,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.calendula.ui.common.GroupedRow import de.jeanlucmakiola.calendula.ui.common.Position @@ -96,6 +97,8 @@ fun AgendaScreen( onOpenSettings: () -> Unit, onOpenSearch: () -> Unit, onCreateEvent: (LocalDate, Int?) -> Unit, + quickSwitchViews: List = IMPLEMENTED_VIEWS, + drawerViewOrder: List = IMPLEMENTED_VIEWS, modifier: Modifier = Modifier, viewModel: AgendaViewModel = hiltViewModel(), ) { @@ -120,6 +123,7 @@ fun AgendaScreen( CalendarDrawer( currentView = selectedView, currentDate = anchor, + viewOrder = drawerViewOrder, onSelectView = { view -> onSelectView(view) scope.launch { drawerState.close() } @@ -140,7 +144,7 @@ fun AgendaScreen( topBar = { AgendaTopBar( selectedView = selectedView, - onCycleView = { onSelectView(selectedView.next()) }, + onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, scrollBehavior = scrollBehavior, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarDrawer.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarDrawer.kt index 2b66b98..37b2c29 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarDrawer.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarDrawer.kt @@ -59,6 +59,7 @@ fun CalendarDrawer( onSelectView: (CalendarView) -> Unit, onJumpToDate: (LocalDate) -> Unit, onSettings: () -> Unit, + viewOrder: List = IMPLEMENTED_VIEWS, ) { var showDatePicker by remember { mutableStateOf(false) } @@ -73,10 +74,10 @@ fun CalendarDrawer( DrawerHeader() DrawerSectionHeader(stringResource(R.string.view_section)) - IMPLEMENTED_VIEWS.forEachIndexed { index, view -> + viewOrder.forEachIndexed { index, view -> GroupedRow( title = stringResource(view.labelRes), - position = positionOf(index, IMPLEMENTED_VIEWS.size), + position = positionOf(index, viewOrder.size), selected = view == currentView, minHeight = 56.dp, leading = { Icon(view.icon, contentDescription = null) }, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt index 57151ab..b701d95 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt @@ -45,11 +45,39 @@ val IMPLEMENTED_VIEWS: List = /** Next view in [available], wrapping around. Falls back to Month if absent. */ fun CalendarView.next(available: List = IMPLEMENTED_VIEWS): CalendarView { + if (available.isEmpty()) return this val i = available.indexOf(this) - if (i < 0) return available.firstOrNull() ?: CalendarView.Month + if (i < 0) return available.first() return available[(i + 1) % available.size] } +/** + * The user's customisation of the top-bar quick-switch button (#24): which views + * it cycles through ([enabled]) and in what [order]. [order] always lists every + * implemented view — the settings screen reorders the whole set — while [cycle] + * is the subset the pill actually steps through, in [order]. The navigation + * drawer keeps its own separate order and always lists every view, so a view + * disabled here stays reachable there. + */ +data class QuickSwitchConfig( + val order: List, + val enabled: Set, +) { + /** Views the pill steps through, in [order]. */ + val cycle: List get() = order.filter { it in enabled } + + companion object { + /** All views, in default order, all enabled. */ + val Default = QuickSwitchConfig(IMPLEMENTED_VIEWS, IMPLEMENTED_VIEWS.toSet()) + + /** + * Fewest views that keep the switch meaningful — a "switch" needs at + * least two targets, so the settings screen blocks disabling below this. + */ + const val MIN_ENABLED = 2 + } +} + /** * The top-level view back stack (bottom → top): the [default] home view always * sits at the bottom. Pressing back pops one level until only the home view diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt new file mode 100644 index 0000000..369ee3a --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt @@ -0,0 +1,103 @@ +package de.jeanlucmakiola.calendula.ui.common + +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.zIndex + +/** + * A vertical list whose rows can be dragged into a new order by their handle. + * + * Built for the short, fixed grouped-card lists in Settings (#24) — no external + * dependency and no [androidx.compose.foundation.lazy.LazyColumn] (the settings + * screens are a single [androidx.compose.foundation.verticalScroll] column, which + * can't nest a scrolling list). [rowContent] receives the [Position] for the + * row's current place in the order (so it can reuse [GroupedRow]'s card shaping) + * and a `dragHandle` [Modifier] to attach to whatever element should start a + * drag. While a row is held it floats above the others, which reflow live; the + * final order is committed once via [onReorder] on release, so the persistence + * layer sees one write per gesture, not one per crossed row. + * + * Reordering is measurement-driven: each row reports its own height, so rows of + * unequal height still swap at the right point. + */ +@Composable +fun ReorderableColumn( + items: List, + keyOf: (T) -> Any, + onReorder: (List) -> Unit, + modifier: Modifier = Modifier, + rowContent: @Composable (item: T, position: Position, dragHandle: Modifier, isDragging: Boolean) -> Unit, +) { + // Local working copy so rows reflow instantly during a drag; re-seeded when + // the incoming list changes (including the echo of our own committed order). + var order by remember(items) { mutableStateOf(items) } + var draggedKey by remember { mutableStateOf(null) } + // Translation of the held row from its settled slot; a swap folds one slot + // height back into this so the row stays under the finger across the swap. + var dragOffsetY by remember { mutableStateOf(0f) } + val heights = remember { mutableStateMapOf() } + + Column(modifier) { + order.forEachIndexed { index, item -> + val key = keyOf(item) + val isDragging = key == draggedKey + val dragHandle = Modifier.pointerInput(key) { + detectDragGestures( + onDragStart = { + draggedKey = key + dragOffsetY = 0f + }, + onDragEnd = { + draggedKey = null + dragOffsetY = 0f + onReorder(order) + }, + onDragCancel = { + draggedKey = null + dragOffsetY = 0f + }, + onDrag = { change, dragAmount -> + change.consume() + dragOffsetY += dragAmount.y + val cur = order.indexOfFirst { keyOf(it) == key } + if (cur < 0) return@detectDragGestures + // Swap once the row has travelled past half of the + // neighbour it's heading toward, in the drag direction. + if (dragAmount.y > 0 && cur < order.lastIndex) { + val nextHeight = heights[keyOf(order[cur + 1])] ?: 0 + if (nextHeight > 0 && dragOffsetY > nextHeight / 2f) { + order = order.toMutableList().apply { add(cur + 1, removeAt(cur)) } + dragOffsetY -= nextHeight + } + } else if (dragAmount.y < 0 && cur > 0) { + val prevHeight = heights[keyOf(order[cur - 1])] ?: 0 + if (prevHeight > 0 && dragOffsetY < -prevHeight / 2f) { + order = order.toMutableList().apply { add(cur - 1, removeAt(cur)) } + dragOffsetY += prevHeight + } + } + }, + ) + } + Box( + Modifier + .onSizeChanged { heights[key] = it.height } + .zIndex(if (isDragging) 1f else 0f) + .graphicsLayer { translationY = if (isDragging) dragOffsetY else 0f }, + ) { + rowContent(item, positionOf(index, order.size), dragHandle, isDragging) + } + } + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt index 97cbdce..ce43538 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/day/DayScreen.kt @@ -72,6 +72,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.NowLine import de.jeanlucmakiola.calendula.ui.common.ViewSwitcherPill import de.jeanlucmakiola.calendula.ui.common.calendarSlideTransition @@ -116,6 +117,8 @@ fun DayScreen( onOpenSettings: () -> Unit, onOpenSearch: () -> Unit, onCreateEvent: (LocalDate, Int?) -> Unit, + quickSwitchViews: List = IMPLEMENTED_VIEWS, + drawerViewOrder: List = IMPLEMENTED_VIEWS, modifier: Modifier = Modifier, initialDateIso: String? = null, viewModel: DayViewModel = hiltViewModel(), @@ -175,6 +178,7 @@ fun DayScreen( CalendarDrawer( currentView = selectedView, currentDate = date, + viewOrder = drawerViewOrder, onSelectView = { view -> onSelectView(view) scope.launch { drawerState.close() } @@ -196,7 +200,7 @@ fun DayScreen( DayTopBar( date = date, selectedView = selectedView, - onCycleView = { onSelectView(selectedView.next()) }, + onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, scrollBehavior = scrollBehavior, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index f57f6e3..3413ce7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -68,6 +68,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff import de.jeanlucmakiola.calendula.ui.common.rememberCurrentMinute @@ -98,6 +99,8 @@ fun MonthScreen( onOpenSettings: () -> Unit, onOpenSearch: () -> Unit, onCreateEvent: (LocalDate, Int?) -> Unit, + quickSwitchViews: List = IMPLEMENTED_VIEWS, + drawerViewOrder: List = IMPLEMENTED_VIEWS, modifier: Modifier = Modifier, viewModel: MonthViewModel = hiltViewModel(), ) { @@ -157,6 +160,7 @@ fun MonthScreen( CalendarDrawer( currentView = selectedView, currentDate = LocalDate(month.year, month.month, 1), + viewOrder = drawerViewOrder, onSelectView = { view -> onSelectView(view) scope.launch { drawerState.close() } @@ -178,7 +182,7 @@ fun MonthScreen( MonthTopBar( month = month, selectedView = selectedView, - onCycleView = { onSelectView(selectedView.next()) }, + onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, scrollBehavior = scrollBehavior, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index d3fc450..3c3c97a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -41,6 +41,8 @@ import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.CalendarMonth import androidx.compose.material.icons.filled.Dashboard import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.DragHandle +import androidx.compose.material.icons.filled.SwapVert import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Favorite @@ -99,9 +101,13 @@ import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel +import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold import de.jeanlucmakiola.calendula.ui.common.GroupedRow import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS +import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig +import de.jeanlucmakiola.calendula.ui.common.ReorderableColumn +import de.jeanlucmakiola.calendula.ui.common.icon import de.jeanlucmakiola.calendula.ui.common.labelRes import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip import de.jeanlucmakiola.calendula.ui.common.OptionPicker @@ -121,7 +127,7 @@ import java.time.format.TextStyle as JavaTextStyle import java.util.Calendar /** The settings sub-screens reached from the hub's category rows. */ -private enum class SettingsSection { Appearance, EventForm, Notifications } +private enum class SettingsSection { Appearance, Views, EventForm, Notifications } /** * Token-based accent for a leading icon chip (container / on-container pair). @@ -166,6 +172,13 @@ fun SettingsScreen( ) { AppearanceScreen(state = state, viewModel = viewModel, onBack = { section = null }) } + AnimatedVisibility( + visible = section == SettingsSection.Views, + enter = slideInHorizontally(slideSpec) { it } + fadeIn(), + exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), + ) { + ViewsScreen(state = state, viewModel = viewModel, onBack = { section = null }) + } AnimatedVisibility( visible = section == SettingsSection.EventForm, enter = slideInHorizontally(slideSpec) { it } + fadeIn(), @@ -204,6 +217,13 @@ private fun SettingsHub( leading = { CategoryIcon(Icons.Default.Palette, ChipAccent.Neutral) }, onClick = { onOpenSection(SettingsSection.Appearance) }, ) + GroupedRow( + title = stringResource(R.string.settings_section_views), + summary = stringResource(R.string.settings_views_subtitle), + position = Position.Middle, + leading = { CategoryIcon(Icons.Default.SwapVert, ChipAccent.Neutral) }, + onClick = { onOpenSection(SettingsSection.Views) }, + ) GroupedRow( title = stringResource(R.string.settings_section_event_form), summary = stringResource(R.string.settings_event_form_subtitle), @@ -664,6 +684,129 @@ private fun AppearanceScreen( } } +/** + * Views (#24): reorder the top-bar quick-switch cycle and choose which views it + * steps through, plus reorder the navigation-drawer list. Two independent lists — + * a view disabled in the quick-switch cycle is still reachable from the drawer, + * which always lists every view. The switch needs at least two targets, so the + * last [QuickSwitchConfig.MIN_ENABLED] enabled views can't be turned off. + */ +@Composable +private fun ViewsScreen( + state: SettingsUiState, + viewModel: SettingsViewModel, + onBack: () -> Unit, +) { + CollapsingScaffold( + title = stringResource(R.string.settings_section_views), + onBack = onBack, + ) { + val config = state.quickSwitchConfig + + SectionHeader(stringResource(R.string.settings_quick_switch_header)) + SettingsHint(stringResource(R.string.settings_quick_switch_hint)) + Spacer(Modifier.height(8.dp)) + // Turning a view off is blocked once only the minimum remain enabled. + val canDisable = config.enabled.size > QuickSwitchConfig.MIN_ENABLED + ReorderableColumn( + items = config.order, + keyOf = { it }, + onReorder = { viewModel.setQuickSwitchConfig(config.copy(order = it)) }, + ) { view, position, dragHandle, isDragging -> + val checked = view in config.enabled + ViewRow( + view = view, + position = position, + isDragging = isDragging, + dragHandle = dragHandle, + dimmed = !checked, + trailing = { + Switch( + checked = checked, + // Keep the last two on: with fewer, the pill can't switch. + enabled = !checked || canDisable, + onCheckedChange = { on -> + val enabled = if (on) config.enabled + view else config.enabled - view + viewModel.setQuickSwitchConfig(config.copy(enabled = enabled)) + }, + ) + }, + ) + } + + Spacer(Modifier.height(24.dp)) + SectionHeader(stringResource(R.string.settings_drawer_order_header)) + SettingsHint(stringResource(R.string.settings_drawer_order_hint)) + Spacer(Modifier.height(8.dp)) + ReorderableColumn( + items = state.drawerViewOrder, + keyOf = { it }, + onReorder = { viewModel.setDrawerViewOrder(it) }, + ) { view, position, dragHandle, isDragging -> + ViewRow( + view = view, + position = position, + isDragging = isDragging, + dragHandle = dragHandle, + ) + } + } +} + +/** One reorderable view row: the view's icon and name, an optional [trailing] + * control, and a drag handle carrying the [dragHandle] gesture modifier. */ +@Composable +private fun ViewRow( + view: CalendarView, + position: Position, + isDragging: Boolean, + dragHandle: Modifier, + dimmed: Boolean = false, + trailing: @Composable (() -> Unit)? = null, +) { + GroupedRow( + title = stringResource(view.labelRes), + position = position, + dimmed = dimmed, + minHeight = 64.dp, + container = if (isDragging) MaterialTheme.colorScheme.secondaryContainer else null, + leading = { + Icon( + imageVector = view.icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailing = { + Row(verticalAlignment = Alignment.CenterVertically) { + trailing?.invoke() + if (trailing != null) Spacer(Modifier.width(8.dp)) + Box( + modifier = dragHandle.size(48.dp), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Default.DragHandle, + contentDescription = stringResource(R.string.reorder_drag_handle), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + ) +} + +/** Muted supporting text under a [SectionHeader], matching the form-fields hint. */ +@Composable +private fun SettingsHint(text: String) { + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) +} + @Composable private fun EventFormScreen( state: SettingsUiState, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index 52c23ae..43667f9 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -9,6 +9,8 @@ import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventFormField import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS +import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig /** * Settings screen state (M4). Persisted preferences are instant to read, so @@ -37,6 +39,10 @@ data class SettingsUiState( val agendaShowRangeBar: Boolean = true, /** The calendar view the app opens on, and the home of the view back stack (M1). */ val defaultView: CalendarView = CalendarView.Week, + /** Which views the top-bar quick-switch button cycles through, and their order (#24). */ + val quickSwitchConfig: QuickSwitchConfig = QuickSwitchConfig.Default, + /** Order of the views in the navigation drawer (#24); every view is always listed. */ + val drawerViewOrder: List = IMPLEMENTED_VIEWS, /** Optional event-form fields shown by default (rest behind "more fields"). */ val defaultFormFields: Set = SettingsPrefs.DEFAULT_FORM_FIELDS, /** Whether the new-event form auto-focuses the title and shows the keyboard (#10). */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 557ab21..cc1b263 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -21,6 +21,7 @@ import de.jeanlucmakiola.calendula.domain.EventFormField import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.storageValue import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget @@ -103,8 +104,14 @@ class SettingsViewModel @Inject constructor( prefs.autofocusEventTitle, prefs.pastEventDisplay, prefs.dimCompletedEvents, - ::MiscSettings, - ), + // View customisation (#24) folded into one flow so it fits this + // group — the outer combine is already at its five-arg limit. + combine(prefs.quickSwitchConfig, prefs.drawerViewOrder) { quickSwitch, drawer -> + ViewCustomization(quickSwitch, drawer) + }, + ) { showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization -> + MiscSettings(showRangeBar, autofocus, pastEvents, dimCompleted, viewCustomization) + }, ) { base, defaults, overrides, views, misc -> base.copy( defaultView = views.defaultView, @@ -116,6 +123,8 @@ class SettingsViewModel @Inject constructor( autofocusEventTitle = misc.autofocusEventTitle, pastEventDisplay = misc.pastEventDisplay, dimCompletedEvents = misc.dimCompletedEvents, + quickSwitchConfig = misc.viewCustomization.quickSwitch, + drawerViewOrder = misc.viewCustomization.drawerOrder, allowColorOnUnsupportedCalendars = defaults.allowColor, defaultReminderMinutes = defaults.defaultReminder, defaultAllDayReminderMinutes = defaults.allDayReminder, @@ -164,6 +173,12 @@ class SettingsViewModel @Inject constructor( val autofocusEventTitle: Boolean, val pastEventDisplay: PastEventDisplay, val dimCompletedEvents: Boolean, + val viewCustomization: ViewCustomization, + ) + + private data class ViewCustomization( + val quickSwitch: QuickSwitchConfig, + val drawerOrder: List, ) fun setThemeMode(mode: ThemeMode) { @@ -247,6 +262,14 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { prefs.setFormFieldDefault(field, enabled) } } + fun setQuickSwitchConfig(config: QuickSwitchConfig) { + viewModelScope.launch { prefs.setQuickSwitchConfig(config) } + } + + fun setDrawerViewOrder(order: List) { + viewModelScope.launch { prefs.setDrawerViewOrder(order) } + } + fun setRemindersEnabled(enabled: Boolean) { viewModelScope.launch { prefs.setRemindersEnabled(enabled) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index e91de43..c40ab29 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -81,6 +81,7 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarDrawer import de.jeanlucmakiola.calendula.ui.common.CalendarFabColumn import de.jeanlucmakiola.calendula.ui.common.CalendarFailure import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.EventDimAlpha import de.jeanlucmakiola.calendula.ui.common.LocalDimCutoff import de.jeanlucmakiola.calendula.ui.common.NowLine @@ -131,6 +132,8 @@ fun WeekScreen( onOpenSettings: () -> Unit, onOpenSearch: () -> Unit, onCreateEvent: (LocalDate, Int?) -> Unit, + quickSwitchViews: List = IMPLEMENTED_VIEWS, + drawerViewOrder: List = IMPLEMENTED_VIEWS, modifier: Modifier = Modifier, viewModel: WeekViewModel = hiltViewModel(), ) { @@ -195,6 +198,7 @@ fun WeekScreen( CalendarDrawer( currentView = selectedView, currentDate = weekStart, + viewOrder = drawerViewOrder, onSelectView = { view -> onSelectView(view) scope.launch { drawerState.close() } @@ -216,7 +220,7 @@ fun WeekScreen( WeekTopBar( weekStart = weekStart, selectedView = selectedView, - onCycleView = { onSelectView(selectedView.next()) }, + onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) }, onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, scrollBehavior = scrollBehavior, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d121f90..334e980 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -308,6 +308,12 @@ %d day %d days + Views + Quick-switch button + Choose which views the top-right button cycles through, and drag to reorder them. Turned-off views stay reachable from the navigation menu. + Navigation menu + Drag to reorder the views listed in the navigation menu. + Drag to reorder New event form Fields shown by default — everything else sits behind \"More fields\" Focus title on new event @@ -343,6 +349,7 @@ Add or improve a language on Weblate Theme, default view, week start + Quick-switch button and menu order Default fields for new events Event reminders About diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt index eec1075..ef2f097 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt @@ -6,6 +6,9 @@ import androidx.datastore.preferences.core.Preferences import com.google.common.truth.Truth.assertThat import de.jeanlucmakiola.calendula.domain.EventFormField import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange +import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS +import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.DayOfWeek @@ -149,6 +152,78 @@ class SettingsPrefsTest { assertThat(prefs.defaultFormFields.first()).isEmpty() } + @Test + fun `quick-switch defaults to every view enabled in default order`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + val config = prefs.quickSwitchConfig.first() + assertThat(config.order).containsExactlyElementsIn(IMPLEMENTED_VIEWS).inOrder() + assertThat(config.enabled).containsExactlyElementsIn(IMPLEMENTED_VIEWS) + assertThat(config.cycle).containsExactlyElementsIn(IMPLEMENTED_VIEWS).inOrder() + } + + @Test + fun `quick-switch config round-trips order and disabled views`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + val config = QuickSwitchConfig( + order = listOf(CalendarView.Agenda, CalendarView.Month, CalendarView.Week, CalendarView.Day), + enabled = setOf(CalendarView.Agenda, CalendarView.Month), + ) + prefs.setQuickSwitchConfig(config) + val loaded = prefs.quickSwitchConfig.first() + assertThat(loaded.order).containsExactly( + CalendarView.Agenda, CalendarView.Month, CalendarView.Week, CalendarView.Day, + ).inOrder() + assertThat(loaded.enabled).containsExactly(CalendarView.Agenda, CalendarView.Month) + assertThat(loaded.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder() + } + + @Test + fun `quick-switch parse appends missing views enabled and drops unknowns`(@TempDir tempDir: Path) = runTest { + val store = newDataStore(tempDir) + val prefs = SettingsPrefs(store) + // Only Day (disabled) and Week stored, plus a bogus name; Month & Agenda absent. + store.updateData { p -> + val m = p.toMutablePreferences() + m[SettingsPrefs.QUICK_SWITCH_VIEWS_KEY] = "!Day,Week,Hologram" + m + } + val config = prefs.quickSwitchConfig.first() + // Stored order first (Day, Week), then the omitted views in default order. + assertThat(config.order).containsExactly( + CalendarView.Day, CalendarView.Week, CalendarView.Month, CalendarView.Agenda, + ).inOrder() + // Day was explicitly disabled; the appended Month & Agenda default enabled. + assertThat(config.enabled).containsExactly( + CalendarView.Week, CalendarView.Month, CalendarView.Agenda, + ) + } + + @Test + fun `drawer order defaults to the implemented order and round-trips`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + assertThat(prefs.drawerViewOrder.first()).containsExactlyElementsIn(IMPLEMENTED_VIEWS).inOrder() + prefs.setDrawerViewOrder( + listOf(CalendarView.Agenda, CalendarView.Day, CalendarView.Week, CalendarView.Month), + ) + assertThat(prefs.drawerViewOrder.first()).containsExactly( + CalendarView.Agenda, CalendarView.Day, CalendarView.Week, CalendarView.Month, + ).inOrder() + } + + @Test + fun `drawer order parse appends missing views and drops unknowns`(@TempDir tempDir: Path) = runTest { + val store = newDataStore(tempDir) + val prefs = SettingsPrefs(store) + store.updateData { p -> + val m = p.toMutablePreferences() + m[SettingsPrefs.DRAWER_VIEW_ORDER_KEY] = "Agenda,Nope,Week" + m + } + assertThat(prefs.drawerViewOrder.first()).containsExactly( + CalendarView.Agenda, CalendarView.Week, CalendarView.Month, CalendarView.Day, + ).inOrder() + } + @Test fun `unknown stored form-field names are dropped`(@TempDir tempDir: Path) = runTest { val store = newDataStore(tempDir) diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt index 7c4f118..93d5028 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt @@ -91,4 +91,33 @@ class ViewBackStackTest { assertThat(viewBaseStack(CalendarView.Agenda, CalendarView.Agenda)) .containsExactly(CalendarView.Agenda) } + + @Test + fun `next cycles through the configured views in order, wrapping around`() { + val cycle = listOf(CalendarView.Month, CalendarView.Agenda) + assertThat(CalendarView.Month.next(cycle)).isEqualTo(CalendarView.Agenda) + // Wraps back to the first from the last. + assertThat(CalendarView.Agenda.next(cycle)).isEqualTo(CalendarView.Month) + } + + @Test + fun `next from a view outside the cycle lands on the first enabled view`() { + // Day was disabled in the quick-switch cycle but is still the current view. + val cycle = listOf(CalendarView.Week, CalendarView.Month) + assertThat(CalendarView.Day.next(cycle)).isEqualTo(CalendarView.Week) + } + + @Test + fun `next on an empty cycle stays put`() { + assertThat(CalendarView.Week.next(emptyList())).isEqualTo(CalendarView.Week) + } + + @Test + fun `quick-switch cycle keeps enabled views in their configured order`() { + val config = QuickSwitchConfig( + order = listOf(CalendarView.Agenda, CalendarView.Day, CalendarView.Month, CalendarView.Week), + enabled = setOf(CalendarView.Agenda, CalendarView.Month), + ) + assertThat(config.cycle).containsExactly(CalendarView.Agenda, CalendarView.Month).inOrder() + } } -- 2.49.1 From 580dc5a669bf0cb44dae4429bd23b1102805a874 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:23:33 +0200 Subject: [PATCH 21/49] feat(widget): tap anywhere on a month-widget day to open it (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only the small day-number was a tap target, so tapping the blank area of a day cell (or its "+N" overflow) did nothing. Make every non-event part of a day column open that day — the day number, the empty lane cells, and the overflow row — via a shared openDayAction, so a tap anywhere on a day opens it, matching the in-app month grid. Event bars keep opting out to open their own detail. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/widget/month/MonthWidget.kt | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index d08e65b..89b401b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -304,6 +304,15 @@ private fun WeekRow( } } +/** + * Open [date]'s day view rooted in the month view (so back returns to the month + * grid) — the same target the in-app month grid uses when a day cell is tapped. + * Shared by every tappable part of a day column so, as in the app, a tap anywhere + * on a day opens it; only an event bar on top opts out to open its own detail. + */ +private fun openDayAction(context: Context, date: LocalDate) = + actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month)) + @Composable private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: Dp) { val context = LocalContext.current @@ -311,11 +320,7 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: modifier = GlanceModifier .width(colW) .height(DAY_NUMBER_HEIGHT) - // Tap a day number to open that day, rooted in the month view so back - // returns to the month grid. - .clickable( - actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month)), - ), + .clickable(openDayAction(context, date)), contentAlignment = Alignment.Center, ) { Box( @@ -342,6 +347,7 @@ private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: @Composable private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) { + val context = LocalContext.current Row(modifier = GlanceModifier.fillMaxWidth()) { var col = 0 while (col < 7) { @@ -355,7 +361,14 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) { if (timed != null) { SpanBar(event = timed, dark = dark, width = colW) } else { - Box(GlanceModifier.width(colW).height(LANE_HEIGHT)) {} + // Empty lane cell: a tap opens that day, so blank space in a + // day column is a day-open target just like the number is. + Box( + GlanceModifier + .width(colW) + .height(LANE_HEIGHT) + .clickable(openDayAction(context, week.days[col])), + ) {} } col += 1 } @@ -404,14 +417,20 @@ private fun SpanBar(event: EventInstance, dark: Boolean, width: Dp) { @Composable private fun OverflowRow(week: MonthWeek, colW: Dp) { + val context = LocalContext.current Row(modifier = GlanceModifier.fillMaxWidth()) { week.days.forEachIndexed { col, date -> val shownSpans = week.spans.count { col in it.startCol..it.endCol && it.lane < MAX_LANES } val freeSlots = (MAX_LANES - shownSpans).coerceAtLeast(0) val timedShown = minOf(freeSlots, week.timedByDay[date].orEmpty().size) val hidden = (week.countByDay[date] ?: 0) - shownSpans - timedShown + // The overflow row is part of the day column too: tapping it (whether + // it shows "+N" or is blank) opens that day, same as the app. Box( - modifier = GlanceModifier.width(colW).height(LANE_HEIGHT), + modifier = GlanceModifier + .width(colW) + .height(LANE_HEIGHT) + .clickable(openDayAction(context, date)), contentAlignment = Alignment.CenterStart, ) { if (hidden > 0) { -- 2.49.1 From e5aa90bdd6f315fbd94eb5fb348ecbed083cf264 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:37:42 +0200 Subject: [PATCH 22/49] docs(changelog): note month-widget day tap opens that day (#18) Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e20ae8..ab6a6ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 widget opens the app on the month view, and tapping the "Upcoming" title on the agenda widget opens it on your default view — so there's a one-tap way back into the app that lands where you'd expect, instead of only through a day - or event. The month widget's "today" button now snaps the grid back to the - current month in place. Thanks to @rgz46vic and @ptab for the suggestions - ([#18], [#20]). + or event. On the month widget, tapping anywhere on a day — not just the small + date number — now opens that day, and the "today" button snaps the grid back + to the current month in place. Thanks to @rgz46vic and @ptab for the + suggestions ([#18], [#20]). ### Fixed - Month widget arrows and "today" button work again. On release builds the -- 2.49.1 From cbb241bc4d85a47d53ba2a58673f5bad23d0d1ec Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:41:52 +0200 Subject: [PATCH 23/49] feat(contacts): calendar-wide reminders for special-dates calendars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Managed calendars now treat reminders as a calendar-level setting instead of the seed-once-per-event model, matching how these homogeneous birthday/anniversary calendars are actually used. - New per-type "Reminders" control in the Contact special dates section. Changing it persists the per-calendar all-day override (so new events match) AND re-applies the set to *all existing events* in that calendar (CalendarDataSource.applyManagedCalendarReminders → SpecialDatesSyncEngine .applyReminders), encoding each event's all-day offset from its own date. - Settings → Notifications: the contact-date calendars no longer offer a per-calendar override row; they show a link that jumps to the Contact special dates section (managedCalendarIds now in SettingsUiState). Tests cover applyReminders (override persisted + bulk-apply invoked, and None clears). lint + unit tests + assembleDebug green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../data/calendar/CalendarDataSource.kt | 40 ++++++++++++ .../data/contacts/SpecialDatesSyncEngine.kt | 22 +++++++ .../calendula/ui/settings/SettingsScreen.kt | 62 ++++++++++++++++-- .../calendula/ui/settings/SettingsUiState.kt | 9 +++ .../ui/settings/SettingsViewModel.kt | 64 +++++++++++++------ app/src/main/res/values-de/strings.xml | 6 +- app/src/main/res/values/strings.xml | 6 +- .../data/calendar/FakeCalendarDataSource.kt | 12 ++++ .../contacts/SpecialDatesSyncEngineTest.kt | 33 ++++++++++ 9 files changed, 226 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt index 74e6d86..2f56ffa 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt @@ -29,6 +29,7 @@ import de.jeanlucmakiola.calendula.domain.ics.IcsEvent import de.jeanlucmakiola.calendula.domain.ics.ParsedIcsEvent import de.jeanlucmakiola.calendula.domain.rruleTruncatedAt import kotlinx.datetime.toJavaLocalDate +import java.time.Instant import java.time.ZoneId import java.time.ZoneOffset import java.util.UUID @@ -133,6 +134,19 @@ interface CalendarDataSource { */ fun updateManagedFields(eventId: Long, values: Map) + /** + * Replace the reminders on **every** event in [calendarId] with [minutes] + * (all-day lead times), each encoded from its own date so it fires at + * [allDayReminderTimeMinutes]. Used by the special-dates section so a reminder + * change applies to existing events too, not just future ones — the managed + * calendars treat reminders as a calendar-level setting. + */ + fun applyManagedCalendarReminders( + calendarId: Long, + allDayReminderTimeMinutes: Int, + minutes: List, + ) + /** * Insert a new event; returns the new `Events._ID`. [allDayReminderTimeMinutes] * (minutes from local midnight) is the wall-clock time all-day reminders @@ -456,6 +470,32 @@ class AndroidCalendarDataSource @Inject constructor( if (rows == 0) throw WriteFailedException("update managed event id=$eventId") } + override fun applyManagedCalendarReminders( + calendarId: Long, + allDayReminderTimeMinutes: Int, + minutes: List, + ) { + val zone = ZoneId.systemDefault() + resolver.query( + CalendarContract.Events.CONTENT_URI, + arrayOf(CalendarContract.Events._ID, CalendarContract.Events.DTSTART), + "${CalendarContract.Events.CALENDAR_ID} = ? AND ${CalendarContract.Events.DELETED} = 0", + arrayOf(calendarId.toString()), + null, + )?.use { c -> + while (c.moveToNext()) { + val eventId = c.getLong(0) + // Managed events are all-day: DTSTART is a UTC midnight. + val startDate = Instant.ofEpochMilli(c.getLong(1)) + .atZone(ZoneOffset.UTC).toLocalDate() + val encoded = minutes + .map { toProviderAllDayMinutes(it, startDate, zone, allDayReminderTimeMinutes) } + .distinct() + reconcileReminders(eventId, encoded) + } + } + } + override fun instances(beginMillis: Long, endMillis: Long): List { ensureObserversRegistered() val uri = CalendarContract.Instances.CONTENT_URI.buildUpon().apply { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt index 4a4b370..655a7a1 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt @@ -90,6 +90,28 @@ class SpecialDatesSyncEngine @Inject constructor( return SpecialDatesSyncResult.Success } + /** + * Set the reminder default for a type's managed calendar and apply it to + * **all** its existing events too (not just future ones) — for managed + * calendars the reminder is a calendar-level setting. Persists the per-calendar + * all-day override so new events keep matching. No-op if the calendar for + * [type] doesn't exist yet. + */ + suspend fun applyReminders(type: SpecialDateType, override: CalendarReminderOverride) { + val calendarId = prefs.specialDatesCalendars.first()[type] ?: return + prefs.setCalendarAllDayReminderOverride(calendarId, override) + val minutes = when (override) { + CalendarReminderOverride.Inherit -> prefs.defaultAllDayReminderMinutes.first() + CalendarReminderOverride.None -> emptyList() + is CalendarReminderOverride.Minutes -> override.minutes + } + calendars.applyManagedCalendarReminders( + calendarId = calendarId, + allDayReminderTimeMinutes = prefs.allDayReminderTimeMinutes.first(), + minutes = minutes, + ) + } + /** Delete every managed calendar and forget its id — used when the feature is turned off. */ suspend fun teardown() { calendars.findManagedCalendars().forEach { calendars.deleteCalendar(it.id) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 9e096fd..4c0b4a3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.Cake import androidx.compose.material.icons.filled.CalendarMonth @@ -180,7 +181,12 @@ fun SettingsScreen( enter = slideInHorizontally(slideSpec) { it } + fadeIn(), exit = slideOutHorizontally(slideSpec) { it } + fadeOut(), ) { - NotificationsScreen(state = state, viewModel = viewModel, onBack = { section = null }) + NotificationsScreen( + state = state, + viewModel = viewModel, + onBack = { section = null }, + onOpenSpecialDates = { section = SettingsSection.SpecialDates }, + ) } AnimatedVisibility( visible = section == SettingsSection.SpecialDates, @@ -795,6 +801,7 @@ private fun NotificationsScreen( state: SettingsUiState, viewModel: SettingsViewModel, onBack: () -> Unit, + onOpenSpecialDates: () -> Unit, ) { val context = LocalContext.current val launcher = rememberLauncherForActivityResult( @@ -919,6 +926,25 @@ private fun NotificationsScreen( Column { state.writableCalendars.forEach { calendar -> Spacer(Modifier.height(16.dp)) + // A contact special-dates calendar owns its reminders in + // its own section — link there instead of an override. + if (calendar.id in state.managedCalendarIds) { + GroupedRow( + title = calendar.displayName, + summary = stringResource(R.string.settings_calendar_reminders_managed_hint), + position = Position.Alone, + leading = { CalendarColorChip(calendar.color) }, + trailing = { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + onClick = onOpenSpecialDates, + ) + return@forEach + } val expanded = calendar.id in expandedCalendars // Calendar card; tapping expands it into a grouped list // of three (the card + the timed and all-day rows). @@ -1074,6 +1100,7 @@ private fun SpecialDatesScreen( var confirmDisableAll by remember { mutableStateOf(false) } var confirmDisableType by remember { mutableStateOf(null) } var editTemplate by remember { mutableStateOf(null) } + var reminderPickerType by remember { mutableStateOf(null) } CollapsingScaffold( title = stringResource(R.string.settings_section_special_dates), @@ -1120,13 +1147,13 @@ private fun SpecialDatesScreen( ) if (state.enabled) { - Spacer(Modifier.height(24.dp)) - // Per-type toggles + their title templates. + // Per-type card: the toggle, and while on, its calendar-wide reminder. SpecialDateType.entries.forEachIndexed { index, type -> + Spacer(Modifier.height(if (index == 0) 24.dp else 16.dp)) val on = type in state.types GroupedRow( title = stringResource(specialDateTypeLabel(type)), - position = if (index == 0) Position.Top else Position.Middle, + position = if (on) Position.Top else Position.Alone, trailing = { Switch( checked = on, @@ -1140,11 +1167,21 @@ private fun SpecialDatesScreen( if (on) confirmDisableType = type else viewModel.setSpecialDateTypeEnabled(type, true) }, ) + if (on) { + GroupedRow( + title = stringResource(R.string.settings_special_dates_reminders), + summary = reminderChoiceLabel(specialDatesReminderMinutes(state.reminderChoices[type])), + position = Position.Bottom, + onClick = { reminderPickerType = type }, + ) + } } + + Spacer(Modifier.height(24.dp)) GroupedRow( title = stringResource(R.string.settings_special_dates_template), summary = state.titleTemplates[SpecialDateType.Birthday].orEmpty(), - position = Position.Bottom, + position = Position.Alone, onClick = { editTemplate = SpecialDateType.Birthday }, ) @@ -1202,8 +1239,23 @@ private fun SpecialDatesScreen( onDismiss = { editTemplate = null }, ) } + reminderPickerType?.let { type -> + ReminderDefaultPicker( + title = stringResource(R.string.settings_special_dates_reminders), + presets = ALLDAY_REMINDER_PRESETS, + selected = state.reminderChoices[type] ?: CalendarReminderOverride.None, + // Managed calendars own their reminders outright — no "inherit global". + allowInherit = false, + onSelect = { viewModel.setSpecialDatesReminders(type, it) }, + onDismiss = { reminderPickerType = null }, + ) + } } +/** The lead-time list backing a managed calendar's reminder choice (for the summary label). */ +private fun specialDatesReminderMinutes(choice: CalendarReminderOverride?): List = + (choice as? CalendarReminderOverride.Minutes)?.minutes.orEmpty() + @Composable private fun SpecialDatesDisableDialog( message: String, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index ad153ce..e516ef7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -1,5 +1,6 @@ package de.jeanlucmakiola.calendula.ui.settings +import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.ThemeMode @@ -70,6 +71,12 @@ data class SettingsUiState( * colour palette (the colour may then not survive their next sync). */ val allowColorOnUnsupportedCalendars: Boolean = false, + /** + * Ids of the contact special-dates calendars. Their reminders are owned by + * the Contact special dates section, so the per-calendar override list links + * out to it instead of offering an override here. + */ + val managedCalendarIds: Set = emptySet(), ) /** @@ -82,6 +89,8 @@ data class SpecialDatesUiState( val enabled: Boolean = false, val types: Set = SpecialDateType.entries.toSet(), val titleTemplates: Map = emptyMap(), + /** The reminder choice per type's managed calendar (applied to all its events). */ + val reminderChoices: Map = emptyMap(), val showAge: Boolean = true, /** True when READ_CONTACTS was revoked after enabling — the mirror is paused. */ val stalledPermission: Boolean = false, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 9e80d2e..f029392 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -97,8 +97,9 @@ class SettingsViewModel @Inject constructor( prefs.perCalendarReminderOverride, prefs.perCalendarAllDayReminderOverride, writableCalendars, - ) { overrides, allDayOverrides, calendars -> - ReminderOverrides(overrides, allDayOverrides, calendars) + prefs.managedCalendarIds, + ) { overrides, allDayOverrides, calendars, managedIds -> + ReminderOverrides(overrides, allDayOverrides, calendars, managedIds) }, combine( prefs.defaultView, @@ -135,6 +136,7 @@ class SettingsViewModel @Inject constructor( perCalendarReminderOverride = overrides.timed, perCalendarAllDayReminderOverride = overrides.allDay, writableCalendars = overrides.calendars, + managedCalendarIds = overrides.managedIds, ) }.stateIn( scope = viewModelScope, @@ -160,6 +162,7 @@ class SettingsViewModel @Inject constructor( val timed: Map>, val allDay: Map>, val calendars: List, + val managedIds: Set, ) private data class ViewSettings( @@ -179,24 +182,39 @@ class SettingsViewModel @Inject constructor( /** Contact special-dates sub-page (issue #15); its own flow — the main state is full. */ val specialDatesState: StateFlow = combine( - prefs.specialDatesEnabled, - prefs.specialDatesTypes, - prefs.specialDatesTitleTemplates, - prefs.specialDatesShowAge, - prefs.specialDatesStatus, - ) { enabled, types, templates, showAge, status -> - SpecialDatesUiState( - enabled = enabled, - types = types, - // A blank stored template resolves to the localized default so the - // editor always shows something sensible. - titleTemplates = SpecialDateType.entries.associateWith { type -> - templates[type]?.takeIf { it.isNotBlank() } - ?: specialDatesSpec.defaultTitleTemplate(type) + combine( + prefs.specialDatesEnabled, + prefs.specialDatesTypes, + prefs.specialDatesTitleTemplates, + prefs.specialDatesShowAge, + prefs.specialDatesStatus, + ) { enabled, types, templates, showAge, status -> + SpecialDatesUiState( + enabled = enabled, + types = types, + // A blank stored template resolves to the localized default so the + // editor always shows something sensible. + titleTemplates = SpecialDateType.entries.associateWith { type -> + templates[type]?.takeIf { it.isNotBlank() } + ?: specialDatesSpec.defaultTitleTemplate(type) + }, + showAge = showAge, + stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked, + lastRun = status.lastRun, + ) + }, + prefs.specialDatesCalendars, + prefs.perCalendarAllDayReminderOverride, + ) { base, calendars, allDayOverrides -> + base.copy( + reminderChoices = calendars.mapValues { (_, calendarId) -> + // An override is always seeded on creation; present-empty = None. + when (val minutes = allDayOverrides[calendarId]) { + null -> CalendarReminderOverride.Inherit + emptyList() -> CalendarReminderOverride.None + else -> CalendarReminderOverride.Minutes(minutes) + } }, - showAge = showAge, - stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked, - lastRun = status.lastRun, ) }.stateIn( scope = viewModelScope, @@ -245,6 +263,14 @@ class SettingsViewModel @Inject constructor( } } + /** + * Set a type's managed-calendar reminder default and apply it to all its + * existing events (managed calendars own their reminders calendar-wide). + */ + fun setSpecialDatesReminders(type: SpecialDateType, override: CalendarReminderOverride) { + viewModelScope.launch { withContext(io) { specialDatesEngine.applyReminders(type, override) } } + } + /** Kick off an immediate reconcile (the "Sync now" action). */ fun syncSpecialDatesNow() = SpecialDatesScheduler.runNow(appContext) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8246bb..93dbdf6 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -428,13 +428,15 @@ Weitere Tage Titelformat Verwende {name} für den Kontakt und {age} für das Alter (ausgeblendet, wenn das Jahr unbekannt ist). - Hinweis: {age} ist das Alter zum nächsten Termin beim letzten Sync. Da ein jährlicher Termin nur einen Titel hat, kann es bei weit entfernten Terminen unpassend wirken. + Hinweis: Das Alter ist ein einzelner Wert vom letzten Sync — es wird nicht für jedes Jahr aktualisiert. Jeder jährliche Termin zeigt daher dieselbe Zahl, was bei weiter zurück- oder vorausliegenden Terminen falsch sein kann. + Erinnerungen Alter anzeigen {age} in Titeln anzeigen, wenn das Geburtsjahr bekannt ist Jetzt synchronisieren Noch nicht synchronisiert Zuletzt synchronisiert %1$s - Farbe, Sichtbarkeit und Erinnerungen jedes Kalenders legst du in den Kalender-Einstellungen fest. + Farbe und Sichtbarkeit jedes Kalenders legst du in den Kalender-Einstellungen fest. + In Besondere Kontakttage festgelegt Pausiert Calendula kann deine Kontakte nicht mehr lesen, daher werden diese Kalender nicht aktualisiert. Zugriff erlauben diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4c8f0c..6dbd4b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -357,14 +357,16 @@ Other dates Title format Use {name} for the contact and {age} for their age (hidden when the year is unknown). - Note: {age} is the age at the next date as of the last sync. Because a yearly event has one title, it can look off on occurrences far in the future. + Note: the age is a single value set at the last sync — it isn\'t updated for each year, so every yearly occurrence shows the same number and it can be wrong for dates further in the past or future. + Reminders Show age Include {age} in titles when the birth year is known Sync now Not synced yet Last synced %1$s - Set each calendar\'s colour, visibility and reminders in Calendars settings. + Set each calendar\'s colour and visibility in Calendars settings. + Set in Contact special dates Paused Calendula can no longer read your contacts, so these calendars aren\'t updating. Grant access diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt index edbff7f..f5a023a 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/FakeCalendarDataSource.kt @@ -190,6 +190,18 @@ internal class FakeCalendarDataSource : CalendarDataSource { return id } + /** (calendarId, allDayTime, minutes) recorded from applyManagedCalendarReminders. */ + val appliedManagedReminders = mutableListOf>>() + + override fun applyManagedCalendarReminders( + calendarId: Long, + allDayReminderTimeMinutes: Int, + minutes: List, + ) { + writeError?.let { throw it } + appliedManagedReminders += Triple(calendarId, allDayReminderTimeMinutes, minutes) + } + override fun updateManagedFields(eventId: Long, values: Map) { writeError?.let { throw it } if (values.isEmpty()) return diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt index c24a411..ad953bc 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt @@ -7,6 +7,7 @@ import androidx.datastore.preferences.core.Preferences import com.google.common.truth.Truth.assertThat import de.jeanlucmakiola.calendula.data.calendar.FakeCalendarDataSource import de.jeanlucmakiola.calendula.data.calendar.ManagedEventRow +import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType @@ -163,6 +164,38 @@ class SpecialDatesSyncEngineTest { assertThat(settings.specialDatesCalendars.first()).doesNotContainKey(SpecialDateType.Anniversary) } + @Test + fun `applyReminders persists the override and applies it to all events`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) + val (engine, settings) = engineWith(tempDir, contacts, calendars) + engine.sync(today) + val birthdayCal = settings.specialDatesCalendars.first().getValue(SpecialDateType.Birthday) + + engine.applyReminders(SpecialDateType.Birthday, CalendarReminderOverride.Minutes(listOf(0, 1440))) + + // Persisted as the per-calendar all-day override (so new events match)... + assertThat(settings.perCalendarAllDayReminderOverride.first()[birthdayCal]) + .isEqualTo(listOf(0, 1440)) + // ...and applied to the existing events in that calendar. + assertThat(calendars.appliedManagedReminders).contains( + Triple(birthdayCal, settings.allDayReminderTimeMinutes.first(), listOf(0, 1440)), + ) + } + + @Test + fun `applyReminders with None clears the reminders`(@TempDir tempDir: Path) = runTest { + val calendars = FakeCalendarDataSource() + val (engine, settings) = engineWith(tempDir, FakeContacts(), calendars) + engine.sync(today) + val birthdayCal = settings.specialDatesCalendars.first().getValue(SpecialDateType.Birthday) + + engine.applyReminders(SpecialDateType.Birthday, CalendarReminderOverride.None) + + assertThat(settings.perCalendarAllDayReminderOverride.first()[birthdayCal]).isEmpty() + assertThat(calendars.appliedManagedReminders.last().third).isEmpty() + } + // --- pure diff --- private fun desired(uid: String, title: String, dt: Long, rrule: String? = "FREQ=YEARLY") = -- 2.49.1 From 52816b327af405d0a3d2a0b72974ea6cf63ca255 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 12:54:11 +0200 Subject: [PATCH 24/49] feat(contacts): per-type title format in the special-dates section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Title format" row was only editable for Birthdays; move it into each per-type card so Anniversaries and Other dates get their own editable template too (the dialog was already keyed by type). Each enabled type card now reads toggle → title format → reminders. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/settings/SettingsScreen.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 4c0b4a3..f5ad659 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -1147,7 +1147,8 @@ private fun SpecialDatesScreen( ) if (state.enabled) { - // Per-type card: the toggle, and while on, its calendar-wide reminder. + // Per-type card: the toggle, and while on, its title format and its + // calendar-wide reminder. SpecialDateType.entries.forEachIndexed { index, type -> Spacer(Modifier.height(if (index == 0) 24.dp else 16.dp)) val on = type in state.types @@ -1168,6 +1169,12 @@ private fun SpecialDatesScreen( }, ) if (on) { + GroupedRow( + title = stringResource(R.string.settings_special_dates_template), + summary = state.titleTemplates[type].orEmpty(), + position = Position.Middle, + onClick = { editTemplate = type }, + ) GroupedRow( title = stringResource(R.string.settings_special_dates_reminders), summary = reminderChoiceLabel(specialDatesReminderMinutes(state.reminderChoices[type])), @@ -1177,14 +1184,6 @@ private fun SpecialDatesScreen( } } - Spacer(Modifier.height(24.dp)) - GroupedRow( - title = stringResource(R.string.settings_special_dates_template), - summary = state.titleTemplates[SpecialDateType.Birthday].orEmpty(), - position = Position.Alone, - onClick = { editTemplate = SpecialDateType.Birthday }, - ) - Spacer(Modifier.height(24.dp)) GroupedRow( title = stringResource(R.string.settings_special_dates_show_age), -- 2.49.1 From 4e125e58d5cb044d2e44c5e8e22e0facf6e01b13 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 13:19:04 +0200 Subject: [PATCH 25/49] feat(contacts): show source year instead of age in titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A FREQ=YEARLY event has one static title, so a per-occurrence age is impossible without heavy per-year exception rows. Replace {age} with {year} — the birth year (or an anniversary's start year) — which is static and correct on every occurrence, and shows everywhere (widgets, other apps, exports). - renderSpecialDateTitle substitutes {year}; drop the age snapshot computation and the sync `today` parameter. - Default templates become "{name}'s birthday ({year})" / "…anniversary ({year})". - Rename the setting to "Show year" (prefs specialDatesShowYear) and remove the now-unneeded {age} snapshot disclaimer in the template editor. - Update tests, CHANGELOG and the design-doc note. lint + unit tests + assembleDebug green. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 2 +- .../data/contacts/SpecialDatesScheduler.kt | 6 +-- .../data/contacts/SpecialDatesSyncEngine.kt | 27 ++++++-------- .../calendula/data/prefs/SettingsPrefs.kt | 10 ++--- .../domain/contacts/SpecialDateFormatting.kt | 29 ++++----------- .../calendula/ui/settings/SettingsScreen.kt | 20 +++------- .../calendula/ui/settings/SettingsUiState.kt | 3 +- .../ui/settings/SettingsViewModel.kt | 10 ++--- app/src/main/res/values-de/strings.xml | 11 +++--- app/src/main/res/values/strings.xml | 11 +++--- .../contacts/SpecialDatesSyncEngineTest.kt | 37 +++++++++---------- .../calendula/data/prefs/SettingsPrefsTest.kt | 2 +- .../contacts/SpecialDateFormattingTest.kt | 32 +++------------- docs/design/contact-special-dates.md | 14 +++---- 14 files changed, 78 insertions(+), 136 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f6971..e4468d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 "Anniversaries" and "Other dates" calendars that stay in sync as your contacts change. Each is a normal local calendar, so you set its colour, visibility and reminders the usual way — new birthdays even start with a reminder a week before - *and* on the day. The title format is yours to customise (`{name}`, `{age}`). + *and* on the day. The title format is yours to customise (`{name}`, `{year}`). This is the first feature to use the contacts permission: it is requested only when you turn the feature on, everything stays on your device (Calendula has no internet access), and your contacts are only ever read, never changed. Thanks diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt index f5237b7..05a1bac 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt @@ -17,10 +17,7 @@ import dagger.hilt.components.SingletonComponent import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason import kotlinx.coroutines.flow.first -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn import java.util.concurrent.TimeUnit -import kotlin.time.Clock /** * Schedules the contact special-dates mirror. Birthdays change rarely, so this @@ -97,9 +94,8 @@ class SpecialDatesSyncWorker( return Result.success() } - val today = Clock.System.todayIn(TimeZone.currentSystemDefault()) return try { - val result = deps.syncEngine().sync(today) + val result = deps.syncEngine().sync() val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) { SpecialDatesStalledReason.PermissionRevoked } else { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt index 655a7a1..fa3369f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt @@ -14,12 +14,10 @@ import de.jeanlucmakiola.calendula.domain.SimpleRecurrence import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.domain.contacts.anchorDate -import de.jeanlucmakiola.calendula.domain.contacts.ageAtNextOccurrence import de.jeanlucmakiola.calendula.domain.contacts.managedEventUid import de.jeanlucmakiola.calendula.domain.contacts.renderSpecialDateTitle import de.jeanlucmakiola.calendula.domain.toRRule import kotlinx.coroutines.flow.first -import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import java.time.ZoneId @@ -58,11 +56,10 @@ class SpecialDatesSyncEngine @Inject constructor( ) { /** - * Reconcile every enabled type against the device's contacts. [today] anchors - * the age snapshot. Returns why it stopped early (disabled / permission gone) - * or [SpecialDatesSyncResult.Success]. + * Reconcile every enabled type against the device's contacts. Returns why it + * stopped early (disabled / permission gone) or [SpecialDatesSyncResult.Success]. */ - suspend fun sync(today: LocalDate): SpecialDatesSyncResult { + suspend fun sync(): SpecialDatesSyncResult { if (!prefs.specialDatesEnabled.first()) return SpecialDatesSyncResult.Disabled if (!contacts.hasPermission()) return SpecialDatesSyncResult.PermissionMissing @@ -72,7 +69,7 @@ class SpecialDatesSyncEngine @Inject constructor( val desiredByType = contacts.readSpecialDates().groupBy { it.type } val reminderCtx = readReminderContext() val templates = prefs.specialDatesTitleTemplates.first() - val showAge = prefs.specialDatesShowAge.first() + val showYear = prefs.specialDatesShowYear.first() calendarByType.forEach { (type, calendarId) -> val template = templates[type]?.takeIf { it.isNotBlank() } @@ -81,9 +78,8 @@ class SpecialDatesSyncEngine @Inject constructor( calendarId = calendarId, type = type, contactsOfType = desiredByType[type].orEmpty(), - today = today, template = template, - showAge = showAge, + showYear = showYear, reminderCtx = reminderCtx, ) } @@ -165,13 +161,12 @@ class SpecialDatesSyncEngine @Inject constructor( calendarId: Long, type: SpecialDateType, contactsOfType: List, - today: LocalDate, template: String, - showAge: Boolean, + showYear: Boolean, reminderCtx: ReminderContext, ) { val built = contactsOfType.map { sd -> - buildManagedEvent(calendarId, type, sd, today, template, showAge, reminderCtx) + buildManagedEvent(calendarId, type, sd, template, showYear, reminderCtx) } val diff = diffManagedEvents(built.map { it.desired }, calendars.queryManagedEvents(calendarId)) val formByUid = built.associate { it.desired.uid to it.form } @@ -186,15 +181,15 @@ class SpecialDatesSyncEngine @Inject constructor( calendarId: Long, type: SpecialDateType, sd: ContactSpecialDate, - today: LocalDate, template: String, - showAge: Boolean, + showYear: Boolean, reminderCtx: ReminderContext, ): BuiltManagedEvent { val anchor = sd.anchorDate() val start = LocalDateTime(anchor, LocalTime(0, 0)) - val age = if (showAge) sd.ageAtNextOccurrence(today) else null - val title = renderSpecialDateTitle(template, sd.displayName, age) + // The source year is static and correct on every occurrence (unlike age). + val year = if (showYear) sd.year else null + val title = renderSpecialDateTitle(template, sd.displayName, year) val form = EventForm( calendarId = calendarId, title = title, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 96d95c3..2f7c87c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -476,11 +476,11 @@ class SettingsPrefs @Inject constructor( store.edit { it[titleTemplateKey(type)] = template } } - /** Whether `{age}` resolves in the title (only meaningful when the year is known). Default ON. */ - val specialDatesShowAge: Flow = store.data.map { it[SPECIAL_DATES_SHOW_AGE_KEY] ?: true } + /** Whether `{year}` resolves in the title (only meaningful when the year is known). Default ON. */ + val specialDatesShowYear: Flow = store.data.map { it[SPECIAL_DATES_SHOW_YEAR_KEY] ?: true } - suspend fun setSpecialDatesShowAge(enabled: Boolean) { - store.edit { it[SPECIAL_DATES_SHOW_AGE_KEY] = enabled } + suspend fun setSpecialDatesShowYear(enabled: Boolean) { + store.edit { it[SPECIAL_DATES_SHOW_YEAR_KEY] = enabled } } /** Outcome of the last mirror sync, for the settings status line. */ @@ -571,7 +571,7 @@ class SettingsPrefs @Inject constructor( const val MIN_BACKUP_INTERVAL = 30L internal val SPECIAL_DATES_ENABLED_KEY = booleanPreferencesKey("special_dates_enabled") - internal val SPECIAL_DATES_SHOW_AGE_KEY = booleanPreferencesKey("special_dates_show_age") + internal val SPECIAL_DATES_SHOW_YEAR_KEY = booleanPreferencesKey("special_dates_show_year") internal val SPECIAL_DATES_LAST_RUN_KEY = longPreferencesKey("special_dates_last_run") internal val SPECIAL_DATES_STALLED_KEY = stringPreferencesKey("special_dates_stalled_reason") internal val SPECIAL_DATES_LAST_FG_SYNC_KEY = diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt b/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt index 9c78a36..f356abb 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt @@ -1,7 +1,6 @@ package de.jeanlucmakiola.calendula.domain.contacts import kotlinx.datetime.LocalDate -import kotlinx.datetime.number /** * The deterministic `Events.UID_2445` that ties a mirrored event to its source @@ -21,30 +20,18 @@ fun ContactSpecialDate.anchorDate(): LocalDate = LocalDate(year ?: YEARLESS_ANCHOR_YEAR, month, day) /** - * The age the contact reaches on the *upcoming* occurrence of this date, - * relative to [today], or null when the year is unknown. Because a - * `FREQ=YEARLY` event carries a single static title, this is a snapshot taken - * at sync time (refreshed on the next sync), not a per-occurrence value. - */ -fun ContactSpecialDate.ageAtNextOccurrence(today: LocalDate): Int? { - val birthYear = year ?: return null - val monthDayPassedThisYear = month < today.month.number || - (month == today.month.number && day < today.day) - val nextYear = if (monthDayPassedThisYear) today.year + 1 else today.year - return nextYear - birthYear -} - -/** - * Render a title [template] for a contact, substituting `{name}` and `{age}` - * (the latter empty when [age] is null). Collapses the whitespace the empty - * `{age}` may leave behind, so "{name}'s birthday ({age})" degrades cleanly to + * Render a title [template] for a contact, substituting `{name}` and `{year}` + * (the source year — a birthday's birth year or an anniversary's start year; + * empty when [year] is null). Unlike an age, the year is static and correct on + * every occurrence of the yearly event. Collapses the whitespace an empty + * `{year}` may leave behind, so "{name}'s birthday ({year})" degrades cleanly to * "Jane's birthday" when no year is known. */ -fun renderSpecialDateTitle(template: String, name: String, age: Int?): String = +fun renderSpecialDateTitle(template: String, name: String, year: Int?): String = template .replace("{name}", name) - .replace("{age}", age?.toString().orEmpty()) - // Drop an empty "()" left by an unresolved {age}, then tidy spacing. + .replace("{year}", year?.toString().orEmpty()) + // Drop an empty "()" left by an unresolved {year}, then tidy spacing. .replace(Regex("""\(\s*\)"""), "") .replace(Regex("""\s+"""), " ") .trim() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index f5ad659..b0b7fec 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -1186,16 +1186,16 @@ private fun SpecialDatesScreen( Spacer(Modifier.height(24.dp)) GroupedRow( - title = stringResource(R.string.settings_special_dates_show_age), - summary = stringResource(R.string.settings_special_dates_show_age_hint), + title = stringResource(R.string.settings_special_dates_show_year), + summary = stringResource(R.string.settings_special_dates_show_year_hint), position = Position.Top, trailing = { Switch( - checked = state.showAge, - onCheckedChange = viewModel::setSpecialDatesShowAge, + checked = state.showYear, + onCheckedChange = viewModel::setSpecialDatesShowYear, ) }, - onClick = { viewModel.setSpecialDatesShowAge(!state.showAge) }, + onClick = { viewModel.setSpecialDatesShowYear(!state.showYear) }, ) GroupedRow( title = stringResource(R.string.settings_special_dates_sync_now), @@ -1302,16 +1302,6 @@ private fun SpecialDatesTemplateDialog( style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - // Only warn about {age}'s snapshot behaviour once the user has - // actually put it in the template. - if (text.contains("{age}")) { - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(R.string.settings_special_dates_template_age_note), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.tertiary, - ) - } } }, confirmButton = { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt index e516ef7..f905a9e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsUiState.kt @@ -91,7 +91,8 @@ data class SpecialDatesUiState( val titleTemplates: Map = emptyMap(), /** The reminder choice per type's managed calendar (applied to all its events). */ val reminderChoices: Map = emptyMap(), - val showAge: Boolean = true, + /** Whether {year} resolves in titles (the source year is static and always correct). */ + val showYear: Boolean = true, /** True when READ_CONTACTS was revoked after enabling — the mirror is paused. */ val stalledPermission: Boolean = false, /** Epoch millis of the last sync, or 0 if it has never run. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index f029392..8b75a1a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -186,9 +186,9 @@ class SettingsViewModel @Inject constructor( prefs.specialDatesEnabled, prefs.specialDatesTypes, prefs.specialDatesTitleTemplates, - prefs.specialDatesShowAge, + prefs.specialDatesShowYear, prefs.specialDatesStatus, - ) { enabled, types, templates, showAge, status -> + ) { enabled, types, templates, showYear, status -> SpecialDatesUiState( enabled = enabled, types = types, @@ -198,7 +198,7 @@ class SettingsViewModel @Inject constructor( templates[type]?.takeIf { it.isNotBlank() } ?: specialDatesSpec.defaultTitleTemplate(type) }, - showAge = showAge, + showYear = showYear, stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked, lastRun = status.lastRun, ) @@ -256,9 +256,9 @@ class SettingsViewModel @Inject constructor( } } - fun setSpecialDatesShowAge(enabled: Boolean) { + fun setSpecialDatesShowYear(enabled: Boolean) { viewModelScope.launch { - prefs.setSpecialDatesShowAge(enabled) + prefs.setSpecialDatesShowYear(enabled) SpecialDatesScheduler.runNow(appContext) } } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 93dbdf6..882f2e1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -415,8 +415,8 @@ Geburtstage Jahrestage Besondere Tage - {name}s Geburtstag - {name}s Jahrestag + {name}s Geburtstag ({year}) + {name}s Jahrestag ({year}) {name} Verwaltet von „%1$s“ — Titel, Datum und Wiederholung werden aus deinen Kontakten synchron gehalten. Erinnerungen, Ort und Notizen kannst du selbst bearbeiten. Geburtstage & Jahrestage aus Kontakten @@ -427,11 +427,10 @@ Jahrestage Weitere Tage Titelformat - Verwende {name} für den Kontakt und {age} für das Alter (ausgeblendet, wenn das Jahr unbekannt ist). - Hinweis: Das Alter ist ein einzelner Wert vom letzten Sync — es wird nicht für jedes Jahr aktualisiert. Jeder jährliche Termin zeigt daher dieselbe Zahl, was bei weiter zurück- oder vorausliegenden Terminen falsch sein kann. + Verwende {name} für den Kontakt und {year} für das Jahr (Geburtsjahr bzw. Startjahr eines Jahrestags; ausgeblendet, wenn unbekannt). Erinnerungen - Alter anzeigen - {age} in Titeln anzeigen, wenn das Geburtsjahr bekannt ist + Jahr anzeigen + {year} in Titeln anzeigen, wenn es bekannt ist Jetzt synchronisieren Noch nicht synchronisiert Zuletzt synchronisiert %1$s diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6dbd4b6..5abcfb4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -356,11 +356,10 @@ Anniversaries Other dates Title format - Use {name} for the contact and {age} for their age (hidden when the year is unknown). - Note: the age is a single value set at the last sync — it isn\'t updated for each year, so every yearly occurrence shows the same number and it can be wrong for dates further in the past or future. + Use {name} for the contact and {year} for the year (the birth year, or an anniversary\'s start year; hidden when unknown). Reminders - Show age - Include {age} in titles when the birth year is known + Show year + Include {year} in titles when it is known Sync now Not synced yet @@ -500,7 +499,7 @@ Birthdays Anniversaries Special dates - {name}\'s birthday - {name}\'s anniversary + {name}\'s birthday ({year}) + {name}\'s anniversary ({year}) {name} diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt index ad953bc..dbfcff8 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngineTest.kt @@ -13,15 +13,12 @@ import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest -import kotlinx.datetime.LocalDate import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir import java.nio.file.Path class SpecialDatesSyncEngineTest { - private val today = LocalDate(2026, 3, 1) - private class FakeContacts( var permission: Boolean = true, var dates: List = emptyList(), @@ -34,7 +31,7 @@ class SpecialDatesSyncEngineTest { override fun displayName(type: SpecialDateType) = type.name override fun color(type: SpecialDateType) = 0xFF112233.toInt() override fun defaultTitleTemplate(type: SpecialDateType) = when (type) { - SpecialDateType.Birthday -> "{name}'s birthday ({age})" + SpecialDateType.Birthday -> "{name}'s birthday ({year})" SpecialDateType.Anniversary -> "{name}'s anniversary" SpecialDateType.Custom -> "{name}" } @@ -65,7 +62,7 @@ class SpecialDatesSyncEngineTest { fun `disabled feature is a no-op`(@TempDir tempDir: Path) = runTest { val calendars = FakeCalendarDataSource() val (engine, _) = engineWith(tempDir, FakeContacts(), calendars, enabled = false) - assertThat(engine.sync(today)).isEqualTo(SpecialDatesSyncResult.Disabled) + assertThat(engine.sync()).isEqualTo(SpecialDatesSyncResult.Disabled) assertThat(calendars.createdManagedCalendars).isEmpty() } @@ -73,7 +70,7 @@ class SpecialDatesSyncEngineTest { fun `missing permission reports stalled without touching calendars`(@TempDir tempDir: Path) = runTest { val calendars = FakeCalendarDataSource() val (engine, _) = engineWith(tempDir, FakeContacts(permission = false), calendars) - assertThat(engine.sync(today)).isEqualTo(SpecialDatesSyncResult.PermissionMissing) + assertThat(engine.sync()).isEqualTo(SpecialDatesSyncResult.PermissionMissing) assertThat(calendars.createdManagedCalendars).isEmpty() } @@ -88,16 +85,16 @@ class SpecialDatesSyncEngineTest { ) val (engine, settings) = engineWith(tempDir, contacts, calendars) - assertThat(engine.sync(today)).isEqualTo(SpecialDatesSyncResult.Success) + assertThat(engine.sync()).isEqualTo(SpecialDatesSyncResult.Success) // All three type calendars exist; the birthday one holds both contacts. assertThat(calendars.createdManagedCalendars.map { it.type }) .containsExactlyElementsIn(SpecialDateType.entries) assertThat(calendars.insertedManagedEvents).hasSize(2) assertThat(calendars.insertedManagedEvents.map { it.second }) .containsExactly("contact-birthday:a@calendula", "contact-birthday:b@calendula") - // Year-less contact drops the age; year-known keeps it. + // Year-less contact drops the year; year-known keeps it. val titles = calendars.insertedManagedEvents.map { it.first.title } - assertThat(titles).contains("Jane's birthday (36)") + assertThat(titles).contains("Jane's birthday (1990)") assertThat(titles).contains("Bob's birthday") // Calendar ids were persisted for the editor / next sync. assertThat(settings.specialDatesCalendars.first()).containsKey(SpecialDateType.Birthday) @@ -109,10 +106,10 @@ class SpecialDatesSyncEngineTest { val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) val (engine, _) = engineWith(tempDir, contacts, calendars) - engine.sync(today) + engine.sync() val insertsAfterFirst = calendars.insertedManagedEvents.size val calendarsAfterFirst = calendars.createdManagedCalendars.size - engine.sync(today) + engine.sync() assertThat(calendars.insertedManagedEvents).hasSize(insertsAfterFirst) assertThat(calendars.createdManagedCalendars).hasSize(calendarsAfterFirst) @@ -125,16 +122,16 @@ class SpecialDatesSyncEngineTest { val calendars = FakeCalendarDataSource() val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) val (engine, _) = engineWith(tempDir, contacts, calendars) - engine.sync(today) + engine.sync() contacts.dates = listOf(birthday("a", "Janet", 5, 14, 1990)) - engine.sync(today) + engine.sync() assertThat(calendars.updatedManagedFields).hasSize(1) val (_, columns) = calendars.updatedManagedFields.single() // Only managed columns are ever written — never reminders/location. assertThat(columns.keys).containsExactly(CalendarContract.Events.TITLE) - assertThat(columns[CalendarContract.Events.TITLE]).isEqualTo("Janet's birthday (36)") + assertThat(columns[CalendarContract.Events.TITLE]).isEqualTo("Janet's birthday (1990)") } @Test @@ -142,10 +139,10 @@ class SpecialDatesSyncEngineTest { val calendars = FakeCalendarDataSource() val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) val (engine, _) = engineWith(tempDir, contacts, calendars) - engine.sync(today) + engine.sync() contacts.dates = emptyList() - engine.sync(today) + engine.sync() assertThat(calendars.deletedEventIds).hasSize(1) } @@ -154,11 +151,11 @@ class SpecialDatesSyncEngineTest { fun `disabling a type deletes its calendar on the next sync`(@TempDir tempDir: Path) = runTest { val calendars = FakeCalendarDataSource() val (engine, settings) = engineWith(tempDir, FakeContacts(), calendars) - engine.sync(today) + engine.sync() val anniversaryId = settings.specialDatesCalendars.first().getValue(SpecialDateType.Anniversary) settings.setSpecialDateTypeEnabled(SpecialDateType.Anniversary, false) - engine.sync(today) + engine.sync() assertThat(calendars.deletedCalendarIds).contains(anniversaryId) assertThat(settings.specialDatesCalendars.first()).doesNotContainKey(SpecialDateType.Anniversary) @@ -169,7 +166,7 @@ class SpecialDatesSyncEngineTest { val calendars = FakeCalendarDataSource() val contacts = FakeContacts(dates = listOf(birthday("a", "Jane", 5, 14, 1990))) val (engine, settings) = engineWith(tempDir, contacts, calendars) - engine.sync(today) + engine.sync() val birthdayCal = settings.specialDatesCalendars.first().getValue(SpecialDateType.Birthday) engine.applyReminders(SpecialDateType.Birthday, CalendarReminderOverride.Minutes(listOf(0, 1440))) @@ -187,7 +184,7 @@ class SpecialDatesSyncEngineTest { fun `applyReminders with None clears the reminders`(@TempDir tempDir: Path) = runTest { val calendars = FakeCalendarDataSource() val (engine, settings) = engineWith(tempDir, FakeContacts(), calendars) - engine.sync(today) + engine.sync() val birthdayCal = settings.specialDatesCalendars.first().getValue(SpecialDateType.Birthday) engine.applyReminders(SpecialDateType.Birthday, CalendarReminderOverride.None) diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt index f39c5cb..af7a4ac 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt @@ -349,7 +349,7 @@ class SettingsPrefsTest { assertThat(prefs.specialDatesTypes.first()).isEqualTo(SpecialDateType.entries.toSet()) assertThat(prefs.specialDatesCalendars.first()).isEmpty() assertThat(prefs.managedCalendarIds.first()).isEmpty() - assertThat(prefs.specialDatesShowAge.first()).isTrue() + assertThat(prefs.specialDatesShowYear.first()).isTrue() } @Test diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt index 393ccbe..9961f7a 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt @@ -34,36 +34,14 @@ class SpecialDateFormattingTest { } @Test - fun `age is null when the year is unknown`() { - assertThat(birthday(5, 14, null).ageAtNextOccurrence(LocalDate(2026, 1, 1))).isNull() + fun `title substitutes name and year`() { + assertThat(renderSpecialDateTitle("{name}'s birthday ({year})", "Jane", 1990)) + .isEqualTo("Jane's birthday (1990)") } @Test - fun `age counts the upcoming birthday when it is still ahead this year`() { - // Born 1990-05-14; today 2026-03-01 → next birthday 2026 → turns 36. - assertThat(birthday(5, 14, 1990).ageAtNextOccurrence(LocalDate(2026, 3, 1))).isEqualTo(36) - } - - @Test - fun `age rolls to next year once the birthday has passed`() { - // Born 1990-05-14; today 2026-06-01 → next birthday 2027 → turns 37. - assertThat(birthday(5, 14, 1990).ageAtNextOccurrence(LocalDate(2026, 6, 1))).isEqualTo(37) - } - - @Test - fun `age counts today's birthday as this year`() { - assertThat(birthday(5, 14, 1990).ageAtNextOccurrence(LocalDate(2026, 5, 14))).isEqualTo(36) - } - - @Test - fun `title substitutes name and age`() { - assertThat(renderSpecialDateTitle("{name}'s birthday ({age})", "Jane", 30)) - .isEqualTo("Jane's birthday (30)") - } - - @Test - fun `title drops an empty age parenthesis and tidies spacing`() { - assertThat(renderSpecialDateTitle("{name}'s birthday ({age})", "Jane", null)) + fun `title drops an empty year parenthesis and tidies spacing`() { + assertThat(renderSpecialDateTitle("{name}'s birthday ({year})", "Jane", null)) .isEqualTo("Jane's birthday") } } diff --git a/docs/design/contact-special-dates.md b/docs/design/contact-special-dates.md index 2b27c88..e92965d 100644 --- a/docs/design/contact-special-dates.md +++ b/docs/design/contact-special-dates.md @@ -13,14 +13,14 @@ Implementation notes / deviations from this design: (and doubles as a stable iCal UID). The managed **calendar** still carries a `CAL_SYNC2` marker for re-adoption (calendar-row writes already use the sync-adapter URI). -- **Age is a sync-time snapshot.** A `FREQ=YEARLY` row has one static title, so - `{age}` reflects the upcoming birthday at the last sync (refreshed daily / on - foreground), not a per-occurrence value. Because of this, `{age}` is **kept out - of the default templates**; users can add it, and the title-format editor shows - a disclaimer once they do. +- **Year, not age.** A `FREQ=YEARLY` row has one static title, so a per-occurrence + age is impossible without per-year exception rows (rejected as too heavy). The + template instead offers `{year}` — the birth year (or an anniversary's start + year) — which is static and correct on every occurrence. A "Show year" toggle + gates it; it's in the default templates ("{name}'s birthday ({year})"). - New managed calendars seed a per-calendar all-day reminder default of on-the-day + one week before, so birthdays get lead time out of the box. -- Title template is user-editable from day one (`{name}`, `{age}`). +- Title template is user-editable per type from day one (`{name}`, `{year}`). This document captures the full design for surfacing contact birthdays — and, by extension, anniversaries and custom dates — as auto-updating local @@ -239,7 +239,7 @@ rest. - [x] Sync engine: idempotent diff keyed on the deterministic `UID_2445` (see notes; not `SYNC_DATA1`); targeted managed-column updates; seed user-owned fields on insert only. -- [x] `FREQ=YEARLY` event generation + age (when year known; sync-time snapshot). +- [x] `FREQ=YEARLY` event generation + `{year}` in titles (static, always correct). - [x] Auto-update: on enable, on foreground, daily WorkManager. - [x] Editor: disable managed fields for managed-calendar events. - [x] Revoked-permission detection + stalled-state surface. -- 2.49.1 From 37f0e22911dab4941696c53c0520f88d105f910c Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 13:25:06 +0200 Subject: [PATCH 26/49] fix(views): correct snapping + animation for reorderable view lists Rework ReorderableColumn: the first cut measured each row, but grouped cards have position-dependent padding, so slot heights varied and rows snapped to the wrong places with no animation. Now rows are a fixed uniform pitch, the target slot is the whole-pitches dragged, neighbours slide aside with a spring, and the held row settles onto its slot before the order commits. Add a lift (scale + shadow) on the held row and cancel an in-flight settle if a new drag pre-empts it. GroupedRow gains gapBelow so the reorderable list can own uniform inter-row spacing. On-device verified on Pixel 10. Co-Authored-By: Claude Opus 4.8 --- .../calendula/ui/common/GroupedList.kt | 10 +- .../calendula/ui/common/ReorderableColumn.kt | 151 ++++++++++++------ .../calendula/ui/settings/SettingsScreen.kt | 5 +- 3 files changed, 114 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt index c183aab..dd39503 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt @@ -142,6 +142,9 @@ fun GroupedRow( dimmed: Boolean = false, container: Color? = null, minHeight: Dp = 72.dp, + // The 2.dp separation between rows in a run. Suppressed by the reorderable + // list, which owns uniform spacing itself so every slot has the same pitch. + gapBelow: Boolean = true, leading: @Composable (() -> Unit)? = null, trailing: @Composable (() -> Unit)? = null, onClick: (() -> Unit)? = null, @@ -160,9 +163,10 @@ fun GroupedRow( topStart = small, topEnd = small, bottomStart = full, bottomEnd = full, ) } - val gap = when (position) { - Position.Top, Position.Middle -> Modifier.padding(bottom = 2.dp) - Position.Bottom, Position.Alone -> Modifier + val gap = when { + !gapBelow -> Modifier + position == Position.Top || position == Position.Middle -> Modifier.padding(bottom = 2.dp) + else -> Modifier } val itemColors = if (selected) { ListItemDefaults.colors( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt index 369ee3a..d6a1f5e 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt @@ -1,19 +1,38 @@ package de.jeanlucmakiola.calendula.ui.common +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +/** Uniform row height for [ReorderableColumn]; a fixed pitch keeps drag maths exact. */ +val ReorderableRowHeight: Dp = 64.dp +private val RowGap: Dp = 2.dp /** * A vertical list whose rows can be dragged into a new order by their handle. @@ -21,15 +40,15 @@ import androidx.compose.ui.zIndex * Built for the short, fixed grouped-card lists in Settings (#24) — no external * dependency and no [androidx.compose.foundation.lazy.LazyColumn] (the settings * screens are a single [androidx.compose.foundation.verticalScroll] column, which - * can't nest a scrolling list). [rowContent] receives the [Position] for the - * row's current place in the order (so it can reuse [GroupedRow]'s card shaping) - * and a `dragHandle` [Modifier] to attach to whatever element should start a - * drag. While a row is held it floats above the others, which reflow live; the - * final order is committed once via [onReorder] on release, so the persistence - * layer sees one write per gesture, not one per crossed row. + * can't nest a scrolling list). Rows are a fixed [ReorderableRowHeight] with a + * uniform gap, so a row's target slot is simply how many whole pitches it has + * been dragged. The held row follows the finger while the others slide out of + * its way (animated); on release the held row settles into its slot, then the + * order is committed with a single [onReorder] call. * - * Reordering is measurement-driven: each row reports its own height, so rows of - * unequal height still swap at the right point. + * [rowContent] receives the [Position] for the row's place in the order (to reuse + * [GroupedRow]'s card shaping — pass `gapBelow = false` there, this owns spacing) + * and a `dragHandle` [Modifier] to attach to the element that starts a drag. */ @Composable fun ReorderableColumn( @@ -39,64 +58,100 @@ fun ReorderableColumn( modifier: Modifier = Modifier, rowContent: @Composable (item: T, position: Position, dragHandle: Modifier, isDragging: Boolean) -> Unit, ) { - // Local working copy so rows reflow instantly during a drag; re-seeded when - // the incoming list changes (including the echo of our own committed order). + val pitchPx = with(LocalDensity.current) { (ReorderableRowHeight + RowGap).toPx() } + val scope = rememberCoroutineScope() + + // Local working copy; re-seeded when the incoming list changes (including the + // echo of our own committed order). var order by remember(items) { mutableStateOf(items) } var draggedKey by remember { mutableStateOf(null) } - // Translation of the held row from its settled slot; a swap folds one slot - // height back into this so the row stays under the finger across the swap. - var dragOffsetY by remember { mutableStateOf(0f) } - val heights = remember { mutableStateMapOf() } + // Live translation of the held row from its slot (px); also drives the + // release settle, animated back onto a slot before the order is committed. + var dragOffset by remember { mutableFloatStateOf(0f) } + // The running release/cancel settle, cancelled if a new drag pre-empts it. + var settleJob by remember { mutableStateOf(null) } - Column(modifier) { + val draggedIndex = draggedKey?.let { key -> order.indexOfFirst { keyOf(it) == key }.takeIf { it >= 0 } } + // Whole slots dragged → the slot the held row currently hovers over. + val targetIndex = draggedIndex?.let { + (it + (dragOffset / pitchPx).roundToInt()).coerceIn(0, order.lastIndex) + } + + Column(modifier, verticalArrangement = Arrangement.spacedBy(RowGap)) { order.forEachIndexed { index, item -> val key = keyOf(item) - val isDragging = key == draggedKey + val isDragged = key == draggedKey + + // Slide neighbours by one pitch to open the gap the held row will drop + // into. Snap (not animate) once idle, so committing the new order — which + // moves each row's slot — doesn't visibly fight a lingering animation. + val shift = when { + draggedIndex == null || targetIndex == null || isDragged -> 0f + index in (draggedIndex + 1)..targetIndex -> -pitchPx + index in targetIndex until draggedIndex -> pitchPx + else -> 0f + } + val animatedShift by animateFloatAsState( + targetValue = shift, + animationSpec = if (draggedKey != null) spring(stiffness = Spring.StiffnessMediumLow) else snap(), + label = "reorderShift", + ) + val dragHandle = Modifier.pointerInput(key) { detectDragGestures( onDragStart = { + settleJob?.cancel() draggedKey = key - dragOffsetY = 0f + dragOffset = 0f + }, + onDrag = { change, amount -> + change.consume() + dragOffset += amount.y }, onDragEnd = { - draggedKey = null - dragOffsetY = 0f - onReorder(order) + val from = order.indexOfFirst { keyOf(it) == key } + if (from < 0) return@detectDragGestures + val to = (from + (dragOffset / pitchPx).roundToInt()).coerceIn(0, order.lastIndex) + settleJob = scope.launch { + // Settle the held row onto its target slot, then commit — + // resetting offset and slot in the same frame, so nothing jumps. + Animatable(dragOffset).animateTo((to - from) * pitchPx, tween(160)) { + dragOffset = value + } + if (to != from) { + order = order.toMutableList().apply { add(to, removeAt(from)) } + onReorder(order) + } + draggedKey = null + dragOffset = 0f + } }, onDragCancel = { - draggedKey = null - dragOffsetY = 0f - }, - onDrag = { change, dragAmount -> - change.consume() - dragOffsetY += dragAmount.y - val cur = order.indexOfFirst { keyOf(it) == key } - if (cur < 0) return@detectDragGestures - // Swap once the row has travelled past half of the - // neighbour it's heading toward, in the drag direction. - if (dragAmount.y > 0 && cur < order.lastIndex) { - val nextHeight = heights[keyOf(order[cur + 1])] ?: 0 - if (nextHeight > 0 && dragOffsetY > nextHeight / 2f) { - order = order.toMutableList().apply { add(cur + 1, removeAt(cur)) } - dragOffsetY -= nextHeight - } - } else if (dragAmount.y < 0 && cur > 0) { - val prevHeight = heights[keyOf(order[cur - 1])] ?: 0 - if (prevHeight > 0 && dragOffsetY < -prevHeight / 2f) { - order = order.toMutableList().apply { add(cur - 1, removeAt(cur)) } - dragOffsetY += prevHeight - } + settleJob = scope.launch { + Animatable(dragOffset).animateTo(0f, tween(160)) { dragOffset = value } + draggedKey = null + dragOffset = 0f } }, ) } + Box( Modifier - .onSizeChanged { heights[key] = it.height } - .zIndex(if (isDragging) 1f else 0f) - .graphicsLayer { translationY = if (isDragging) dragOffsetY else 0f }, + .height(ReorderableRowHeight) + .zIndex(if (isDragged) 1f else 0f) + .graphicsLayer { + translationY = if (isDragged) dragOffset else animatedShift + if (isDragged) { + scaleX = 1.02f + scaleY = 1.02f + shadowElevation = 8.dp.toPx() + shape = RoundedCornerShape(20.dp) + clip = false + } + }, ) { - rowContent(item, positionOf(index, order.size), dragHandle, isDragging) + rowContent(item, positionOf(index, order.size), dragHandle, isDragged) } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 3c3c97a..8b17a74 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -107,6 +107,7 @@ import de.jeanlucmakiola.calendula.ui.common.GroupedRow import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig import de.jeanlucmakiola.calendula.ui.common.ReorderableColumn +import de.jeanlucmakiola.calendula.ui.common.ReorderableRowHeight import de.jeanlucmakiola.calendula.ui.common.icon import de.jeanlucmakiola.calendula.ui.common.labelRes import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip @@ -768,7 +769,9 @@ private fun ViewRow( title = stringResource(view.labelRes), position = position, dimmed = dimmed, - minHeight = 64.dp, + minHeight = ReorderableRowHeight, + // The reorderable column owns the inter-row spacing (uniform pitch). + gapBelow = false, container = if (isDragging) MaterialTheme.colorScheme.secondaryContainer else null, leading = { Icon( -- 2.49.1 From bfc9a0db39ecd1a1c43c2bd6c9751a756db3421d Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 1 Jul 2026 15:09:41 +0200 Subject: [PATCH 27/49] feat(theme): user-selectable custom fonts (#19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a per-role font picker in Appearance settings. The two Material typeface roles can be set independently: brand (display/headline) and plain (title/body/label). Each can be the system default, a bundled font (Atkinson Hyperlegible, Lora, JetBrains Mono), or a .ttf/.otf the user loads from the device. The choice rebuilds the app-wide typography in MainActivity; a null/"system" choice leaves the Material default. Per the issue discussion the font-size and font-colour parts were dropped — Android's accessibility font scaling and the dynamic-colour theme already cover them. Custom files are copied into app-private storage and validated with android.graphics.fonts.Font.Builder before replacing the previous one, so a bad pick can't wedge global text rendering; a missing/unreadable file degrades to the system font. Each bundled font is previewed in its own face in the picker. OFL licences + attribution vendored under licenses/fonts/. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../jeanlucmakiola/calendula/MainActivity.kt | 14 ++ .../calendula/data/fonts/CustomFontStore.kt | 57 ++++++ .../calendula/data/prefs/SettingsPrefs.kt | 24 +++ .../calendula/domain/FontRole.kt | 8 + .../calendula/ui/settings/SettingsScreen.kt | 183 ++++++++++++++++++ .../ui/settings/SettingsViewModel.kt | 57 ++++++ .../calendula/ui/theme/Fonts.kt | 124 ++++++++++++ .../calendula/ui/theme/Theme.kt | 10 +- .../res/font/atkinson_hyperlegible_bold.ttf | Bin 0 -> 55256 bytes .../font/atkinson_hyperlegible_regular.ttf | Bin 0 -> 54348 bytes app/src/main/res/font/jetbrains_mono_bold.ttf | Bin 0 -> 274096 bytes .../main/res/font/jetbrains_mono_regular.ttf | Bin 0 -> 270224 bytes app/src/main/res/font/lora.ttf | Bin 0 -> 212196 bytes app/src/main/res/values-de/strings.xml | 9 + app/src/main/res/values/strings.xml | 9 + .../calendula/ui/theme/FontsTest.kt | 52 +++++ licenses/fonts/AtkinsonHyperlegible-OFL.txt | 92 +++++++++ licenses/fonts/JetBrainsMono-OFL.txt | 93 +++++++++ licenses/fonts/Lora-OFL.txt | 93 +++++++++ licenses/fonts/README.md | 17 ++ 20 files changed, 839 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/data/fonts/CustomFontStore.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/domain/FontRole.kt create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt create mode 100644 app/src/main/res/font/atkinson_hyperlegible_bold.ttf create mode 100644 app/src/main/res/font/atkinson_hyperlegible_regular.ttf create mode 100644 app/src/main/res/font/jetbrains_mono_bold.ttf create mode 100644 app/src/main/res/font/jetbrains_mono_regular.ttf create mode 100644 app/src/main/res/font/lora.ttf create mode 100644 app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt create mode 100644 licenses/fonts/AtkinsonHyperlegible-OFL.txt create mode 100644 licenses/fonts/JetBrainsMono-OFL.txt create mode 100644 licenses/fonts/Lora-OFL.txt create mode 100644 licenses/fonts/README.md diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index a2de42c..4565119 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -32,8 +32,11 @@ import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.crash.CrashReportActivity import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport +import de.jeanlucmakiola.calendula.domain.FontRole import de.jeanlucmakiola.calendula.ui.settings.SettingsViewModel import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme +import de.jeanlucmakiola.calendula.ui.theme.calendulaTypography +import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime @@ -97,9 +100,20 @@ class MainActivity : AppCompatActivity() { val use24Hour = remember(settings.timeFormat, context) { settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(context)) } + // The user's custom-font choice, resolved to a Material typography + // (issue #19). Recomputed only when a token changes; "system for both" + // returns the default scale untouched. + val fonts by settingsViewModel.fontState.collectAsStateWithLifecycle() + val typography = remember(fonts, context) { + calendulaTypography( + brand = resolveFontFamily(fonts.brand, FontRole.BRAND, context), + plain = resolveFontFamily(fonts.plain, FontRole.PLAIN, context), + ) + } CalendulaTheme( darkTheme = darkTheme, dynamicColor = settings.dynamicColor, + typography = typography, ) { CompositionLocalProvider( LocalUse24HourFormat provides use24Hour, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/fonts/CustomFontStore.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/fonts/CustomFontStore.kt new file mode 100644 index 0000000..d27ec99 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/fonts/CustomFontStore.kt @@ -0,0 +1,57 @@ +package de.jeanlucmakiola.calendula.data.fonts + +import android.content.Context +import android.net.Uri +import de.jeanlucmakiola.calendula.domain.FontRole +import java.io.File +import java.util.Locale +import android.graphics.fonts.Font as PlatformFont + +/** + * Storage for user-loaded custom fonts (issue #19). A font a user picks via the + * system file picker is copied into the app's private storage — one file per + * [FontRole] — so the selection survives even if the original is later moved or + * deleted, and never leaves the app. The chosen file is validated as a real font + * before it replaces the previous one, so a bad pick can't wedge the app-wide + * typography. + */ +object CustomFontStore { + + private fun dir(context: Context): File = File(context.filesDir, "fonts") + + /** The stored font file for [role] (may not exist yet). */ + fun file(context: Context, role: FontRole): File = + File(dir(context), "${role.name.lowercase(Locale.ROOT)}.ttf") + + fun exists(context: Context, role: FontRole): Boolean = + file(context, role).let { it.exists() && it.length() > 0 } + + /** + * Copy [uri] into per-role storage, first validating that it parses as a + * font. Returns true on success; on any failure the previous file is left + * untouched and false is returned (the caller keeps the old selection). + */ + fun import(context: Context, role: FontRole, uri: Uri): Boolean { + val target = file(context, role) + target.parentFile?.mkdirs() + val tmp = File(target.parentFile, "${role.name.lowercase(Locale.ROOT)}.tmp") + return try { + context.contentResolver.openInputStream(uri)?.use { input -> + tmp.outputStream().use { output -> input.copyTo(output) } + } ?: return false + // Font.Builder throws IOException for anything that isn't a valid + // font file (API 29+, which is our minSdk) — a cheap, reliable check. + PlatformFont.Builder(tmp).build() + if (target.exists() && !target.delete()) return false + tmp.renameTo(target) + } catch (_: Exception) { + tmp.delete() + false + } + } + + /** Forget the custom font for [role] (used when switching away from it). */ + fun clear(context: Context, role: FontRole) { + file(context, role).delete() + } +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 1a1f1d5..9937ca7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -12,6 +12,7 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange import de.jeanlucmakiola.calendula.ui.agenda.storageValue import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.datetime.DayOfWeek @@ -104,6 +105,27 @@ class SettingsPrefs @Inject constructor( store.edit { it[DYNAMIC_COLOR_KEY] = enabled } } + /** + * Custom-font tokens per Material typeface role (issue #19). Stored as opaque + * strings — "system", "custom", or a bundled font's token — resolved to a + * FontFamily at the theme layer; an unknown token degrades to the default. + */ + val brandFont: Flow = store.data.map { prefs -> + prefs[BRAND_FONT_KEY] ?: FONT_SYSTEM_TOKEN + } + + val plainFont: Flow = store.data.map { prefs -> + prefs[PLAIN_FONT_KEY] ?: FONT_SYSTEM_TOKEN + } + + suspend fun setBrandFont(token: String) { + store.edit { it[BRAND_FONT_KEY] = token } + } + + suspend fun setPlainFont(token: String) { + store.edit { it[PLAIN_FONT_KEY] = token } + } + suspend fun setWeekStart(pref: WeekStartPref) { store.edit { it[WEEK_START_KEY] = pref.storageValue() } } @@ -436,6 +458,8 @@ class SettingsPrefs @Inject constructor( companion object { internal val THEME_MODE_KEY = stringPreferencesKey("theme_mode") internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color") + internal val BRAND_FONT_KEY = stringPreferencesKey("brand_font") + internal val PLAIN_FONT_KEY = stringPreferencesKey("plain_font") internal val WEEK_START_KEY = stringPreferencesKey("week_start") internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range") internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range") diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/domain/FontRole.kt b/app/src/main/java/de/jeanlucmakiola/calendula/domain/FontRole.kt new file mode 100644 index 0000000..92ddc93 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/FontRole.kt @@ -0,0 +1,8 @@ +package de.jeanlucmakiola.calendula.domain + +/** + * The two Material 3 typeface roles a user can set independently (issue #19): + * [BRAND] drives the display/headline styles (expression), [PLAIN] the + * title/body/label styles (readability). Each defaults to the system typeface. + */ +enum class FontRole { BRAND, PLAIN } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index d3fc450..95870ca 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -7,9 +7,11 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.drawable.Icon +import android.net.Uri import android.os.Build import android.os.PowerManager import android.provider.Settings +import android.widget.Toast import androidx.annotation.RequiresApi import android.text.format.DateFormat import androidx.activity.compose.rememberLauncherForActivityResult @@ -51,6 +53,7 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Translate import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.filled.UploadFile import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -60,6 +63,7 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -74,6 +78,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat @@ -95,9 +100,11 @@ import de.jeanlucmakiola.calendula.qs.NewEventTileService import de.jeanlucmakiola.calendula.ui.crash.CrashReportDialog import de.jeanlucmakiola.calendula.ui.crash.openIssueTracker import de.jeanlucmakiola.calendula.ui.crash.submitCrashReport +import de.jeanlucmakiola.calendula.domain.FontRole import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker +import de.jeanlucmakiola.calendula.ui.common.FullScreenPicker import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold import de.jeanlucmakiola.calendula.ui.common.GroupedRow @@ -115,6 +122,10 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec import de.jeanlucmakiola.calendula.ui.common.currentLocale import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel +import de.jeanlucmakiola.calendula.ui.theme.BundledFont +import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN +import de.jeanlucmakiola.calendula.ui.theme.FONT_SYSTEM_TOKEN +import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalTime import java.time.format.TextStyle as JavaTextStyle @@ -477,6 +488,18 @@ private fun AppearanceScreen( var showAgendaScreenRange by remember { mutableStateOf(false) } var showAgendaWidgetRange by remember { mutableStateOf(false) } var showPastEvents by remember { mutableStateOf(false) } + var showBrandFont by remember { mutableStateOf(false) } + var showPlainFont by remember { mutableStateOf(false) } + + val fonts by viewModel.fontState.collectAsStateWithLifecycle() + // A picked file that didn't parse as a font: tell the user and keep the old choice. + val context = LocalContext.current + val importFailedMessage = stringResource(R.string.settings_font_import_failed) + LaunchedEffect(Unit) { + viewModel.fontImportFailed.collect { + Toast.makeText(context, importFailedMessage, Toast.LENGTH_LONG).show() + } + } CollapsingScaffold( title = stringResource(R.string.settings_section_appearance), @@ -513,6 +536,24 @@ private fun AppearanceScreen( Spacer(Modifier.height(16.dp)) + // Fonts — the two Material typeface roles, each independently choosable + // (issue #19). Headings = brand (display/headline); body = plain + // (title/body/label). Both default to the system typeface. + GroupedRow( + title = stringResource(R.string.settings_font_headings), + summary = fontLabel(fonts.brand), + position = Position.Top, + onClick = { showBrandFont = true }, + ) + GroupedRow( + title = stringResource(R.string.settings_font_body), + summary = fontLabel(fonts.plain), + position = Position.Bottom, + onClick = { showPlainFont = true }, + ) + + Spacer(Modifier.height(16.dp)) + // Calendar — view, week, and timeline formatting GroupedRow( title = stringResource(R.string.settings_default_view), @@ -604,6 +645,26 @@ private fun AppearanceScreen( onDismiss = { showTheme = false }, ) } + if (showBrandFont) { + FontPicker( + title = stringResource(R.string.settings_font_headings), + role = FontRole.BRAND, + selected = fonts.brand, + onSelect = { viewModel.setFont(FontRole.BRAND, it) }, + onImport = { viewModel.importCustomFont(FontRole.BRAND, it) }, + onDismiss = { showBrandFont = false }, + ) + } + if (showPlainFont) { + FontPicker( + title = stringResource(R.string.settings_font_body), + role = FontRole.PLAIN, + selected = fonts.plain, + onSelect = { viewModel.setFont(FontRole.PLAIN, it) }, + onImport = { viewModel.importCustomFont(FontRole.PLAIN, it) }, + onDismiss = { showPlainFont = false }, + ) + } if (showWeekStart) { OptionPicker( title = stringResource(R.string.settings_week_start), @@ -1180,6 +1241,128 @@ private fun themeLabel(mode: ThemeMode): String = stringResource( }, ) +/** The summary label for a stored font token (issue #19). */ +@Composable +private fun fontLabel(token: String): String = when (token) { + FONT_SYSTEM_TOKEN -> stringResource(R.string.settings_font_system) + FONT_CUSTOM_TOKEN -> stringResource(R.string.settings_font_custom_selected) + else -> BundledFont.fromToken(token)?.let { stringResource(it.labelRes) } + ?: stringResource(R.string.settings_font_system) +} + +/** + * Full-screen font chooser for one [FontRole]: the system default, each bundled + * font (previewed in its own face), and "Choose file…" which opens the system + * picker to load a .ttf/.otf. Selecting a system/bundled option applies at once; + * a file is validated and imported by the caller, switching to the custom font + * on success. + */ +@Composable +private fun FontPicker( + title: String, + role: FontRole, + selected: String, + onSelect: (String) -> Unit, + onImport: (Uri) -> Unit, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument(), + ) { uri -> + if (uri != null) { + onImport(uri) + onDismiss() + } + } + + // System default + the bundled fonts + the "Choose file…" row. + val rowCount = BundledFont.entries.size + 2 + val isCustom = selected == FONT_CUSTOM_TOKEN + + FullScreenPicker(title = title, onDismiss = onDismiss) { + FontOptionRow( + label = stringResource(R.string.settings_font_system), + preview = FontFamily.Default, + selected = selected == FONT_SYSTEM_TOKEN, + position = positionOf(0, rowCount), + onClick = { + onSelect(FONT_SYSTEM_TOKEN) + onDismiss() + }, + ) + BundledFont.entries.forEachIndexed { index, font -> + FontOptionRow( + label = stringResource(font.labelRes), + preview = font.family, + selected = selected == font.token, + position = positionOf(index + 1, rowCount), + onClick = { + onSelect(font.token) + onDismiss() + }, + ) + } + FontOptionRow( + label = if (isCustom) { + stringResource(R.string.settings_font_custom_selected) + } else { + stringResource(R.string.settings_font_choose_file) + }, + // A loaded font previews in its own face; otherwise show an upload cue. + preview = if (isCustom) resolveFontFamily(FONT_CUSTOM_TOKEN, role, context) else null, + leadingIcon = if (isCustom) null else Icons.Default.UploadFile, + selected = isCustom, + position = positionOf(rowCount - 1, rowCount), + onClick = { launcher.launch(arrayOf("*/*")) }, + ) + } +} + +/** + * One row in the [FontPicker]: the font's name, a leading "Ag" sample rendered in + * the option's own [preview] face (or an [leadingIcon] cue when there's nothing + * to preview), and a check when it's the current selection. + */ +@Composable +private fun FontOptionRow( + label: String, + preview: FontFamily?, + selected: Boolean, + position: Position, + onClick: () -> Unit, + leadingIcon: ImageVector? = null, +) { + GroupedRow( + title = label, + position = position, + selected = selected, + leading = { + if (preview != null) { + Text( + text = "Ag", + fontFamily = preview, + style = MaterialTheme.typography.titleLarge, + ) + } else if (leadingIcon != null) { + Icon(imageVector = leadingIcon, contentDescription = null) + } + }, + trailing = if (selected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } else { + null + }, + onClick = onClick, + ) +} + /** Picker options: "Follow system" first, then Monday…Sunday in ISO order. */ private val WEEK_START_OPTIONS: List = listOf(WeekStartPref.Auto) + DayOfWeek.entries.map { WeekStartPref.Day(it) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 557ab21..e2889e8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -1,6 +1,7 @@ package de.jeanlucmakiola.calendula.ui.settings import android.content.Context +import android.net.Uri import android.os.Build import androidx.glance.appwidget.GlanceAppWidgetManager import androidx.glance.appwidget.state.updateAppWidgetState @@ -10,6 +11,8 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository +import de.jeanlucmakiola.calendula.data.di.IoDispatcher +import de.jeanlucmakiola.calendula.data.fonts.CustomFontStore import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs @@ -18,14 +21,20 @@ import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventFormField +import de.jeanlucmakiola.calendula.domain.FontRole import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.storageValue import de.jeanlucmakiola.calendula.ui.common.CalendarView +import de.jeanlucmakiola.calendula.ui.theme.AppFontSettings +import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_PAST_DISPLAY_KEY import de.jeanlucmakiola.calendula.widget.agenda.AGENDA_RANGE_KEY import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget import de.jeanlucmakiola.calendula.widget.month.MonthWidget +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.catch @@ -35,12 +44,14 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( private val prefs: SettingsPrefs, repository: CalendarRepository, + @IoDispatcher private val io: CoroutineDispatcher, @ApplicationContext private val appContext: Context, ) : ViewModel() { @@ -131,6 +142,27 @@ class SettingsViewModel @Inject constructor( initialValue = SettingsUiState(dynamicColorAvailable = dynamicColorAvailable), ) + /** + * The app-wide custom-font choice (issue #19). Its own flow — both this + * screen and [MainActivity] (which builds the typography) read it + * independently of the main settings combine. + */ + val fontState: StateFlow = combine( + prefs.brandFont, + prefs.plainFont, + ) { brand, plain -> + AppFontSettings(brand = brand, plain = plain) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = AppFontSettings(), + ) + + // Emitted when a picked font file couldn't be read as a font; the screen + // surfaces it and the previous selection stays put. + private val _fontImportFailed = MutableSharedFlow(extraBufferCapacity = 1) + val fontImportFailed: SharedFlow = _fontImportFailed + // Serialises widget redraws so a rapid flip-and-flip-back can't run two // updateAll calls at once — concurrent calls coalesce around a stale read // and can leave the widget on the intermediate value. Held sequentially, @@ -174,6 +206,31 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { prefs.setDynamicColor(enabled) } } + fun setFont(role: FontRole, token: String) { + viewModelScope.launch { + when (role) { + FontRole.BRAND -> prefs.setBrandFont(token) + FontRole.PLAIN -> prefs.setPlainFont(token) + } + } + } + + /** + * Import a user-picked font file for [role]. Copies and validates it off the + * main thread; on success the role switches to the custom font, otherwise the + * previous choice is kept and [fontImportFailed] fires. + */ + fun importCustomFont(role: FontRole, uri: Uri) { + viewModelScope.launch { + val ok = withContext(io) { CustomFontStore.import(appContext, role, uri) } + if (ok) { + setFont(role, FONT_CUSTOM_TOKEN) + } else { + _fontImportFailed.emit(Unit) + } + } + } + fun setWeekStart(pref: WeekStartPref) { viewModelScope.launch { prefs.setWeekStart(pref) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt new file mode 100644 index 0000000..8b2b135 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt @@ -0,0 +1,124 @@ +package de.jeanlucmakiola.calendula.ui.theme + +import android.content.Context +import androidx.annotation.StringRes +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation +import androidx.compose.ui.text.font.FontWeight +import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.data.fonts.CustomFontStore +import de.jeanlucmakiola.calendula.domain.FontRole +import java.io.File + +/** Stored token for "use the device default typeface" — the default for both roles. */ +const val FONT_SYSTEM_TOKEN = "system" + +/** Stored token for "use the font the user loaded from a file" (per role). */ +const val FONT_CUSTOM_TOKEN = "custom" + +/** + * A font checked into the app that a user can pick without providing a file + * (issue #19). [token] is the stable value persisted in preferences; [family] is + * the resolved Compose family. Kept deliberately small and readability-focused. + */ +enum class BundledFont( + val token: String, + @StringRes val labelRes: Int, + val family: FontFamily, +) { + /** Purpose-built for low-vision legibility (Braille Institute). */ + AtkinsonHyperlegible( + token = "atkinson_hyperlegible", + labelRes = R.string.font_atkinson_hyperlegible, + family = FontFamily( + Font(R.font.atkinson_hyperlegible_regular, FontWeight.Normal), + Font(R.font.atkinson_hyperlegible_bold, FontWeight.Bold), + ), + ), + + /** A serif alternative; shipped as a variable font, weights via its wght axis. */ + Lora( + token = "lora", + labelRes = R.string.font_lora, + family = FontFamily( + Font(R.font.lora, FontWeight.Normal, variationSettings = weightAxis(400)), + Font(R.font.lora, FontWeight.Medium, variationSettings = weightAxis(500)), + Font(R.font.lora, FontWeight.Bold, variationSettings = weightAxis(700)), + ), + ), + + /** A monospace option. */ + JetBrainsMono( + token = "jetbrains_mono", + labelRes = R.string.font_jetbrains_mono, + family = FontFamily( + Font(R.font.jetbrains_mono_regular, FontWeight.Normal), + Font(R.font.jetbrains_mono_bold, FontWeight.Bold), + ), + ); + + companion object { + fun fromToken(token: String?): BundledFont? = entries.firstOrNull { it.token == token } + } +} + +private fun weightAxis(weight: Int) = FontVariation.Settings(FontVariation.weight(weight)) + +/** The two roles' current font tokens (see [FontRole]); the app-wide typeface choice. */ +data class AppFontSettings( + val brand: String = FONT_SYSTEM_TOKEN, + val plain: String = FONT_SYSTEM_TOKEN, +) + +/** + * Resolve a stored [token] for [role] to a Compose [FontFamily], or null to mean + * "leave the Material default" (the system typeface). A custom token whose file + * is missing or unreadable also resolves to null, so a deleted file degrades to + * the system font rather than crashing text layout. + */ +fun resolveFontFamily(token: String, role: FontRole, context: Context): FontFamily? = when (token) { + FONT_SYSTEM_TOKEN -> null + FONT_CUSTOM_TOKEN -> customFontFamily(CustomFontStore.file(context, role)) + else -> BundledFont.fromToken(token)?.family +} + +private fun customFontFamily(file: File): FontFamily? = + if (file.exists() && file.length() > 0) { + runCatching { FontFamily(Font(file)) }.getOrNull() + } else { + null + } + +/** + * Build the app typography from the chosen [brand] and [plain] families. A null + * family leaves that role's Material default (system typeface) in place, so the + * common "system for both" case returns [CalendulaTypography] untouched. Brand + * drives display + headline; plain drives title + body + label (M3's two + * typeface roles). + */ +fun calendulaTypography(brand: FontFamily?, plain: FontFamily?): Typography { + if (brand == null && plain == null) return CalendulaTypography + val base = CalendulaTypography + fun TextStyle.withFamily(family: FontFamily?): TextStyle = + if (family != null) copy(fontFamily = family) else this + return base.copy( + displayLarge = base.displayLarge.withFamily(brand), + displayMedium = base.displayMedium.withFamily(brand), + displaySmall = base.displaySmall.withFamily(brand), + headlineLarge = base.headlineLarge.withFamily(brand), + headlineMedium = base.headlineMedium.withFamily(brand), + headlineSmall = base.headlineSmall.withFamily(brand), + titleLarge = base.titleLarge.withFamily(plain), + titleMedium = base.titleMedium.withFamily(plain), + titleSmall = base.titleSmall.withFamily(plain), + bodyLarge = base.bodyLarge.withFamily(plain), + bodyMedium = base.bodyMedium.withFamily(plain), + bodySmall = base.bodySmall.withFamily(plain), + labelLarge = base.labelLarge.withFamily(plain), + labelMedium = base.labelMedium.withFamily(plain), + labelSmall = base.labelSmall.withFamily(plain), + ) +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt index 8b42f63..652aa77 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MotionScheme +import androidx.compose.material3.Typography import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable @@ -15,15 +16,18 @@ import androidx.compose.ui.platform.LocalContext * - System light/dark. * - Dynamic Color on API 31+, else falls back to the hand-tuned scheme * derived from [CalendulaSeed]. + * - A user-chosen [typography] (custom fonts, issue #19), defaulting to the + * Material scale on the system typeface. * - * The Settings screen (later) can override useDynamicColor and themePreference, - * but the V1 foundation just follows the system. + * The Settings screen overrides darkTheme, dynamicColor, and typography; the + * bare defaults (used by the crash screen) just follow the system. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun CalendulaTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, + typography: Typography = CalendulaTypography, content: @Composable () -> Unit, ) { val colorScheme = when { @@ -42,7 +46,7 @@ fun CalendulaTheme( // the bouncy variant felt overdone in review (2026-06-11). MaterialExpressiveTheme( colorScheme = colorScheme, - typography = CalendulaTypography, + typography = typography, motionScheme = MotionScheme.standard(), content = content, ) diff --git a/app/src/main/res/font/atkinson_hyperlegible_bold.ttf b/app/src/main/res/font/atkinson_hyperlegible_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ca3f613847de62b8da83d2dab500ec391c8fe477 GIT binary patch literal 55256 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 Ca=@Jc literal 0 HcmV?d00001 diff --git a/app/src/main/res/font/atkinson_hyperlegible_regular.ttf b/app/src/main/res/font/atkinson_hyperlegible_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b3cc4200fcf09d600eac74002ab4ea65b404a536 GIT binary patch literal 54348 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 N)RR(%LbP=Ae*iZmk*xp# literal 0 HcmV?d00001 diff --git a/app/src/main/res/font/jetbrains_mono_bold.ttf b/app/src/main/res/font/jetbrains_mono_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..cd1bee0704e05a899cd70f0640a801f1afe046b2 GIT binary patch literal 274096 zcmcG%3!K%{{{R18pU+yKru)5y$+lFiK_$gzjO^uZW)Xf$t8#Vs?>G4(-4VsEft)P7AsnbrIGVaLm zGLbRZuUF@^vFA=BzXAPCMzc^E#*fXq_x1Ng&Z0PH@|3YNrq%zTNipR!$cRrJ zJ7wbS4?n$F!jmV6xT(|5I(K^c$u$ZjtlB6tc;U2jCQh6C?xL8;lv&jOgXr7@$2QsV z>|x)GsbBJ~)DL%%BU@@c`<3d}kH#+SWV4uB}x{(B_7IY^P?%j{gpyG$p>XU!gC;G^&lBsgBwhR`8-^ulORaLK!gp}H#B4{TL8*>Aly+K~hXz6H5$rY*T zZ7sGzbtC!VCvr+O=a{m1Toy~+>T2(Uz23}^i@Eo+KhmmY@DWww5@GWsNcJ)32g^ArOeV$$ zYi8`)UyuJo4#T#h{bknvFSS2q9YKBR4E_Fxs2$xW)Q+Y*q*c?uhWf^ltNJZ8v!4IE zdJJuK|2eqbFt(KwASaE+Hj}QAP9KS;<5B&$f29ws+5hQ${8oFNi%fj9j{25~lg4E1 zK^#i@=-)v1lUv}|j;+6p_5b#EKK&lNt}|)P|9?S`2?wKNDgF(9ZJ#Tm?r(9>yjTCk z`uY__W3R`_Uow)s4C+sxU-17jw*Sq!*0Jk;q`o`?kAucr=T6s5=9sUl=LM~wIj^yA zNh`?gdzpQtA$fX!(DquEv8CnhAhVwI_^ro2EiVC`Gex%*(y6)KLt0~@$8xoybu;HQ z^-2BEc5Tz?-l)bX6IVS34Fz52zw}xCQ+t}P{$_m6_^A1M9M%0s%Pvl%^U)HtkIZ@q zNo!vjJ9>Ojnn7khv~H$cx|*k|XnsaD zt#<0C^R-^4ylpzIYf1gm?My$K*1Dxo4#PqBy`E`w9d&^|Y1OpW)3|9nt*bt2`U|r>YptI)bH~QI`00MOM+Fu8e)Q>#?$07Wq7M77vNvSA z2Wb2IfO*<~B&?%s223P<0TAQ;Z?gR|l#qS|jibacRjzfGlSlsk)#w=d=J=iR<7)C0 zZ2a^+%JJZ*g_M5|^t10Bcsq@zGwh(=eEekXWa|8M4(aXW&%;iDG8g-60Y3jk{k@j- z$)3G=zOMGW*D*&re=ns~=dlxPN^>r%^Qd(m1ICt`>ubI)pU7rH%!XPHVk~LHp2SUnOY&YCE$%mGo_(alHk!T$v17Phn0|wx^)#=WrMa zBj7dAJXOtW3fZ8(D8xC{|6fq`OZ(Ef&@pj5*!LE^!W>RR-(~(-t3RzrHJ-XIe#acL zU+hf=6+b?kPOm+v-U`w>_lMH%2W)&qx;1Iyqh!{Cu7}K8)bvl+vi&vsB=yI_EMOd| z?Od;#-b-xu?P7c3&)LsD@DqOgP)hlqNfW#M*xWk_KhI)&3;BD{RllgCeWYz^+e}*b zq5T}cGTT4x^yyUJ*w+00tSimeeQDoj*iHHr@>r|;+XM5E>ho!IBl$DY57K#yneQiH z76{N^3f73Osb3=%zXbI%#C$rn29F>;6x3D*{7)Tg*{4(I2VF0^2547}->Ez;%hdnh zR_*K9w0({zseR;8RQ>1(I_6*VTm2aDuQ`y$Y##z$pa~R0&9t&MQs>rUvt4$g+UoU1s!#P>Vg096 z)9SPOqW)^W&Uq$J^VJ{CQ`EmqTFWx+dXk<8mB8^SRqx=snKtT|+SWR%<-oc~`KJ4U zqCP3=yN+Myvm5Ao*7yv8*^n7?M%$97(60I)=((^MI;7G5)F~0ftE1`n*L9u*0}EX zO+Z!Gyza}oKCoN8B%P<*zsA&bzN+?_sZ;IK8D9>lu05@%{iW+y@A7rUr4<|1o6=08mCNJRoefned@a4wu8108b_)w+dpf2 z(6;(;aGe9T)Sm7qsu|cma7-gFlUB{t$>izwuccFU*HIrb$I%=cf*X#dRHZp$vQq`vBWi(Tsx);*^(&fKeNBv6IN%@sYA2iNP`2o8J*~{ep zmL5FMnL585=g;yFvi&o^502L_d^^zoN0I$Hw`<@-n8G%nv+chEy1}K8CsI8UZJ4G2 z9SW_|@EP1bJ`>tMm3nniZF>To0(>5~pSJtv!t10PL+(MSt&H7_YP(bIo+ zp=mPdU-M7P)W^e6^-=rKzW9(&ecZ3Uv`WpvM{*;d+Fc~4%PhD=mZGy{tvt^B{HyGA zxinpJo3xYL5lDX<}x$i+-&YL_nD;GY`!#qHQ$=*ASdV)bPFnjlY{4j)xn#=Tfry6KZEZ> z8`cTyhmFFVFcG#2i^KE6OT)S0yl{SaU-(Fv4Brah5C3TE*h6i?_ON~Iadwm)V{fr{ z*$3=$`>cJz{>gr3|6;$b^;PVu*gdfy;wD}@-XPv2-YlLM&yTl^w~u#@_lWn7_lr-9 zUl_kAJ~Mu8d|CX7_{#W;@t1Rh+`74qbDQNJntNDoUT%JFyWG;;>vHeNy(f2Z?gP0G z=RTVIcx{W$lt+&|~;$o(p>PF~}@rg?|v#qtXB+T?Z5E6MAX*C(%U-oU&; zc|-C>|G53t)hAb< zQoXObnh4hAQ_uF&Ll(;u=EhWX7Mmx{yXFh?mDz3f2H8Q!bab90I&To2O{wTe7#)aC zLD)5%PIP7yog2cN!Uw`f!{y=I;fL1PdN#-A+n%=E9&bn6`SwnGzkR|!W1qL1>}I>& z?j}03h|c14bQ%+#>~wTG#=FN$;^jnVdMY}z;*b3zI!$vAIVd`FbLZ#YO?2+heK7Zt zbaXZloll9*mqbVM>Jgo6qLaug%9|2nz)we+(mSL%9s2^ffU?Yu(;sSf)xet6>KCrUmg^l#RsA@mFO%g ze4XfgQTS~tI@b^#M|2i%Ngf=Xur24l<-{oU7nD?Aok{;o_ix+3WdCAzN{$}kH|%vi zO|h1S=Y-S3N#WSADjY5+SHH}$;BT;g|2zDFAW|O{X+Zlgug3i}gTc6ze#ix-d_LnWuHH^?Lu1K|M}u= z<3GP|Th;a>d=K0HvTetY1itv6wICQOwcPhA>5-@!|%d9;g2?(<53G6W6ihLTq479 zIs>U9+u7&;>tEZR{2r;g{L7wltQ}@2k>YG+m)e)?M*9c$l~3&!`-Rx8Vi z8H@j}S?6e8*7Scb`DISng=5hA@NWA%*T>EZ-whuNmxZ^xczAvI5J#xR9HVY@g)ZN< z3~#ZQ5z9j*M{=crvs4c`TFPa(jFc1Q6d5fO>B8VGcJfO`&OL2AV-;uo-G9%qiwPbH2IQ%rsYp zkK1d*CtO>On>X9{iS7oDhI8!|(Mo%z`F;4NE3&hrm+jTAb(kCGMbEmP_VV!7XiXS5 zAB5||8XSAG!UfWZue;Qe=6qAAnPf{$I!a49g0tRK~g=9pnJ-<&Krnv>)PQz?&_>GF^{TNdzSe5pBC9yHVB zVRMc=&iACAGMCCqdBI#QFPb^B+FWB!lw#>B=a>??*POw(j^2}jrmmc3I>@c2O71eJ z%0e?AnQ8jTb!Md8WJbvr56^8;VY+GhU7 zS$?DW(EP#t(R|ER*{9|+v&DRFwwkZa-_0)bjroW9r}^G|$FsEo20gE;CklwQcpgR`m#~#$nUsXeT(mry~CC0 zddZS^`R3T$(m}qHPO?Y3$zEwKf0rWpM%v0hxa$3*VTS!|K^(6uiYfq)b4WK>^4{Ia-DS*_A}Srerf;WO59Yt*$r|7Ij`(? z^<5j+#2w=b>_6P0ZlXKFwQ`Yj_7m6K<+yCu!X4rQ`@QS$PICj?sqPFHbBDXZZis8( z8oDf3<;J2TwPa> zv)ZS2zZ+)%?m~AqXSZecR=3E$96lR9AFc{tuy@#n_BMOFyPNC!CvAIsraj4?$eDN) z=gDH*)t+HH*>S1s%aiR8JIS76$J)-EM~||VoUQxV>2|z5*Ou4`c7#30_O%uERNKpz z+CjFvon|N6Zg#jmn`_j`_88m6mf68}s_oCYzPCNkPO+zRS5ak;wrAM^_B77T9c@25 zkh_fz_A$HI-t2n2qg*dn=1Ses&e&=vZloLTM%aCJul>nxcgMS-?l`V@U$8H7E`QFx zZr|V<_yPN%y@c!GOYNgv4X@`szrem`U*-DvZhNo2$1dUyW}RKa{mgy#P5Y32*gj&{ z*mvw&?r(0gtD^2vnqEn*0sB2UZ4T>yh@?$w~H;DQ~ zEu+EFsOW^~xaf$eMbtkkjD|#KL?fdxIxT7w9nZP`n5brSax^q*9py%yqPkJ%sGj?$ z`#!4U_PKvVwcTF#O*B3l;l7K;MZ?{<(OJ={?#F0qG|~MK)p9?%UD4QRnA;sqi6*!` zQQN49>xCnuj!}oGebgg5I_ed5iJC+?QFc^6>KTd(16! zPrH@wDYwEccS-l8d&a%$UUO^R6K=EH;&yO#@`?N0{l)#wZFgU|Pu*7cmHXO#=C--N zy1%bGUVY5zxg z9KMQPyMa?5dwW8@gDj;UuB3#kCyznPK*w|_l&8_T>6=E!-4Apw@k0irQ5yr(XuD(5 z3`Ym0;rT}DX~bakL>L0c0?$80?+la)Ff@(&c03G&%Ry}^SHp-jI&W%Mxfyuold}CN zS^<@ySHvf!c^*C46F!NK@`U%H>IZ~cf3(MG9L9J;wOQri3X(64r+EV%2jk&0&@m`q zz{E6Pp{K%WupcIQOoX29F*VUMJmwJeOi$|j;*&{(IUJn=Q%Sc(&+-`cbDGC!9L@%= zy^YTOxgMkQs#oG*bZ*Y`7#;WdZ~^(6Hv=vteKC5G$6ST#HiVC(Gd=cN^b$|_1bV5* zwM7?u!mg;!FN7MS`#tu3^Z}3k1G>axH=qxC>?ZUfkDZG??6Fs%OFhv_^bwD}64f|? z`91oWCwvoK=5a-+`U-Y7s_Ouvmr;#9*sIay9@iRO0Z-w7E~@=O^eps0{0UlYtl4AUr7_7 zucoPmzLusrx;9M%^z}5&&^OX#qdH$o4Adv3BfOQSCHi)nBhYu!9Eq+^b1eF9n*Qi} zpfNuNG{#Ck=9!8|mem+m5bF?*$j%`~SwYxoy`upcJ+UFN(w9hZo zXuBP0zDNI(=C9~iX}(5vTiFdeJ*E-*H;<`}YQ4X+uIiz?(x|;}JVw|5KRl)f`cIG1 z*r{DG9no(+Mq~G#C-t04{R5Xk_jpWS^aqd882#ws7ak%%!Cq{RMfZ8kCFp*S(e+U6 z;r>(~)1=@|Rk+_wp)urMECqM3!o6q;U6iF6Mt>MwQao?JP zJ73XbZtA#8thjGYA#s#>OwA>6{9 z##ZB$idAQ{u}9~Od*>8-?ZEwW3XLuI)hYCx&;4`?jfr9ZRJb!2Jx`?070kCe)T7rO z+n?~nV#|e5J!QH$^=RV(~*A3j+ zdvuODMx@SBWvGr#sR8OA=y{!ce~*r}wMWnYJQ?ul+^}9#YjyzIHjT!loky<`bnT|l z`P2CVz1HBq-lMT)O{dm%U$j#iomY(q=rslRiyoc7t{%On;Qmo`?Wg9hI|_Pzq5DsY zINH;r*BU%u@aQ}p<&nG-OfBijDh3ecXI>!Uk=-eOU(Q6mIR!E_9taAsc>wqDo74836k6z30Y{sMW ztuX_=cHmi!M}5~AgI*W#?8c*h=v;$biH=O8edt_+T!o&PM*GpZ2APfOJSyso&Naw2 zsLrFJ{p;L;%tcR0qkWF{$hD}>qoRIQd1MZ%^Qfrr<2*7S)j3kMj|m<=qZ6JJdDIu3 z7myp!)6%G~lRSEz!m|d?bo305UgPlm!E-h`*&_?kDQVQ#sUE%d;TeVJTy&a8ubX%l z=Fz-!JbK;4voVk6Y2U!7iK%Noz3$_AnV9oEdac8A8jr?K%RsMtcy^OQuUUAWh%kU1@UB#h%nP?tP?{ z5_r-h_oB%(I>+yNc;Y8UZGm2onD;$;?_xHjsf+&3qu1+t9hss7`hiDoMK`9YLO=9~ z`uK-5x)%QEk%j1{G`fcVo%TMB468p6F@xBTw`q`mrZ^1>Nk4UP3?dMC$)PJ+>YCy~iHOH8=AZ8tk*Lgv3CJ zEUi7rO#d!ozHMxN*j zG{+M$&tbw7%|;8L751+}*_Xm%($}KexB9|7hw7_x6lfcWIDUlM2BLG(t383Xxh72s z%Dx@WWnOe1=6Qk((Hr1K@^wzwkCk8%%39I;?BEpi0eFP;RP<4JjI_?xGI)aY{pgdP z;7*i%Sqa#$!{zW6X`P3+J;7b*JD%VkbiF57jIwT&;BNFiPkOPnn+*57tp;;UdvEb&#$ zHng6{{0*(|(f!yqfX39>h&F+yq(4TR!J(u-MOgzjL3%ry4@Z#x8g1<{tY6#4W4=R+ zJbG-fZ9RHy(6z2>JkW8s2i=FW(az9?^x zF+iYmyx9}5{;aNP2$*-PYX}0?qW#Ph3`Mtlg1YFRJ;Cwl7oMOV`lTn(Sncox_0hk0 z0*%{lPtX9>H3tDPto4;A*dh{}B*dqY$H%^w~#D#{k0miRt)3pS8qT3yQE#Vyp*6pS8qR zc!YHld&;BFU1BRed}=1Kr#<=%CdT?v_#{nYtRF?6-^5mV_;gKT&w2D-KgPOJ_{2?O ztSd$D^<%3&dh{sbH`rs=yQnJs~&yk9ed5A&m>}N zJ<<++-J{PZVsCi#xq0kOk3QRot@G%!b!!+JmDJja8Jm3kHxNs288*Ny-t2Lg zqPKV)^O3s%Zl(Tgl(;JHO7wP*y98yf6?YkWhsRxx-tBQ$qpSnP%|RD?9CMv}KRiIb z`uiZTW}VK@Bk(9`_4jd)W1Zx_?0v3q5*0k!L+_3hF$rJsNpj zSG0!5eurjx?BCFu9@h-5<*{F*wLNYUTF2v9PkHq`b{AUT<2Y92HSpMNXhV-HMjLrt zF51}R)L*p)dfk+l?Qs<->r>Hdu{_qP;<}?Tk2?}gc=Y-!FW;lrTzLf^$3C4`2yJNF z0qyK@Q_(ISy&lZdc>y;F)iHn@h^pUUiAkRJ4SF4yr*^?rqP;wJ58B6LiG5yQkKKy) z_c)!W0Un1>dD;iKCaCre?kseO$Nm!?;jvrL6Fp9KjK>{=R(YI`d#uO)10CmaI)CFm zZX!CtQbh1aUmGg9MfoqQHJc7$X)o*awsIFIVEzq+)?hy1G zkJI&guE&0lUgU8)PZxXKY3NLk(>c4s<4#4d^f>i*He5wtx-QuN^R6L17@Y&xlGgQj zgU4l|H+r1T*?f=FdAiBtbpCGkIGwXwU;%bcLT~jr_3<{io%|W-LXW!uy~E>lOm}(Q zbaau&osV+7QrvmyJsvk2z1QPTK^J?R#`!*v8-?EQaVMY;c$~&(iN|T&9`rb!@0A{> z``jvztAW1YaTz2R}Xj@Ehf-YM@bkKK>H?Qz4< z_dI(4l()fS|B7z)xWmw|J@)Tt8W)n@<#D=>zVXnD18^qxLZ;?ZXY ziK9GLZS?Y3?WecLvhO5HJ^G9zakR&dL(4q+93s)jqt6Kv}DlyQb&ruS`c=UNmVvxtGzk@yc+#;cNL7#0Tj`iqsmc(%$%RZGD>apWd z^%X4ZIiceRtMf73V<(^^Jo@Y;p?1NZgX%nj)iLS(f~`Q)*i%WXPhfQ}H6CDf{&X%t zpYbF#24FQ-I)1RzP@Oxl6H)eO#cG_ZJo;QGp>qWGY*b?a`fMqoV*@)G)iHoRTS}-c z&}UK!^$)DZ?KF=*XG&;1z)nR^_gIaO#vb$;RzlYqSX~#|2iWt_DITk;?ZK*zvw$(_ zv!aCh3ifDJ=Mk)yYYf0@{54Ks)i1RR`m8H)zQ=Y%FYwrY=nRk5^>U#{pNl0f^4JdO zwI2HzI@eMwWN08M|+|WE%QXDq3SC{I#1=E=y+7u5JVc2 zexB$URQragCaQ6Q=wx)DCmM=sJ&0PPI;Rk6{B`^g>71#r5YP9f6y)A2)84^>;> z{)wtj;J!zPd7?V#aF5%Ej_|mDpgLxVYNI;8;P#@rhQNJ;>iULgJgRFKBDJCW3Apc2 z9UDaBP>mTx+NZ89aNnY%J<(aHu4#x)MXNmSM|7+wnu@9|h$f=iH@F|r37)7Hs%7AQ zLe&-oCdWI*Of}ZJ#)b?bL+k4K-F(>J5lWq+$X5o1@}3+%H#fmKId_NL!bA!?dS_0_XWDz<32@S^ti3)OCI+X z`m)D;jjr*y&(K#qZX2p&2KQI=HIMr{y4K_VjK1!1U!rfoI_kfVzU6W2(YHPB_vkww zr**XqoaVjfaqptq9-P|V2+XC!Zh?*soYvL;@XP%XRiD7UiGJsC^x1NXC;T0HmM6rA zme+Xf`6#h#N$l(e{G6h($FhD4n|dtkx3Ia#vVIG@dMxX=aH=OHmW9|?LgH7*ex`)P zyYOsJNQ?^?c-%ecB0wE$sE~0gA+arFUX+j+7H;;~A5q3#$auqE=(q4aZSUtNAI&`O zSo9c={Q+f+N3f5C#J{z8TuszLMB7^YETqC?>1zw~umyY31~duy9(};qX2*G4U$oj2 zcIPX!CwoHnqv}&UA#1&QpU1w!*UlQGkl-=CvQ!MT3znfhfpG*+qNPwydL`N~g?=B$ ze0U5DCI2OK7@SG^jieOClQOcRZ)H3_XqD7Eeo!(Reo{rUb562VW!0(ixg#r*LBZJP zviL}H{P@JUoZQ@`R3@cQqI`uI=~GqKHff5I@v2kXCWE3xZX&mBGAxQucq(ktRLc4! z8~2G zpT6V!CZoO;xyi7ga@ffgRL+@O5l;>sN>*uQPCQwxsp87Y_;Ozxk6MwH-ijyNYk7OE zxn^iZoY|N=HlD0Cw4#cfxR%w@R98)Pt;(sYtgOsowvu)Fj8Dq2ilhwE%DHUk3`)i{ z6&o~mRRbBXHC8zpS6Mk>Y-Q54sjN(opfWyz@g&MB+a_I6yl*^d3&t|08hwUVBx@wf zl39r|B0!U>w#g_p-ONOM!txs9%Hmq6!_D#T>%U1?)pvZ-wa%rmPke5CE`2R;?+Tcq z<14C$=8PRyS&^vBt&As2hgVRRqw|t>rERiCQL<*AHY+6XVa{SRQI=r2B+AAngK?)O z&3GJ1)@a=}S+gjv-qvGSR>n~Ue@m+>wMtca%G+8+D{9u0K7Gqt=N?!%wTphfkm`6V zrVTFiVdz!yzH<{}HK?h{mK+U8GMXS5M%aRS-=q|_7DqCJ(|7pNKNz;s2gatz@mg|1Vbd}8| z!gSl9b#8)YnKbJ`%e4u6vX)jdlK%MLpWL4Z>o-EkA*peKN$!)BUMq|-slBjikt~;> z?}&1j&3vmKQnWl&J+x@KRm~|{?o?Y8EssI;gPOfx@$WVcUZWTOKGp#}~$nBw4@BZ;q2eJ?)38<2-nL zxr{H4UwMpAHNp5)^BJFN0pn9`$@o+Y8J}t^#;1A&<5O+T_*B~!#Y<8<(vd~+s$_Om zoP(aJ(ls#_O}5iD+rB7yWSiuX?2jGT75cMce|3jUj4e*+f&D+$U^%u;cFcr$dA+Ew zuDE1}*2|q~+P8v(qDHaPLDTbFbvhTtyQDntf;Dfa@4p`=IQsmmx0)}_xE|oIyjP-l zd1up9$J&+oU;w|`riz_qY;oIUx1x5qYN7`B(wCNuY68)-@5z+qn?#iBJ z+W51Q?Nc>@osW|$2RqwmY!2zFN{$b|Y%>O{!D=_BdC`eLz^Wac7{2d-s$E)pe7(@_VClC?OP#N+)E z{k2n#Vy{eDYA}ALlQN>BUA%-7x_Te@OpG1~MKUU2Yam4ju3dZ(|9$0ECDPI2$lo^E zJJVZg_S502(hHnl4mlIzQjQ<(bQb$1oA;?0n!`CYUQ*d^d3)1@{r2del@7}p`m@rq zpOt3X9&GQIE&3ECd$c*YLZ(rs!}6kJ&o*;ee!9lyvZw!+Zv?bmvOO;LO$}L>aZ5k* zW4S;p^F!C=oM2CD$DZd0)URlHEzUK%`co_azqh3O|Jnlk|Fep9zV$d(k|@r}J!rk; zR;K;z&jGkco6M{XV5?`FT#cE|OL{a1reGjb(A4ktT%E9oH*S~g%3glVZ{!ccoN3ZH z*^N4bi;~@uAv%kFneKQ$PQsblI<`m`baDvOd|VM%^ZiH+C1Esie9;P%${9u?l`~xH z^rvWq*3raBt)qz(w2mfDEaFZ=`jDz1#krkSWzkc{=bc2#=bfzejFyhlda2YYS}&Cv zt@To=G1_Nes#R&9ni#8nYGR!BsfqDgX8?%_T1OKTwT>oE)jFCuE#+4^sYxlnG>dQD}7Qc%%*Pd1NN>;ZKdp8?UP#l8lTka zb72$wZdGV)C$)Xt*D*4vQ0)gD{K(<<)(>qS!G$9WY)xFm_Kwz=7io>xB*-@ zjLS)L1Lrz_%QtPg)F|T){OAt3PFm|mX`N$v8AsQwmRZ>~T&;B0Fy125i#JFd?owHG z%3f-`rs0~pYih2EO2tWSsx>ToNlFj=r+Lpw$T@v^zL|48cMEeWbbCVi@>aUNDocEZ zlvm~~FVw8(vu260rE|uQNY$i3|4Qo&}4(u`IKvb zI>TVVMlcf=z%rg$41g-&nPRwyXEP0e_KxQPZW7Gm*~CKNIYq>Cp&G?NS&c2c{c9S~ zPu6zW%m3=F2^2v&&`(YJsYyRI>8Ive*bKY*UU(hIhZ4YMZS2>^ex2E{iLYYQt}gBB z(ylJ;>e8+*?f8GrNxdqdZ{BMu^#{Wakp^pkb`5rkG|YlH6vI%M1hZfv&_|>8FaWUK z2-}VEqcMIoo&ob=DPXVhc9ACKuv$3$1NNF?uPOGLVy~HnIj|U3!3NkN!s9DxPW|T8 zZ%+N@)NfAx<_jSS>qN4r!#r35*w5YwJ9!ZmHVK#e@BWJ<_SO#lhvq%TV-=RGW;ME9AU^QQt zpCi&a4#hB(pJ-14#@B`Mb;VA%QT)Vlgh=;RumN_6^bo*S4{Y@q0JP~rn;x|3k%V=y z1@?&aYyhpGRHUQ{>eBnhx^-zk1D!|8q^I<7aZy@ysQty}s&dO~444l~VGV48T_Tk&U;s=9+EuQC4X}e((#(cM zuoBh-_D|X?a&i+Wg4M7Qc8ZKDht05CaK70dv98I3Qa@n!TT zkuj8yp?nPGV<;ap2e3T`+f^1?Kq)NaXXFz^#$kILb;nNw>Q1EpiKAdT%!4Jc8n7`D z8>fgM}7A-Wh{o0<4A2fQ>WRp4O~JP*b9nU!eNLs%sRMvMr_O-+uoTwtQ;#Kp4`<=)S@bi_0)9-x zk7@XEb`g}rDA*x#&Is5baxP4#pXrleJM89X7qmH#Hs`J4rxs0M0bu`p>|cP58RX5N z{~7c@V=++g!V;j(h1kDvG9&@NF2w$YTX@9|g*-DXFl+POKUyF44VicQ~uY*k@S5)w``p&SLpUczdZ2Gy1?W^$bD*U-RA7=5B z`63|i8tPm#9q4;bE5P=gH9)&LJ4CJ>0qbEqKc`;=%S5iD@9TDn%xez=V5P|Q4FDV0 zQ|EfNZ=mc({GX3M^QQqe=40cg$$;M68R-A!Ij|6x!cN#La!V7S{wC56 z)O%ntBzdjTP^f}wuogDLc3yE*0h3?`;Ln4M{lUGw?x+s5fFhu;hbey;dk<@R4Qzs4 zy#6Q);!q6qv2+WN|Hw>Ue>8wsA7SG$(#x>(_-r)l>zww_+b&u%J!yk}_p%wkvt8(@dXvjW+?_NWfz zLkX;dP5jJe0W9IQN0h%nd5VQ1tMTQ<&ajc!9F+roygXB6jRpKzvq$8Wd4RvK(&uYg zu$EUDP2d$q*nb0CZ<oQLvfU6%FO*bFE+rKXXIhM&H@V>xde_YLWM-^L|ofLn*Hw zVlIA9{s*wJ1(5$C`iIH9Qi$?D()XX5@aiD^`;E#k>-z99E0`ne-Rb`C>b-0Gh$8e@OpDU<~_V?i99{S!x-#^gz55=%XNHx|(*?i+Q$TVFaue6J*0I*eNC?FWe=@4u%b4++>(7CfXw=YZIU~*TEJswaQ_V znA)`E9>mmM1)E_vuLa76_JF-QbAUeUQC@!$tby&YmsbMCp%^M)BR@sN7WW#4dkxc& zej4uJXN&WIwvF+pF}9mF5YucKul=F?kZduBF5&evt;EEpi;2@`-Xt*z{K#J~reKel zmMvf?V55-!3b9v6n^v@GH2@aE4lzeihtHu+Yqr~L5mQ8Y(F~x^wzI{wD}qg8j-*Zd znPNIr0OcLlis?83&`$K#X);j1^IkDs7*m(+V!Gx7^}Eg&(=7|=r`u9kCx%d);$oNv zjK6qI>Zg~cdkMNrOpk?tjh^kH6o$flz;;Ok7z}Keuzl2Gpl+{Luv1KL?DocH?_FX_ zvtR+NBNmDoSp@id0)3n~R7}Mr zF_lSPU6Tbf#c;i6PTnkL)I45S(+cowG-ab{I~u#AxA3Z(0f3Dv@~h^E85@UESSDs1 zeT|z1yTpvIg4uxmiPW7)y@}MDxRzJbOb~M#+ozE}y^feOCW$$74UYvzh?#wFXsGJVrF1x#zrw0P7`wxb}wEjhHDpd zDg9rDtyx84E^h*R#axN~*)7CeMgG;;xte;{ECp=OVVmm?bL|{4b4LMrb2o~)4j<;x z&-Js#+|UX(i@C7`XghzUn42a6{@jA?TPDCv*ezxO?Qe|(+qaQ_o9a9wg zqXp34ot(OVwNlt z^B}%Hgr5)X5yLs#EG>t1Vjf`*9-+>o*nD)On8)bvG3qTtA8!SNVS<<^NI$_GJh>2d ziAmzma{61rI99BJy<(oC&Qn!Dou{z15`B7;m}gdtd3Lv$RU^baN8J~ih*?d$)jP$! zI2{(iTENCj)Zsj9UdGPL8^x@dFXok`m{(`Ob}_GEbL}iKuPfMneUF$oN`Ue=*nSgx z>ly%KdTXYbw`u#%JTdDh@wy%Ay_W^pc%S+kR*Cr?egB?5KEU^l^to{t-yxyjKNP`c zUX?Qn&`t2CQdlGAqjE7He5M zrR{euV3U~dD}XwC7Kr&_sF)wy1NlGU$4{$)^xpYm_R(fPx_^h5YT8y$hQ+V}wn!i) zuv7vwNrIpWERrD1hPAL;0?tc;T_b^;AVI_D44hG6|H%O3|1xsMB1PR(F=r6Hb zg8V9=obQkZ1+!o~4@wsS^$K@N&}yv&N3?+Hut|c}#Xx!MWfJh5CulPbHcL=656CZ~ zecNoP0DNehgk2J}YcB!MZGsN1fPOk)t0QCVh<3tO=kYddGqKrIeK} zmEdUlIeH;%k)VvcKH0EFf_?(iCFq|IlVFDg0~^3v367yY&pLv^)EP|vkYy4aI}Ip1 zu02qG++GQW4u#bc93O{S5)7*Y)EiFQ5yh}Yf|2B(K%XaIX{Ens7M5s-ggIqZ?(0=8#Rc3}gU2h_cArvw+# z$3?4w^u?tx9o9-PGaDuYWtY(A67nxu0+d~{LxM}|Kn2VJ`nhzk1edjd5}@5>_;4Bd zmr-{X?Ps-z8GwyhTO_!g_Lt8G+FdaT)<|$A{a?9Mg4xucJxYSBsB;x|u8sq3uU;>~ zHKeb>{xwMn=48QOz~;3UhQb2aDZ$+KFb8%>aNP)4Cc!-F%p*OIKIUzf;QCg8t?RLM zJ@szD_6=;`K>9}dyOI3)O<)eJmf)r=7!31aqXajzee(oZDZwq+zJ<28tmUJG7BCAo zNpNcs%!Az$+(z5mmcU*KZm$69E~MSUa##TL%Q-r@V-hTbT@r9#AKY05Y~Mwi^L20+ z`HM!tI>6T5r9dBdZ;;@gCNKb&!X63kCCz<)us92_vzR{bqmTQh0rl@MhWW5v0?zHh z0}Fxlk`iDHOEyXHAoU-d3F{9xEP^Dgg-x(if_L!eohCp(?@;%hp@98&@Q>?| z;GM;Q-|wu0&9F;?^%m%3J-)0jg%LpC>z7IJZXLkJyBj5V4H zS*>1e)q6ov?FM1kptg3je7R}9w!?9a8gT;Qj=9?R7e2#hlZK2M(ZXQdujVffDh|ru zD}@pN2WNbZw4K_iK0N5cH?Iq$RLukSX8*k1f&BY^BY(+n5M`c0Z`yU72=_xz|DY_Z*bp<)Hk#f0qB;0sFUR^0og!KNI%z z{&*%|<9|>*ZVw3bfjIr~K>ejZtH0<#{==HD>!`X_a5U?vsl=p5X}26hpawVM3mk)3 zyw*gPoj}4M9G_ZKHC$#*HE%|frcHAT8aB+Tnd4Vgji_mpW}P}UP2{!g(z)9&*VqxY zJLNP#dQ8vqVZU5#wTf~Z9dbgCw!JzXwCoOy<*NfeKlrn;Jbxhnq4ZelFrZS#QlGDf z_AV_^$FqX)G|7_4W<{sfWK=RHH4d@Xjj2^5aIU+48^3X*h7D*{pIt=%YS%oB0p&JL zG-WK1+eLSmhKV}Yn}?U(a?7&)JFbWyd2Z+6qcdNf`oZLR#G4%P^0F@z2@7aX1M7<(3%};bv;LP2?Vyb7*#xMzm{?Tcf5P z7dmxp*0f1fqiHNaqD0uKW7jU7TP6||_{5Mcr(e>y+YzPtJH=6S zfW>05SYXlHRxBD9U;#-K_5wB#07(!m5Wyy~fPy5FiX}?2B+IhoBKwWw$g&;FHYLlB zBqy$MOCtBsR$@E)r8#zz*OBBX!t#60%)NID0G0Qzj}k!c?A$vuXU?2{&IE(eOPz;p z;qiXf+dm%0-@#}!nD{FF#%D|oaOHF3aCND0om#T}33;!&mO8$cHtC_b&C1NiddAH1 z(BRXKjXUE(NimoVifLXl84V`mC5eGHQaM~JLn^`~Rw$1{@4{M*M)SC2Hm@*`xZa#o>4R!b`yMae|lL{v6JefbaV`9A#+`cv8nUOliX4?9>M`J!+^`ADQuBEhRy0A z#8k#COi^MA&a=DRRYs_ZGo9ZKu)0q(M@7LOCUYs9`c0R8q;Pj}x zEd?ILk466{wfE4TDa4OO`=3%f=5trm@cp?4W^V){cg zCUskV>0CKwm)li_yn3^`-$_1T5O*L8T+nj>8odfQvOyW6&H=^Pm7>=sF>JtDb5!2A;&=CZUF>&(YGOQcHa^x)Ik;1YhwJWN(j0tzN0 zLzcEI8^%FodWXTgjnX&>5b;1)yjFB%^Ka90G+tU(%A^W+X=Pa@G0iM1B>oasqQfKX zB~}i8=?fA2o{-tD z=?QCZ<`WEnL*o6PmG^Mm68(u@h<4m>&S6HfA2cdZUE-i%TM9Ew$-thDVUtl-`Ber@ z2A{%zKKwU4_w~8Cugft_+%}&UT=wC=%&Hv4uUv_7>DSl4F6Znx zi$6OSeov#b&+i6|`I@695dgO1B^zGxa?)shi%?UEb_T-g_ftZffE zc0K$s`}o5T&$2(v&L%9gc(*L&1Lq?vU>}=b(o!M+FY$J{$iv@>9s`QkXVa`5> z&r)b1r6eymCp*h#HJA;WeV_m`R$(+~4&sNs#X9yMxq9-a_*r;)@q(0wYxuM9-6hwB z{pI_}_jgs=8Om(e&QNB%c7`(BwKF8ziRX&A<3$c&=y{Ixz6|W*xWJlu4m@JLnnl+C}L57Th*v8#1`9iqU_nb6O3z zldM*UH}NGgFQLzZS=@Y$&nr)AgnxiR3up2?q;MdQEKB2bthgSQon?VWBjvF?qad!( zwUQXka&Ng~t|t=iox8(!{%~YPZ$w34PPZ_Ie1-4z7WFLu4u3-*#NmtK++|D6@dh)4 zooZ-V>Xj{J+Y3NdghUD2RTxwXeTvUMG`IcUyFdQvd*8baBbxB6a6EDX%SS#qreykkbiC^JHPP^S;Edli+G3VSI19@0@ zbQbSCM$huKkem+M&#)G>tM9~EqWv78$J;RnjHS1K7SPrD_h{|RtXz|>vqH&=^o8eG z;nU$pf=>~~3Sgc&WPP()EoO*C3}O$!w8W%yq#TDsOxhfdg49JFW(eN5A3Zwv-uJQy z^YxtSVXNNUd}{L>ME3Ch2;N_X_ZLbn@y5Jt1-chr4eKG~jd_UnWZ*L(!Ce3al)V^R zAeeZtkc<_?UZaXnfgWy;{PE^3A3tNZnWm;Dt$EmsZ?aCXli-TGZx1CNWQSINJ_g?>xQkMGG>x6ouF(1D7P%F)hB68IP@c&MTIC9%< z(}^D+xosvIWWAdGgy4g-E#P?|9nKl;8XeEWLbMaV5bc~_sCyVx8l+;Wn(T>IP6h4wJqR8*WSO}UI+Qv zfhmihDGk;h8Ee4WM7w6UrZuPbe^g#2&J=GaScv{ls&-}3pU#?S|3pfAJs(%V?y;0T z&gc5AXxHq`YoXtgu2ms5NgeU9q`;b}NRwpA5oR~=8UfZ7Y^2h|q}m!!Q)QFem6rox zR1XCx=jvLISsa{Tb6sh1SxMhV)${YS&gPPi5_V>i z^+Jc5Nc?5bn9EyKXNNtSb*COD3A1$G{26TJzk@Zb9MayIcp)%Cs`8dNvvH|*5ze(y zfswHU>Z2PNAuCsbA<{Te&)#7Y{rC!3$j}6FeR3%A2ez>)ny85e|2y#lnE*jM_*(hCUQqFALdTDM zh44V^t7dzzy7`9Ok9~DZjqK^cr=0+TEepOa=)5L*2vtf5ENHPNTTr)zx5#>*+LRVt z8}HJ-Hsb@OVofLC1g}3e6&%4sxnzgq| zR;%_>PPG@mUpz$9Z%lg#k_ybmilVALsZ$b<(E1{8^@yaep?^4wSuE;;=xG?P)n)Su zW{iag&0{cD$JEE53rcv3fHoTUcq$mOuPPcnjU}WAs+tAPlbgA-!E&GksRyap86;(l z)It2f@sTS>(@xVD*KF9ZrnO8}V{OQ4FXUOW8$&ndUKgLwp^rH-clGeObBC|q z8tLka+$!I=@5K1{iBGZONLw2OH(KK=$RL$C4-QZY(77T|ImmAr90NU9@WuAXBXFom z73IY~r;kp1ZMzu@Hr7_bmzLsDfr4swW{|hXrW6JKl-Tqd`W-s=-=SzM5Qs&uKNXqi zXPx~M5&WI_X8&Xvth=!w>kM{w1{2>5#`ru*mv6>i8AB`iZ3$GODeB-u;%UsaTsl@=FS%|?UNz#4S_m8v|2{I&3qgU_V;vWkkGPJx1~ zb^R?p7glyeR@D0^HtacnX!TUb`Odah_&pD9+S_%^78+j(|65yi)K*$FGqm@X!0w)g zP_VwL37>D<(zhSZEv%o-ajmRNs~oO7m?QO)|_%^JJWpl|{ZS zlSwsV0vP5L-_}x3N%OjIYpKVCZ>vk{EUzkTtSVZ>5+j{ ze^)$jX8T*iS9N>=tV_UIlSN~|C*Te6Nx)yjr*$|NwKx}*bb8AyW<{ZM0dfH~2v%>6 zQURgZI3TVb4s@@qs`l1Z3ssnG+d+p}qgk5g&liqzd465Tp_Tvj#D+bc(*vDBIwUP0 zz7Xh?4Zw|CU$EV>y>C2xbVVrGJN}i<_JudG4(xvm=GB0CnI&($g6xQF46J}`Y?>`m zgn9_&Ojy=_`xW-)SLEs0*@Z{b-jPSt(8w4L1kAPEW~ht-H9BDAR_%rB(}~{z)_jf}r|-jfnja5yhwMjlFD3gi?C2&oWX9lw z#LeN1C1(P>3zI3U!q=vl3G^G!loomGU}UgZ%lS0OWT?7GF(<(bqI{(}?w-bVU6X_4 zH!T;-t`R;^?kHW&@u3C#j=5mp#T>PL=ehvq$n{?`0tsWwgl=phcQ*)zok9QQ4sTL^ zQ_U?+N6%df1=_=xZnK>^DBBiZ4|R5iWb49f2hRXDLf0T2iG_r0)a z4x}3_?h74~?~hnVs@@`WOVPeqhZF5WhZ8=F)5==l58|*;=8x=H24=XDvbCz-#-sv! zp4i+{R;p{_$;}O@iW`_P(>8h3cPP@5zR|wH;nrSrV%@ zv`q>x0w@BT0Fi-0#w4%DQDn~}izDvdsnRrCO&Lo9izF3YA!C{fb}5KImoz0zSH9%z zh5shI1^t~=z*6)LiUAVy)nb4Io|7IT+WERcAGNr+Y@&}QmNZ`_Er^gOON0ec2^JO? zJ7Fh-!Sd(N-EoUG&tS;29=-iJ7<8(An@UPHRkNQj_=)c3g6^&dU8o>GPFbFb+v8vb zPz`Y!@vV?5>?I|_5Xax{;BQY3<5NV`ow94_a0?Vhvl2hrtymRA1T{}>yr%r-jCmD#S*2GLHmLG;&XLuP-Db67L% zuW~om?3Da*Uttb3^CUP^tbtC+Sx{2I8BU=Rgm^>{jgkpijW9Hqf9J((du;gzCEvF9 zk6!%FwJDq3V92)}W@p)fFBa9_vsY(N zC$eSawY|>lFV2b3XUSJ$#BM;J#T>~G zpe_;;4Hpif*-Iv?37!ISVHB9)$MY)WDDgsmU{C(wZ|?d2PZLFR>>}GoZW0Q@rTrj% zNBBv;#^)~lBxGME8#l!)_(_^7fG+?FCbU>xLS^H^MgY{s=65=> zZ4`Z=5pascNHL{}IL-`3^UAs{gWJjaJigUca{d>p1?~FqYLEToPR$Cv?{rR4sp}vs zNi&|wQYZLSmfQw>sFp52<76f?Q*C9?|3OC;9zkI?F|!_P(Kl>`_y@bHMYZWbbzinC zbiA0_UkyGPjk{cBOscFXt9DfvJ4DQd*(iaz+fz5RC(6#nHWvnI~DFe6lv~j zsisQ}Xy5pH6+3X~n|$AHB|F0s?H4K5PH%ru?|)5i_sCiLdumOWsU0}H+;jKo&jH@b z^ZNVlPiy~z-u^&Zdvd-PmT5nq(*BroS%1%a_4cLboBG^P%5x9u&%Gx1E1%Kd_fSfE zmO^Jy!13X<_5!{AwBC;K5w)cC=kU_un_S;n8U9Yay;tu)ufK=z^;Nz7c4~)RrxQ9% zKk&Ft3QC_xvho@lXYp7>ZYjb_DAy`vgS>bl+C|}3wG&A2~ln-iVa}l=vHYY%? zLWn}&{?65#tCu?q74`XQICtu)&|y8I!*nhc)`~-KzriRS`GBReAO zs(Lsy0FDHVLXaTdkH~G93zmzKGwH}y5I zGMiT4e%xJFQt?Eor_1H3wCx)l+_$z0Cf?QC+8Y|$M+e?_2%kKww+HI$18nnJZ*8qN z@nWs_P-|Un8^?PNTTWMAR_W6O$M0dl+zwt=08_@%XRV0!Girp|4TOa@tQyn$;L_mk z4MZM^P&jYK_h3IUwUcUc8;v`Q3Yp|8Evzi6%*jGbi$N-21*%J@mIncuRX7z1IxIi~ zkg$E7BYEfN4EY;kna{Q6~Cy#|9_eVmV;mhGp$dH3r zOF8hz{T4CpeHk(Bh`29?8kmikb{NNXo1hw|==AydR*5}~;^?sOz;q`JI8$@V1*NNn zY^$xE_~#tOwBLi6w4-|(Tcg2ygB`7ndyWFcI0t-Q9QMCdVUI7CXulwj<9k{M#j_)h zT#IL~OSAdCqj+|#^ZzBDJ?$}J^J|q_3J}j;uv9$zvS9LQcD}zNvrMRTc-K-en_@QD zZq{~-i1vQ_@hyuWs2lZ`fY*6~m)=giLBRJ6hodYFa2Z6y?R?u(xXq;|4Y#3;W#YE_ zJ#^x?!flG%f`+W(Hn~@X9obpSV}(>1L&xnmHcn1#ifza@%$?60$KcP$kv@<%3Q{AgrmytJgFAN) zhF6&t!>y-f^Qu>Jj}|9Tp}eWKzO~hM%f659yQMdL7ZyAn?v>lFUr$71!AB^*^AYZ@ z0RJVue<#*m4om-C&z9O`;V`=v3@Q|>T6%vGBL5GXUL)$9(!zVWaPYT8M3#9LTzoq zplx=1bo^vbc= z?F_;;QHubj+Mp8wN~)uS79rLEBHT21OK)gzZ`DpcwNChJWS zXvGl~(?<+7io#EQ3@nX*TZ~dYp9PO}0CRg6Max#K=>I43cn5%4H8=Jrl zA+g>SL{IoUIlcZj%-d|bRR*tIqWvQ5m`k-^Sf>4aO8fh`Of262UcKET7wF@X?2q?j z{M6?LQl5KIf9^H8hs)C9eGjFy7jRixv_G8IPV`c=pI)Z@jNXoUaT#0m*KD(ya5@Vc za1tIQV~hUt`g>^qNyZlKw^KVm;}j#(2FSaT0T@A21U?U`8JiXt!1 zXO!FUu2@emmIPc;b19Gq3kR`|J0$apwO`WLPPqteuuoM=E8}rpRL#gk=u6K-D8;21 z?(&k#(#rf?1Poh{THs{PwA2D$m8#_8c))`$lw6Xl8do3cU>lBa&Apg=Zl_poH2jkg zB^S_o2DYi|Lu!Eo>j8yn$0n741{YFf8zMttT>z1xJVrJ0A?dcEkn=(>y!D#f2Es0s zZls8s928_=|EotI-M`^uA1QJF@Q3Wk?D*R4GcM=YY~uX_4~DqSpbXm9RaMt2VkX&) zA$fENJ}BaYs%lljt3)J_O@nyI<76oWb|BnuKhZ*j`?=!P$u5f0HO%`7BueQv8h4Z< z5C#RU>&oj8M_5`|1_kj;KwaNCu$kxfpt&u_d`BRhG zoUeyM*AKVWdAGL&XUC83KN)P@x$x^92U$E8+#ifZ+Yh%#1-_7dN9g@G(Lgk3jN?sa z`$bvg8D+L#Sf>4aTKfs$pLqYBY3)P{MEe@chbq!jOe&Y2k=CHtM!GkR&xM;?k=%U!P^kOZ?!=>P7jjubi6`lN;17)_ zn`H2cn>TNMn?5Vwko^pHH2AH~v*)Di@LMtV^mWZ|g&D~$sEO@oHau=Noso~=ZxPGf zf)!as4hG{`+BK|zp%})N&V&&m-H81ddj3gP^T}DZYBn)QF~(vH*b&q*Zk12-eaURU zNMkS2|H3lu=hNDgYdF13J6{{UANJ?unrqcHo53rV9{;SoKV@E^$xOg%KD9s5WVQWv zY6ndof)0XlkVjE1-9Gqq4z%wE5M+zlGD)#U90%fr4TdoYU0pn?#Wa>>MN}3XxGqHb zF!X~Lby|IDxvsEdWM?xeyE>cOFp}X)pyXLBi)1cFl$>kR@Wkmml@WT+di_mJe*7f< zg>T_EvVyANLcX)sOA4{KcHxq`vB_}!Z&_|7MQT<{ zk+TR>dC|bWU!pmr+bCcR^I$p`V{#Y7-B!- z=e`W{rE~vh_?vRthuF7p?sKG?cohUm_1xn-;T-8vAfMpdY_KSDUy0~%N=G8tZF;b+ zBf&1*cIY|{c>gQ`1>MDX$T1}vAjUhdj<*>H1-Y{bSc9(_3@0UL0uS;)g09r8uSIzi z!fEDnY@XYB*S&xI$@{ND(@C8ES>mAUD-VO3=8-wJ~pwD zeQ|c-=bjp(fBe1VOJN>9{s&~JCm2uoU0;Dd3V&3-&6>=IfDXAHT{?!5KcI!lfI+DF z5QVzBf%q}iHR}8B`!2O!E`Ipzk1rpyA_pPMdR(@zA6s!|#k#T1g=cVyUspjvS9#(^ zZ9W_xVm{|&P-1EaoUnd%2#yxk1A+y}T|{wUXm+zxD0W^RWDI|r@BLCGCJcWBLv zJZzw0$T#=a+6xVk4Wi*F4~?PR4lYV^5#hiwK=FYG0LP~`PyW-X-XF~V@1|3mp9M50 zHl74r0J~PqsU9+)SK9up4Jr@@64FkwiJRECo4IS)Vm2ZT1OvL_gm*wCe9OiGRH#V$ zm!omGsqiYVuK+x_8p-bzwgaV<+tpMnq{@on@8B%iVQ)RGC;@*>$Z81I2Kr}8oy`ZF z#jv7ptP8Z()wOQuP3&clyF8tVL+W2NZy^)3fhG(+15pbiftYq7;3ADPU>W#;nxx{2 z!^4Ts_$otQ7>(!P8wiTNLZ2=&XzmzYVmO~7F!-lJJ(=ewRjuJOH;JpH-8J* z<|xI$1GGbKxXRwEoDsP^${7+IfGe5LYQ7Khz2%UeL(it1HPN5=gJ>7_Y9@_QnHYLD?T;Pg*($6z5FznO_D@F$exK&4 zDqIhL9eP+RXhmACN*`z9B3A{m#LMNXsH_-DMY_3zH66Kl@d*6{qR{|;thXMVKXmr& zq4|Ti9*>6pC=}(t0RJ^NF+ShB7Ip75{W`rpsb8nJC-v*}_N1Qty3F-!Cf>hDzvk^r z>DRBzT)$SIOX}CJ3;kMcPwLk+K73Vbds4rqc9QAD_(}bm^ypT-J*h{huOq2PQ#;9Q z;yqlCR`)L`&x149NVCrr@ZvJ!Bzh5btXu&?0l$n!_24DVLOiYUJU_Mi@Vwt=aVNvEAkdfDtpKHy`Ye~^9NmtiR6X_Zz1yxm58x)b)($>&Oc8QN- z3j+9oVWKoxj=Yw8D6Qq*u07>74L(Y0>57K#p_7MSfG7FbehqnIL~=+TXxXRZ1;`mU zNVR39u(;?cEx2no?gqhUl=CEw<3U;vCA1(eT$YzjY0-KC0nrC3E#68=s;u+Yxm_4A zHKj!$5Kn2riYcWf+E(O{r89&BVo!xF zMnP??*HC+le7zA^9^XsZC>B-?Er$G$l;P4qrYVE*Bs5FKXjP_c5dAfVoNbl?s|iz< ztXkU}bvxGE_a2RV$(h^J)e&uPt&cQBydF3Ft`vHQg9mpmDFEbF5HTCb+US*2y^Cbg zB1=~-@;B%lYHYNqII4LYulH}*(C-hF!N*Y+sO{hIlZw(3w>)Lo&{G>ILkL<~prp+0 zE-S03_~I>YS80W9>l$WSv$eE3Kfk(k>zWT8sqneW`bWg)(JBu!d8&LBi8m^If7n3( z0$mygKePjWg%C#$KWk6-B~ld)AtDexlF5*_f@fhsDyIY~E-a)Gqx3$&CX_Ug%FBvS zz=p&JzYj(x2=QQsTzcR{2tcTHI(S}`ZigR+lch7fsd-{k=Y~APLDk)IUA}QJ)D;?A z@yOWe?#P{wv*HwAB5EPs+%6+?mitAIJ!?ml4fl(-0}E)b+%K9!U(@GG>sf9oBEdqT z(@|6?slHJRQ&3Z_uP5mfrS;g;moqqL$lDOxJTbW`Hj;N(UCmR z^6+jXcki#Y&3Sw-C_KFI_ad&VSUAH%4~0S?OCU&c4ac)xX>w>8<@W&V=2PWSAR7vw zxZ*%i^~(&gRCIU?snp?9{UP@8^R^8uw(gq?wFN@5|7c{zvGGt#Alw~Ie3s}9pBJY$ z&`CJGIm>xYX8Q$IHz|X>iFJSuiT+pF3wVx6ec*vk$Y+hx`$gOX(%LA&1=VSwQk?*5 zDU{yAB}Y&kCP~ED1&OLoc1=x%$><9ek-w3Gu+may()%Ho4DNdrsVH?8S*heeqnYGvgd?+ku}w{)HQf2?idUISRx&q6k+`N-L(46sq&693QwvE5dhM19fgOm#4mhUm|So2ofQ?A#W%iIf1 z9`6I51D}ECV(+*O1A9a{H+6_2C1Ri zTkUPG79IwzoD{?+$Kt#j0USa|`#QKOS03t&hkAy(j;ska2jji%?f#a|&X(3r+1fW5 zZQl{=+L0w^A6nJ3H#F1R9_X3qXlahE2rRsgxjsyKCHO&>M0F~dR#-9_n~XARUZ03f z);%8fhmVdv`*c-^(G@@GHz!P&&`OFXkF zI7_J=gCmRKXP)LW$H3^qDxpPwN z9z(bA;Xm~$OyJYQyI+~Og{E-JL^tLzI>h~|eBFu{cmSCmy7@V|UK$WK{Q+*%r@6xJ zOt7gTt0PJ@nkyJX{1@sd8LmhS6&o3yHXB2T^CX2qD=_GDbs7O-WAG5hEHw2RsT!sZ z{Baa{voLX^o-ssmulf=wj|Zzz(~4j~q!glxNUsLSCvsBG!C>x|-W0g1DMm#1eZX(b zS*(lO4c^E1nc{wOV4u@-i?}&JdN;X6)3<@yw1(%-Be#fcPkf4Ui`cfrQ#`kbzn{}Y z5hJSksWaO(Jvy^p)1x!nwR0=lNskrp*YsG?zM!_(ns__*lg9sdkz4fdnvZ6=zBKkb z?MwRb(tUrmXbz;GE$-`~ebCpT>5iG}*7R=CPI|Xkx6r#qeAzMieTZKe5mn>Lp-@T6 z2~v{AWSm%T)iF+hcqj|*Qz8(;F<(YV!DSb)4{6Y|?oAx2LrQ*~+|nWU@ygL5qn z4XsCg2fI5@O-`QtJ{wJZf=oYb7j)=wU*gRJr%rMHNOL~Mae@3dv=^`t12+fZFT$L$ z7SVoLwU=i0KP&gAyoc5<`p>8JC!I;OUrK2gdb5C)rZ;mKa=lr!3%yyyw$YiW%*;Ci z>|iRK0BPl3eUG~cVU{`BP?jrMr53i1yd#1N_jUvW!s(%_%esPm+uVTzbIn3Q))iy6 zGx6lnqx4%>js<*hMis=Us(U@B>iq%-XuV?oLPvy;2DBTppOlerMBYWMTF%n>MlXNq zt|PWQ#gJp2zKryV#OEt|ii>;PY{SA0;t4R~B7efaM7|NXscMN6i}H<k5WJ}_oOS?+( zKP#mSGJ+j{KZo7LG}vXfYp@gT1Uu1RgIzVwaG29C(SBLUTk`!H{Sxn?Gc5XR@i)Ak zpGncq&!j$2!gbNE;X2kY+W?=Xa*jAFd%K*Y5#~w!8*+~Ro;A<1ftlIFmw1g?{$383 zD`{}aY}fdcXeYRc{#sm;nzM}TOgyQ`TgOC!j`_R-@oI}Kf@Ycq;hSd zwitgG_j}#L=lB767l%2uBS#K%5$)&X8PN_IfX1$chYl1&HqrNhE%pBlwZrpN%?cIX z4u1r$X3}emY4f&As7FT?03qus7EnTzU=vQv#fv~mE(iYO6~G{o!K-N5MQm+J?!F$!EE_a;SX#eq-*lMwB$3y+Yz$jnETk=U8BV-G((_lsXNRQu}A z9kw0Xo#4`|zp>Guo4flE-&<|oUg-`o?-ns3s~IxuNyxL=S+d!>Se~^_NWA2tD^6({ zSSTJaTP^c%|HNp#7$d{6RsmI17lWye=8|Htwi;qj;h}weK`*=@mb7Q=m@W@DYs)&c zyBYYw*TrSx_o^~+#Uh#537MGotXX=5bXlv}&zO~Lk`$GJGBG3?Taff%fq?*WF_)0D zm`$=xW=P1^2?+NqtPo8?`cpCzBHrO+F!KN+%~BJpn`_cFsRe1$_0oIntICkFUA?PP z_I!`gkg9U7D%iI+9S8I}_hO zY4f+?CV^63_9sWGWYXk~WDIH$$_>X!}h43$@+TUs`+ zjP{%yuXg|QmsoS#%$|dhEv>CvB8OVT;a1q~jPI+gudk$EFE$Le_wBN6EZEe!ZQM3t ztZ!PoWkusqd;4Gm`$t4;HZ;`LHzlt4J)W8xj|a>Wd&>8Y^MptEo@%;GX1k`#WVUO( zBC}oNt)dTrZd&;k&l4uN7Gx41D_Vh>{PMMp9M+nGdVUwL(C#E+_F2tYzW899lx zBYzjVfQflwqnHcV*GEvk>q>O;#n8TZe}7XX(!@SCyYRdCz7U5a&HDm}qnP)3G4GrI z3%cI{K2#_LKndZSW)yj?vlq-sn!xl##5HQ@ibxR{9Q48nJWM8qYEMj_dXVL|Hq^IX zI&C{K#y&o~@SDa^sB!Dou@e9leDQot&O@KzF#Leroowg4@|=8t^GbZr`{(mHCV415 zC-e7g=X3W^y>9YO`EiT^#*-4#zo;DbbjDKD;Bl(gjgDZas9XJ(JyEY4Ww&9DFY-7% zsq5Exmli3b(fLBqc|9miz@n|bzHRPQs6G^`Z$ON{29sd0(disJ!92nGhES-XJ_tP0 z_rpc?W?UlX#0+f`VHMD<)ViO9aiZJ{P|7d?O>%`{#8(K9fRUFq)_J~)E1aEj2IRg4 zdvooY*d5I7pH8iKY4(kcr#3#RZPAIb{dl_`i{gG$@-b^5qgqLRdeN4I0Fi#TU7C}pahY)VM+}% zqnLRfZH3K}3|52HY$EFe1Vnz1j2(&R_+sV%)YDQ2yAD80HV2201Nmiv=t4^|A!oz^eIV73aa#KVpAv$@MF<9BJMBPHeE{`R+L7mgoVyXkmY$^PRITKTvuF>WLF zxrpRe-0SiT#({wBqQ7A`Mm#uO_EL%%JV?x>zEMajGVl?YsYaywIEfHn;Uq2DYcvkc z3S$%%daGeQUxe;n=3tQ&vysEhsgqFe6EO} ztc4W@S}&>QGunI6&LJ$?foC)yvTK%04?I&!{y@NuF2$(8$zlfbH!>V4j(3o3CSXhwAr3;Q zcB2;*kmWkXy^Frbf#j9>CR8pB5f-_Ily9e3WJ0zbc{+H=!xFYpUl@Mn|2wpMuBE=A z^_cHqPbWg(25d+7U?xcbd&BJ*wshMkd}O5?Neje=dN+!gE={0)2;{Ebe}TXR}tv&8qyXlC##Bn_F9Ot#N^hx$r_7q_5lKh|uC2H-K7Epv_K^9R<6=FB|1ZudT zatoO4XtIEdN!mmuY_VO?eURd5LJ16M9`0Q#!J7IQYGbI6!PbPQP{zew&ZMf!a=+V8 zc&XM}a50xHOV0&SqnfljE)XBJ^ji^qz_Od7-N6xeNwHgAW$tT__V+1StvQvoBSp?~ z_SLr9+E!OVkv-Sn++P2+XnAFur>q!ta>?Q&OP^tX!9FGbn#ac?vL9;R=V)R{vsae4X#)b>NiCfXAvaL%&U$t^;4sJv5lKo8HJTKsAbWLocEk^xQ^M82jPE zy3{qsbnt8-GIdo7g z3b^fcMCrnXB9pVmUX2Qx?}?vEJm31aA2L>6d$s)RTK4F|1Fv4Y_Nr`7{BRwC5%47$ zK9}!TP4Y9(1;%HPPjiv$xEQ)$eZFcWm~;u_F!2Zl&=cJJ0qf zzR!aE=VOU0Y$$P^erO6vRfi1X1I^)gkNHI`2+ZKH)y?bn6U2tVYC%v}?Vjj2vD2Ee%6lbxT)hjC18-EDhwMYO6^0;`5vdXB!v=%(VeCuc97uY}t zT{gC5TU&Q`YkThn1dOaY+|?KC8R|P_!*yeA-E`B~kyvwcw5J`{jkWLW?AmElvJb9` z?~jP}q}{8L5BHnuTPhP?EpbNFdo?(cQvQ~SUptBzPu%3O_9VP_-+t_*(WHDASwSDH z-oDu_k3enP<=eHRa^dsXWBpzY_Dc+EHhomJX(5^n_E9>k(B9CGf2Omh-o3(WkOJoR zoBs>{4$+h0!KaD5J4rSW_=y+F<6J_n#|%> z4MJV8SW-glrK@ENdvNYCvpl}jc=9gxZ>VcHow)nfOPCwLCF9-`%oSb}d?q@B`?K)5 zkALQMA7xy{!RyZ>=K|l;oOwOml(P>C&0KW(ar()pIoL8__Q6*G(tUBpD|v2yGIb= z42FG$26)@fklM>iN+hY)@2xEHl%Qe=ChLT<@3d!g0)~5HNWOx%gzm4!A;T%FwUYv^ zI)88+f7`t~6x!W=N&Ge2-qrO8e%ebnvdCEcrT+a9HaN(Tkp5D9EW$Q^fqi*J^d&0$ z{!(-WzyGy+~F;ych ztek>@*s|{=F3&wRH+bzD5a#{zkpg-U|m#%tc#|%`xTRE?m3$ySipJC2Ea|d@!0b-mQ{cdbM#szFyJhd>-Wg2xKKT^?@$>n%0uE#|%_H81^3Jk(E59HdoQE>BQXrAM z2ia$~__%p&?hR@qX_SH(;~oKEL{1E@0~ z?uwVtcO0oQz8a0^X6%PEj0`9KpU?5zC+5C7_r%;B7WlY)Xu&_H%0a333BUpb#xRAq zU{HwVD1WF*A!>2|EU(?fB@w(;b9E%;pSe@(kN5N(Uw>*g($^Q61+o3dj)M?>Iw%M* zjR{4flT%M{1p^89jV>^nYd&r)y9ar@jU=czJ3xwgdHkPztE6v7@maYl9sezi8c= z@*nV1CEBDXEHD3o_bsvh+veU_CjhW+fB-NcpFEs+IXcnbKN+3rZVv$SG4>z8E2tiB zu~aD?5!|8@TVa-DGgaNuq#6K387E;!h-e@uOLiesp$pOflotoHys^p%E>a+Yg_k-p zgu`CrE%1^UWCpwDQjiKcm8B$sXDN(9S8Q-^I&^5$T{{hIbUi!duBxrAavKj!%LR#- z_in_0@`c3k#L)u{)w^pNupG>)3BPDPg~+pme1_wOc_7DSn&P;yr)INZztx144t}!T zu3~YWVhx08yN%0$0z>O z;-YTcz^O|?Bv^3#Ptk)_TD;U{-9PxjGS_vF&f*eM?Y?fzw;S`#hV16!b=P#a0J#j< zJkAf@VMe!on8qwHN0?_L?tT^CC9aqINsrS7G?I-#PyOBe z7IA%IYxFBa|C%OrdUjNu7izI3zKUXO;c?;-h}_`!37Y7>J?%4>h4Goo!opss-oJ-^ z*uwXL5QX%EVv0`&ZYT~I9k_P}YZREG;yvP%H6Bnb6$|gUrYWFsy(3C+=h?TuDzn*s z_%PdXGH>Ok!QFFf8{6vTsZ*!^DsRn^or%9@wfh4Ni63KRK&=&U?FC%vPLWeuf*-Pl zO$Hd;&1PksLSM_&DyZO290OeOdT80A4}FV{#&M7+E-kxKN~Qu^pldsep$GA6Nb+-W z^Q4F~h9lA@L}!F-0I`AUsz!^ANY5`~j|}eKJTtR-_u%lJJsaI#ue-wQwH?^}p~Q1Q z`5)Z1d2(`dVs7(+u|_|;pF$rJC;bpfAmxU9+v69J zK(K>)gwis3&U`1xAJn8;3rWp|{K5_tm?1e29uBpfV)K2y16{|~{?pv(rJ3nzsI)BP zJie}fJVIU%L?b=$_dUU2m)K*(fYBa%q`r7>5$;AnDM^MshRzMT0-axV)&h{K+-t^J zGn*^umQ_4eL4gp3)gV^HJBa|;;mE=sV?xl|^sOfZW`*&l0B!i3zW-XPu9a_l5)uAU zDs z`--{k_x#bvZ`*OaFXfuXS8raUiy8^<$j2>Ys644H-i*(0Ia<2-pb|x{wG!s6nfuyY z=kfJl11`{&wvS;LTq}z+NOnD{qiy1Ku9Nk@i@0}-`*y%x>0B8l!(oW9BsFL&go6gF zD}jRslo;}5YB6vu6AL|mDzzl|sZ*Dp-I7`re9N=XVqF2Ovz_w*_zr=ST&Ba}f!aAK zL_0F)2?GF`^XzsAcQj{BFvL~DeRxsp}HE|)DZ}D%%RBI zz*J}F)Igx)M;%lL^TxqIM=ZeUub{)nV}a@EKn(kiKk`nVGyV7YC)ptc?dAGEZ~uq1 zcA?`^j`c6o+iA_5h95|_<3yF1+o>@-Y7B)`TB7q#JQtK&z=A2jVN@SZ7?W!1N;Crw3*>j!5J9h$^fosK^Zg% zoGiv5UV__8Lbx<#nUI(b6CCoUlI6w89;4t$6taIgf9v#ZcP>16_2QB9x8JW!C|a>` zHU|mh|0FJxe2ICH&MoW(U*r6df5tt>wXn)f;5(CqcEE8h&dUfe&I;%vQV|*z%@N2A ze1rRevLYd*h?*5P8?rQVY={FycdNYsMPEzR0_fbL1)jqGn2MmkfA{gklvV~EWQgX3 zSNbp@>D&$p^-Qu+ms9n9!rcuK6KaJP5$%wQN{Vo zftA6ajYPdM3;xMA7!8!QN(Brljo#oh`tTA3&Twa=-R`xcUS=fhOq>Epe zqr&mg18Y?tqB^`%cRU6QAn_oQC2_keI3J}{pldnTz2M?k)jhArg+iGH`FJw72$WjA zLN00F^XWlt$&1V0+Mg@Br*WU`19l!mR)EdKl?oX7B=-l z`2lE5uziyQHpBK^&ToBM!uG9yvApfO>Nc9f^viq>7|V|}RAUXx*uKk`wS9+4Jm;PS zFxq<#9H6y*`Pb%aExr2yU0E0gzA{+ zHnc8j_nzW(xBM;b-b7?^c_IQsv?@*zuguDs<0p1C>}nW0di=~S<4wDo_QEQe^$PwZ z{*d_ff8r0u4&3}7+(FhXwXk(!pF==vn7UEd90Hw1nJ0_4%uYfSNi%qRh1VPbzETyB zP^I|acmFK79&CdYF7xi672{GZgzyq`hWBUwZ6*zdteN#w7W?)O@{28TorS*!@z}0X zVdkflx_tw6{0RU#M+rsSVULUtlx) z0{c$&hkMsWC;H4g2g|)Rm6bJ^Ek^a^m#Uu}Q28Lio5cQBK{vuDRj(c;- zqa*T1a2&CJsvnGGI)`02qR7UtnfRm1@T}4LwGYf1tY1ZF*=x-QtJz;5o-C_rUnMz# zRldw?Ldw-RS9AviWF?Hd9k?6ExXawtjsvC|V9;ByIYxFvh~+xAZlBwE>AsIX^#E#) z`G52y%8dEduP`QmH>XXY5S%t0+^Ejr1}a#gfX9`dQr?q?1>D)qQ&(DFdK_ZA6 zF9|m!cml*d4ft?YIq9BsI3=)neouNh#N8|W`f^9)!#j6AaT+xX_aE420UPlr-eiV_ z8~fU4X2!g@D#uxW`-7|ZrS5f>R2#3xJh;V?RIfB>r&(zU+B|s7=NO**`rO>t3D65S ze)TKbTV4YRIngH8!q-JI7r{5pufY*XQx4KRnxtOZhuMqtQh?`rVul_j1|U_ckNG5GW`jiqk+I^ z=OyviOj|U1KYmbIT#oGRzS6xXBGW(j;GcW(PgcmLyMp%xy1N7Skv)Lc2N)qI4C@1z zrZcYcvJ8I$0WHmnp1SjC{)F!(?wI=w_b2!h!|V%*VbXz^)QfSbcHbg@0`PZ9e*#P! zT2weG+3b(rv-=RjpUz(T-1znvpR2ATyv2Kkt=gaBOCZ7I-T4yyi5~Vf`k~ZBdM9A8 zozFGhmjKj%_r8R^JN6!417CtZNTTOIIGpd|_`jqtffJIYeF^Dby^Sy7qd0~ig)hOM z7-64ZxIw-IM75%>lwY#J5=?g!Cw&QcbDA%q(4GThzGT&X2@b(ie7YxL$EEj7o;)>v z&*jFpw#Lh{|N7*PZIj3As;lb&M$jm2Z*1IxpW#QqaA|&otSk}1k(HZ;lrS-{>POH9 zCOqJlaN$L$+c7=8<285@>gwCt>g#0xmk(^+JVEqfUu$gxg)m{g#rU-n>!rJqQ)5VU zVgT7X#fOk|pr|nx30eqr~4*^?7{wl}r5Hf?8LIK@{!Q(IkKi{U7;Uq<*e zXn}WG-vMw51nDh(2Wpq)dkR&8ar#A{=fWnq_>Xq8$g z2DA#+C1CD)(EBpvKY8DY-vGj7n%}^K+mR80iHcMFG88WZ4Atlf^5O#5^~+Gm#Kvv) z{6duAz;rBz3sEi_E-4)?!Pv(?9t*Zcl}m<;+Lb6rcGtId1oIC#gVEOd-ADLbdokBs zeXg2F2)TE8Q84M_K$wSvj8=3lB}BmFTx-=p53Z!(R8(mw(GT{8%( zxDOXx7|OQ-L7%QC2N+D0$wHAwnyu3{R7TXc-9o#WAGc&YfaKoNG8_KJ^?sE7Sb`KB!tL*>v>fv3xhJQ1> zdvAT!Z>s9*@Cyz#S&HHp$x>tkT;8_;a-s%}~oC-Yit== zgm6)x0YR(Kc&3oa#}E!f6>gF~1?xB}npi7%*_9fpn(sE%{-gjaXIOJ9!Xrj<-(m~^ zZF{3R5aBgIM@HrzdrT39KvB#1bfo=%v`tSzh1Yp)W6pO^%WD9uxAreUT6+us0#_<1 zpD^sJC0Td-mHYnm;P#oo-B7T9hHA#2ym<}fj2UcCXKBAe+9-?s3Pdu6Um@&s z3?M21eg$YwKmIX9s%tO5jPqCzxTDGt07CW?>|9<9|osmfA ziAd)!JBgRxiwoi-`}^+gn+UVPl5i{*exoy7ULNkmTDd;K*94xW%^mGa=dnU*#s=@e zMla)YK&Icj_c@SS@~>Ctj$C}t!c+Im-E!NV_wHe(EQf3htc+zRexLYfl14E!AUTaKLKR}fMg=KC)E z3PKRV<#lF**XRYRruh}3h>7P~Bj>Gj+YJeqC|zWyi$k;)oTg-4IzH2R(qqH%na6gMZO? zb~-m3R6^Nv9I@W>% z4fUQo30vVREvG>La!&kNpN1@dst{F-?3e$rla50*LHHm_~7pBz5^G}leytHJ1xPoKtmc7d=b`21Jt@~0lpi<7ItGgm@b9fu>CivW|4OiScRe(1Q${&sFI2{N$sxedPAK?Xj-LUiU*k^w#%=LcR6gQ%1$NI{cpqK|~(Pe}-2%{auSd3B}&Vv)4?SBzP-K-nb2d6h=!$YWxqGK)5pDLm1G|-1d9#{`lqK-krf_Uu{iFEYOX8 z+eVN0tJQcQ%%A9|9v=WcN_OT|Sbsi*=6JlvW~Qt>Oi_fLQ1C567I?)@Jc&F33b?>s zTz0!8A-e@>R(LY6pa2n`$XgacQxv>KAxfBH4T%wZFsDWe5BQUwy*m>x&RwSoiL26S zdMMt^i);xQmF^j*y2ew_rrq&b$}yG*K)WiRDRjRl=sDGAmgPoWY;7k~Uww)YhdwA} zLNA@JhO}&K3U$dxZ*PsvGFTDW1-1~ z8_XHDv7wreOYC2f1(q5ZFNb zYE&~8f^;t?aRQziL@Ygo%IYFIA7ZdUwXypCtbC-uIn75s27Jd)s0iHWr!a z+uJ)9DeLl099;AC?f2X`Afi8dTKh+~43xT|(pF89 zGuT@`2yv51lQ&nvPjmx0ge7rD0VGzJ;QO5@XGnkR6 zjBREwk2TxMZ`;3b^6+8Aq`vUn@W8>_!;zZjqbqZBoR%urfej-MZQL|_@+19It!v!X zdENB^ShiGpO!w577|iRsJWKgW%f!4bf&YtQ-WG|nx^7xXJX`3dswn%ne@&N8Oa;P! z9VS^e#AVs^wwjkY82zP)q{^3&t$|GkJB z0Kt~lb_m?yeTgT7u~?ApT(|+lE^ZNi9B2TTRD&Q2-{l!jYt{fq8^BMRrOtS?g4-dB z3T1g6EX{Cpv3dNa$UVwmSH`g;EwmSn4Yf7UC;8>}ChP`FJ4A%LW-|~Q7YC;tId0J) z{{ZW5iFLGo=zSe)_IzMtWqZrMmTa5z?k|n>hWk1Wu3UMjOPMpo`k%o=GK-?;YyJkk?AMThc79Ly(pAEtmYhX(V1t`Vkd|} z%2c015Gx9zDqXq*5D%1)vL2V|KyQe<(;k*(Y$gpssrUGvim#z^MOwK-RYovU_7@r0 zgd3fsvRt}t@}6_3HM^&#vc!0BJL>POVP8IWEb*q#k6QHnJ}J5jwm_;RyLFyg5goy* z*v@t!kIwT+#scfiygC?(9(vPx#MM(3=syvr>kI0Vsi3GRY8*iCCVsJL$`D3PNSk? zfjexdB`3>ya^ud<={1KhDsxIlYoN2icX@+1(EOf(!V0sm^w|0naJ`WIAsU>mY|pB5 zo*5qCy#6)BYf}xNDhU>3^1$;5hDHt=QF09ot{7Y5b~&nwtLSQ6y0F|Vh?_boipmFP zCqXD9X0dS%+1_yP$;099j;Yn{kqD1jME>DCF8n>JSl4&%#Xk_uJRb|-SAdwhpgl#H zS03qfWX<402e6UC6zOzvBA1)3W>@5~JUUn!nS=jB5W3^TyZ;aSua{gUkMRQTYWa4Y zYl`Eb`qd_0i&$1;D;nWP@>9l*4Ot6p2sCd!a8n(cDu77iuy#vAtmUdfO|_>254oM* z3cJNZdAdU2i8{cR%$S784kN&r9ojt|zTd`1gF6O_OIb(rboZW6a5R?NimG$U_3I3Q z6nAewm3_|CUUjX#i+v-!xA$(uSg`BN(bayfttX8piFgVT7vhIa8Lxu4;ApN^%cZiX=h`nk6EL(ub zO%y*=>4D)4U;`DrLi>i)SyWjf4FL)1eq4G{n4c>(u_oFsRp{ri65h{bjXshO!vdus ziqx7+pX*t_bFkC}hid7e(qd;>dD-Bu!QSvnrmVD$jeKs`S->phf1#qfy5cKeq2Djm z1Wo6%Pw)EN$k;$M215qXbFwuw@mQ?)puuFCElq7FdpEU{of`Q{djE&jc9ZE8zNL1K zKlC1sKX^|?YCGAxk$cVG1JOqBkN2qkwf6}7IK2lmqxq=s;V|X%d`z?>laJ&QioK*d zL<)4$8fpI7qFfp2%4$)jDj0{rH*udRPh}$Z*^V$Ey2jaPh25r=(#{cPr*$$IaG)sZ z35*KDFl=g~Oo*cc5GatP_pGAe$VZ8JEH$Z3a7F^kMjbnWQds%}4>zUSn= zrPVhX7m_Z_zJ(44*m3!u!{SOBj7W#h;d?+b8(;^4h47JNiw@9^GV&{wiWowWIwOij z&I2JKUBF*Oi>0^>T6;6MT9rv<(D4yiXt7W|3N5gZE6}Qr54}%|F>KyD8f>YmYfcR{ zOaxRlUky0K+|`&Z$W*WtVG`NczyFWC_l}RNy4HrzK4`{F^kEQgE-Bgk4ji&hjYRi4v1lhJ}HZ3S(7FbUl@}oO;AAJ*Wzssbax2o zCtBtQy<6qzR&Q|r+?qCnu`RoHsJ5o&8#R`p{v*}%9ox1!=2su-A0ngCX!I*ziAH}; z|D!M52S74Y9^iDggUcb*tFvsH+^p@Kp$EoMqzxGq;{^5h_LTj4ijcs9Hn8F!*q2tH|f5}7U2sJk_7p^Nml{b zD1;h7wvd&9TptbElXfVOSw=v21Vw+{PSU*Pym`xQ^`>+k>hNs1{v~%>aM9{LfzPD` z%k}khp3LFcaq2H+Q#5e-4z<|719V0>E*J;c3EWDYL3*d1kVGh-P6nPuBC!u?9F#PS zoN2U^Z2tl<8kZSix1wATZoUqBjs9A|iD-@hW5pLA`Qp7buhp=j)_dn3`g%u7nk=Wo z)gFTzI3yoZ`&db&JJq@X)@(cxQM9N#L;2DU-~@Q3Qs`{`(Oz$UmK&1^wpLwDStzKJ zjW7@EV20IEH5A&I;OYkG6r^52{D=l(@9E3SadA*x+%Xl#D~_xv;)=PY0Fhuw-@-04 zVh4wKok;A!!hn;4mAdhT!s6n>7dB@*o!OgT z2$q!v<+sR6qYI@GgvEA{Nhpj5uBsK0l#m6k>wvDW%i~qQaMwV~0-z)+1TLLaWE_oQz2Oz?is~f~;-W<_0RLOOb?!Bo$jK zlqQ1&*es)AnsT;g^is~O@1mzhu)H9i0ctDc4SD1sO4`^lEQd$&9eO7~@#Ih|$MeJ|%179tya z_j>i6^}9jY*eXCBr3X38Aw6DUvzMR+ONnsATA%MZp_$-bt(&P+h>EmgO-z$Xw=-wmlRgwO`u9X4Xl ztK^$R&ICOVLEKZ$6<%;ohg26{&P>b~P{B-3Gr^2H^)zC_HSuR{Og!iwdXAFdlRWGe z!_wmD!UEzHf4<9(-f&()RE%Li;l>fJZo(r5d^M6(lx9AU)T3|-p~fZH1$cc4pj;8j zGsP|u3R!_t8Rr;1=0%-%2H#Zpmb`b>2VfkQkzd#{gL`Z&KlETS17(4cuH^ED_(;@c zvSCjM3n2F0W|+)@1P?~nVURWg%ScfS19&qi!xm80puj`OCvx(bSq(fw9&0HGi2sO= z46nYU4<9}H@P?KahB#sDy`N%zun%lh@8iq^qH=zr{7%9LA$~!$2^VZ!7l$ALREQ+} zV&~u#9wm-#3Bp_GsY~vDaogcQe;1QuvY?sC(xtQY%@C*l5Wc1VtHHlmUkWotg_t1oHVTwEazZI#S`i*Pq?Sip+{#JkjIEE>*OU@hu5#H@;$`v z*>JT>j0NVfd@vMJi`MU6;_j(n?@^x>K8L%|o)qp0=!iz0D4cD~K#~p^Nh3a4EqQGm zxCCebMu5hyrS1{t6%aDw844CYXFz>tt&uRKv;k0zeMg(#uYKs>9^AK(24PA2eI*!$ zFSF%;P&3)E`Vb!mb}wKaF$9DXgjJt#$;`CU6VnQDF7{=#ac-0`U1=|dlk|2V_%he2Z>qMbAoAhWEZ04FsG zdNHOl8l)QlMdV@3ai}BOEWr7cqGB`6s!bV{XYfUON z4F)COdWIv-ZF38+dIE7=2oi8+kguS{SbUOY2+4O=w&|*?Hqna~AG{W6BBU@sAw$&f zLD1Xe{We;Ng7Omgd|HI&yYbHI@y^>$a)id1VzV>hJ!y-!=DM*S!H9&k!j3h)KvM%?6YgW?k;})1_kf`{ z>?z31EZFmAI|#ddXlT3o92i>ts*%+j)i=t9F2oi{(3WhFA8<{^huv#Ww6n}#0GR(w zmr2nMcHm=UPc~n3yY5l?r}&G2~!Jmbt6K7fD&PlFF{!cSX3dv3T~Hb&-7S1ON(Kck}1&8_(I)m4)%llq zZ``wuJypHZU6Z}Mva~nDabauIu)8XIQMhET)w+VlQBKH6iVlGlOb^4LaEEYBUt|4$G8Qat% zXsOh%3jCq{46Sik*D;&K;}fEugg|0di7#kbVpU0YW(Kj3`RU1fuT4#3vMu%6vXfWO z{d6m2=?9T^yS<}c&Wi=8$23vt!Gy_Jh&>Cp-Itf-*bI7#7stv%%PEM9TzjlI2-SdE zs&LBsecobE5w0pEjC;P!BsIX9nW^nyy~HC34TBvYZDR4gm2q(JSQoazdYx`n!!V)v z$VxPP!Qqf8RhF%$s!Ljywy@#WL8Doxv!aR^nx5+~F81f5=~JHquUD-1gf;|T$D$D1 z$$k^;F36i7Y^E-8yiJVE3rwOs~GG4luX+--4ugb_NYStbG~y40VA{?6`HM|l6c(FWR6kdBY6cG_T)5dzmx*zQInCJnf0fZkyq0ISK$ z;^{aF4!grdGOdo0H<3YQVP4kFD%Agk!v8~HYd*-T+SDJE$v1||Vpoy=65EAGV{NX7 z?nu4`ieN3rYz3lV7Az6=B8b$=iqb5d!Bj>_F94aI{+kiM0bQR?E{TVDV2Lpqm??oQ z($plPZnXN`=Yogz!x-~M81SGSj*?FslXozh@AqM25%QJ$%i*4!g8qf6 z!m6pPPzh#0|3ZEk4A7+@`=GIKt2OXLQ^IhwxY zP)l!TpzOu2rJuIj?9DBfHT``94V}GRO}!(-Yt&nom)6v<#jT4h=}YU>f9;)jneu1F z{#H>hL>JMKY^h{Zu=A{mu=E*0nzXPl$Aqb4gy{?A;&Y=(KS2MFQ_zcUYrBHDEwink z!;HZQL;y~pLoTprW;u(CJggOsNf1t)YDo}I1bwafQW0`&5%Moc`wFacl&ywEgOpr& zbh(K_gfzfQO)Cf69$A=v^l19R$fC7t)n{8;es6Y|Ob+wB-rn0-3O(Sd*yGwEba50{= zvm`@=CdiYQ!5M}KSmaqHd8apA86I6;>GxMI9}Qng`$@}=k$XmW>=<1%l&03Ctz9&- zqn~{?v!nE$(smk8!p@=nM=Zb3D0nn;1{!9OJ2-h4EA41Z+UrZv5}AGRU5AW#%cD`%1k zxET&U1vSEMx0(@O$2adhe~HpW@WL)0lMHQcyZ!dt=g$4!Y5Y@v*^*xvVsC^Z&F!Jv zLTVVY`4(NJhs%^U*rpNBf%)PGg=?hG3YlUnEA@Gu5SE4jgSQnz#o=W}1AcWv89)&@ zY(^4-g^;RbIQ(Q83<$-555YVhe+L^TkG~_qLMK7%#Ml^(oc++n+0RD&TQ0&W!zIN< zIsRNf8kgnt`>HKeY6JmeHSy{IS|A*#Kt?zU6`@En@S&Tm3GuPp7SmgF>Aew3;)?X9 zA6?zIGuqP=b)~2OM@tJIywf4}R!Ift%Zifz*h>osf5z zo)C@* ze^yp%3X@0)%<^P;iV&(!b_b_D#hvO#IER&4(;!C~9AIK!Ef9M|*k4AbB5Y8CcfG8+ zq`JC%R}q z?_0m(&x2?n&mSR{lp+BDYyNaPP}I2AGt5NeuobGoTq734k~kHnt~$rLgHey5K}jgf zm*pi2j^<_tSoXQ zQJ0RNRBvI58WS8Eu7v!H+p{dt!;u4SW!OnA#FkpP^Z3LOaSQM{F?eRQxCINd(3Z-> zBU=bE8EDSfOtH-!3%(F0c0Dk9wFE*zTS5# z2Rx7#Dn?kT5j^35JFyQ+XQ)`V=*@}@U!wR;Q#$jB=MmIHdmhXYs&lD~SYgTJyB@g2 z=ypLmNx1T$i_!&T^qb}Ms2{pj{6tg+J;!GRrCP^=p^ zG68yp?G))$paFS8#9s)0tXRNj7Jt3A+l+Hc0u2^JV|jh_urGYT7iQfpMR`A{$#uUC zE()}JOK2{2H>*ogX{;us4#12&r$ikHvFp6mM%Blz^Hf;W0L`p8KZzJ2vQk~eSBj_O zvJVAfQef=Pg|`QW6ODj5!UDvgTT#uBWJ#DCEsjeftO&HVyCHN4?t37eqHOau*KDR&wclS|6$mUL zyI~&rhT;n2JtI3pHuim+O`T&4u+IpUF_Wti9hsmL?FANMFF<*Lv{MgD_lkAf8un$n z(iHVk5xU3BF#(d3E-) z2cvMGNfMa=aMxbmn@{7`HFR$>D%@I_cH>$%n_)<8|CnMoVJFF!90=(K`1d7uq` zNrSIx25sVVlxV|4%s!end{afb%&ce6N*kx&OSI8r_JSnnJI!XOHgbEyqs;??FIxz!C+j z&%UsJ?ff|{#ra2Sn|H5Yb*Uw6UfejRr6Bvp=FV%jjO;2c^&c&*&Ucs1sVhGitnlPi zG#;7LQXa?)n@erWB1?w)+A8bH16h7k&^|PG(c0y#y>yw0`#@_MG}!#exKnm*V-e+fMQLPhva$&y)AItgTgMnzq_3kQ)sj4bXhqReyIu*-cvgtG!AiWtEdF(ma$)?j*If67w zZaNJBO9u8*Em$VWcFIku7hQC00d~Its?;rmJSH+qv~auMXro=bDEtUiM{=>qpcvbBkN&w3X9}$ZbxQj zM{X0RDMIe#CiL%|HdK)Hqiv|DPXgZ~RL3A(Cnz>1K{Dn6wIX=P=#S!ix@G(ddjWH zQ`S@Tznc}Pzd;GC4_JwMQ4h=SE@7{S!fKf&+oG65S+L8yz#Mp1VVV^n9+=?hw!#h) z!zqK{V+6lpvqmvAIoO6-%o5BSdJ#Xv!{j4DPIN@iQ*><*v9=`Vfyz=Up(}$ zhet*ZePN`ixM|bl%~e&+{_Y;1rFvDQY;kDox5vi*{qn%#aAZ~W55kR&;j+>dE4g1# zkvh@74EUR9oh{|L%y64=DB8GY*9xVN=DB8E^AtH92+#8um3m5rq96bYG-C{+*5g$I zuoL0|($Ho3hBzZLlC*hbWdeL({GHP}x2|q(>l}(@#miEEQ9EmDzo;JX>VM#UYfBAl zBAhk#KWbWD^%N8o78VqEu(ewbehy)OWRc*lhtmOJd0;GZ?8q2TWrX7LxOambtH>Ut zxI9|7jCHX2*1A?a|H31mx^D;j!)>?iiQPjXXt?J*+%pF>$tFubjZ+`(Az>}qkHcKi zg4d03-3e)}kta=JVRU;P0R#Ew>liIT)a69|0i@0uJ+5A6j5bzPH}7rP+}*XQv2N}& ztab-W?dYnWJ8w?shDOARH(b{8G+M!=W{eBjC-TIapwnlj3v(HK4q%}DFzA3w$qmSD z;hG;SVF2YTLn;s0Kxj?e09q^fhFa&X6A49NC~y?>Z{4@AskWx6v3BSY6q@+LBdgb6 zYuPbc5s6epsw3+j+P?Xrlx+i%QPDOuZ~|?!N{FH-XA7B4pk7c(8&3Ke#G$Ar2T&A2 z9Jn3oSS{mNENq|pHI{a(`tD2n?s$rQO6?1LULB&o6SfoiIdVBaYT+q_l#IOzMF7qk zq{0_N)b8SaMzOzm8Or27Q#2Lo+=?!Twp_cnxwf{kq4r1YLG=xG(bipiMVG6qsv~Uo zj(acLP<1ioRn(z9UbKhiFrjO6{RF=QrBk<=cUWwZG%q5RL_on0kK2PvVkVl&8vXf9 z#u^|teqttbbwwnav9i8P=lY92bNH(|TT}7ewKa1u?Q7`lY{1LXvTZ@>rHu^LpJZ27 zWL@X{EzO&Inp=8$TAInv$fOQ%W)<3Kl9rxE^b*P*@rpWGf^#n?#-&Wg1|F!236UGk z7Ka2=Pdg@1fK8GKCQ3eE7{S-TU-TJf{qMc(x9Z>Jt-F7*J@zR~3PGC>kQPuP)k$5^ z_Dlf%Kz)PE;JE1tQ&lVM5~N&fELs{aEI>uf%5Yt2o!?tfQdoje`Fxh25F>~_uH&9- zQmctp@e*rftd3iqoJpTq*@DYj`;jQrxS^wCV!_E=hU^e)y;{5Y&8D(2{Eoc0i zS$yFZHPBM?YxQqFT4<1y4q_F8hQ*XOk<58o5W{qFS}YakdL5-goKlPv7X7$PZI$B; zLD`Tn`Y#H0mF+6Qz_^lcA{TAS!`>V5>98b`ng=n9dA%R|a{;HOnOcc|Y0FKiTOeaWK5V7l{BS|o z?b1SC&x&D8_{tF~Nw&DIT9>Gmp-^4bR3ylC#~&HJ{|I#=D&V%d2y zi>@2Hcxhp%q2__=h8-i#>&7-WR<~9)HC44%Q|^$A`W_@J6Me9XK4>x%Ee5OV6-xZp zje=3JZqZUcOCQovy8z)OQ+)^~pzxCSw}{v^OnHmAS0y~veiPb%$=+>S_Fc1$s<72G zH=~kEQAtD1Bh^hiMrziL?YvlhaorfZrm?E7y0Njkt_qz{xNo@_eaM9ji4YH0=evv_ z#lxjx_<19x5x>_43>KD~;9&@j>2plzONx7ne=;(K&>-E3(tBB1@AlT-c}?>gH+6Mw zY;0|6?`hqBQI^|Z2`#Fna&CQhZLk=QiEc~HU~}teLBXm>WT2*I0Dtp~Mq642tJ%L< z%;`O^Px!v_5T=#1qvS65$A)6h^; zRB62mE8XkZO?20xo&%HP><#wsicu*9 z#?S%D{J11PqaHX_SBfs-_%U?s1NI`07sZdWx*YSjz|{v zO@7=v>+yrQ{w;poH|z2JiR1qG@oDY9759IgUmu7cpVt0sas7{Z`>|&b{a2FvcRjzJ z=mSKSS?F_sx!4=>yNDGolN3;W))d%w1I94Q%v1by;j^NlU{5sEB$8Nwq z-{jX7pLOgIu0xd^+|zs3v4gnoEq=^**0KGGV}5Z=o@&djxaaHqy1-f2U5o2}%-b0h z$CRn-uIJZL-$T>yDY3~32(2OLDTP* zfQ?^ghY#S9#|`T`qp(cEUnd8gve_79F#T)xFJ4zfK8SoYv%z>d_F9w!2F28bm~<21 zG0l3tc|bCo^}{I^tD-mS7w1N6pi8s(HRk>2bJ1Wl&jEX>qs#$V0m@YZdMwprMfpm5 zDngMT2TF^V!8`^ZyHV^muz#(sqnv)RIS}Um45N{3tF(6+CbhU zTIEX{IIY$ST4lLplUatVMsyZ|L`JQ%SP?cMrkN-MI_XUYd`qGb?`gh+dj!esbRIqR z)M1NJ3(y`I2$Q{D$&FlCu*1{%;GeJl_idi3e^rLSU5Nc15G zQD0%^6C>`jVR81u(PNbtk_${za@!$1u_*!s22`HQ&@0r8vF@jOk(984D5F%w^aCUb zv4ckI05*aqqs6pIvRaKJwv;r*Xf-Z@AE*F+pbl{-E|>r5TSYsQ?!q@||4fVELMj1} zFp4Y{h6}?bp&&uC6y+A=!Y@m?2WcsM!A5LTI+gOjUuP>=?RxhL3m>43+ zvAZAxU*j?o_Cg^8r^(PmkfHDJ*b)%mKOfQ^>9=>C+^)A=f^w$!(J9uW@-; zb5?r}uyh=I9oN-J_t15a>GveA+r!>aIsgZPwG{0;F!=%2*2lE9b&IyPZb_`I2PSvn zcvKvZ#l-PgEOGqU>;<%jLzFTqs*ZK8WduO@++R3dLqk8lYYj5n&ViaP3 zPW1nJem&6#YwyhaLnl7KIK2@inM+yXaYNR!B=_HPkv5PF#%HjK=!RtXpB9ZGl=ZTO z7hbrKUV-xR0A6g*V9%kRLH?(|!hgtL!T(@UgU)x1?PDJ(t!LS#mL%Dwmhx0w%`Rol z2CiMgE_LkoLpv_p&h{;0!GXaA>S06=GU*8J`HIqkED}V&l3faV3&yXAXwP8 zY)PBHBmW`_1;W#A0BzqyzjCCpr(GF{U4EoBxzN)rQ%l5_gSP!3dM+I3 zvsVC9!Y+Am~#mbDj#N{frb z9gF*yE;BEv?^=mI90DD`i9Vdeeub9AccX%aar+f$$oQ(J2>aEx_a7N~^6)CQ@8Ye0 zRAVU{)n5r39R!Wu!u_*9%6_Hwm$qV{3zI}DcAMc15;b+T$i{S4)zxFVGGL zGC{I3?RkzoiUXx6_&C`C%u@@sR{!`}UC_v&*iTKb3kgVohRyx5+V z9n}rrdeO6ZUdPJ5u-6-=m!QwBp#STjk4ahv&n1ji;2@~H<_vffAWtjqiETdm$p&sL;sL609}4pFRJHQB42NROY*UbQUAUIoGg z17tWUFAY=&t5GQc5p8KHl;mpGhACmL(xw$5gov$joK6JM8~@D8`bQ#FrPXWZFIZDk zR@E@l?<*_w`ASQD!K$htUTjZqQ+cydXR7hdX=|I~t3iF}=JKXqX7l9adOW!~p6~h# z3;pz>e#z4HoTeR!Nh|mW8vPL{JEqX1Vpd_JQ!3-|^a-fKL*3jzK~ZT`vC zx)fh_OYa4J3op!Y?zZ}?DuTg^3byY>n`803&J`;8euS5}-gfhew50OKFEag~nGrgii zh7V3G$S!t-$*kn8o71X`YvNjm({vDq_j_ua<`k6_dcv#Lba$t3+4wCwj>xeXopYHc3NNoz>&yI?9yyW)1&WL?ZOL&`g_*umg~zs!AfRr2uGI> zFAP>z2JvEhR&?Cgy`poDGq23Q+#6UN?O4$ZsaQ%cwErr!pVk#>|H9~k={BUe9c;>$ z1Om&0aXXTn-2BW8&5ShF{DgsG8gFTqu*7umPc_}r7;alOys+=Wq5fcHRe(3Yv(}kY z>L2h17td>7zP$Z-_lnspu#CP-NwWx`;>{Per1gEjGaGNlhgqPC@2nmZ$|+%oh(BYkq=o(%WWXvcuomvidAXvwYYR`#+o zpX+v1e@2^Kl)4D6q@Zi=N=1_?Ep(cG2Y2e!d?K$GNjFUd2t(k&KyNQz-jWh8UhLK- z-N(9?Ea^Jdy<}mi_!w1%KUNHtlW6`bGqYRRH$d~-Aa9&P<7duRE!ofWhLiBs} zaklv6N%j4fmhH-y$|oLEmeGAL!F`|6?u+~~gwSI_l^K92Ao`>Ma!K)2Ac=C;lXK^g z1_aMKWzX4>D_wiZ$&+j`4A0xkm1X4pina!m&g zlHFz(&M`;^r($rI>XkYeI2hYAF|h&Vg$u^mT~{G>sa&n5)5mb1pCs;s?#^}}2i?cb zSgq5gjKQz|s|^zqW9(J-BX!$V>K0DNb(6>BF#3x^yQJGAwiJzma>+}fpj86GtDJpyViWr; zOW%oq81nJ|OuCBVM z$uV|DDS|fDE8Xx^B_hb;VcI?`^3YIum$M_Jh%FVS7Rk8Y3|7~nUWrZ@B?LMdPzMb#vU!VvA_s?thk{CQ7WoU#d6HUD>ksQPbdEFsq91bLL)c&2)ih9Q z$gm9Dc;kR2!%(`anZ3+UGPKm!w-{zW<@TL^&;6moodu!$i~VDzi|QU&Hn#lU`Xy!b zdu@-|++I8507xsb@rNZZVgPm^iI8&y8ZC z>SbWU!f_;#bP@=-4MGXW{Fevf2cZHez~H1T!)bM6dB7;1js&CM#CFI>_O4{>ce2eB zs1DC!XZ~;X>4ajCCmvA-5HKbB^bRVY?o`aYPjV9Naww)SJC(SfTRx|HunGHAbb;4w zKqqWAps?iFasah}w9O2=mtcedRawtj38~RkvX#Dqtn~SQG(j1^Nw*tskACXCpMCyB zcA-vRnEMDw?v1zRr$p{|JYLu>z5KJF0Xu_nwU93q98UZAb5RxpLkskT(JRWRhNH|6t!nL=jLB zBrHKtdAUCorLGYVfRzdABYwm(MT-=^~64R7}3<%aP2WnbY=dy%)Gyu2{fS=!Z- z=b?|2ld56z4)#OH@}TnN*w;Z^gYxAcf!9CWGdYH}x(KK?y{Hp#N7UNlMy7Z!wJC{Y~+*5VUIh$y;CW?VpOKdyrhS-TYS z41t48TF}$p7Uj~cs-nEq-y7)ldSDJfC?^v4e9T7%UikVFa*7KsF3b!h&0zn4aLdFG zi2FRpNr*o3#LrUdimT83ZuJI}P3J2+aG=bmvza!m?yB{NWSf3oYwJ9{O({WW-w$R# zMd#GFocWeh$4~ktyM1)&y(QtXaOi=>!(;y9`)fN)=I%N7d;B|R94N`(Sx`d1!*)n= zN0*?vA9{JY)Xd7B5~*||bfq3pJ94_!pfEEriV^CZQCbftwlWH?iT2x2@yawTnM{a% zN=;1}wX>9z_7nm+BMh{E=ITa%HI4N@b2U9XItQH0$9~qvpQFhHtyWoC5e^3eC{T-H zDwR#)@^E=sX`mzk!@sYX{4NQG@H4;TBN-xjtf>Y{1k*?CTxx}J!h5dK*Ex~-KCTTd zbEVy!c1ZV~GnW(w0)+)d{$SzEv7Q>z%h=ylb2(~n{KlW3@2A(~wWW(y-YG zv8I<6`!Rd`AwQfW(`O8&E*lYUgW15B|8vfm2o|X&jVsC(i(&Qd-4#Ksi5pfP+T$xx zEJ{;dU6W#wOT2p?*n4T&;>f+riOZK>Fjnjbms`wTwVa!kFHNjKryyG@(4RBBzmLUU z=ly)_zo?%NL4O>^@1*-9q7aduWT600^K^Z_jKlL_8RUh@QJ$D!=>?RfE+d*xZlRS%`7 zVW08x7a*>U3aC|UM+qfe-yvd z@khk*N9g!^9A6}_m4f=h&v{}u&bb@@?x}00yefXb zJNC#V{7rfvE)agBpVENgoNrURkmC_l4#)n1qXy+LZ`TWxf0O=O{678%{7%QIT}I{b zTBKgqA&?{y!e?Lzgfip+_M-?c*i2^9xtP&nSV(nIk(PIkGtBWb zkYi>86GpAL7&t_iqh-LkfdN8GMQP|y!HQ`ZMF10$K1?R#Xqq`yF&dH0W3eQ@AfZJx zs77p36mRJK$# zmjpZ&#pM(bnCbC(Y*5twiTbyEe*$|1LLBvVeo`=l{yM$OFQTP2nXOFwHvL#ErqJHK z_EU#@w>8(SZGmN?*j<=j5JFvm7tg%ttE%#4_^K-X_@jMd4{)JQ4xaa)uP-aFyP|HS zeeMNyY{`kS8xarkM{1Z^3fH(Ds_9MpFa63^>O(w0eoCqix`(iNHrU5hlSGB zqO!XW0vpBQkb$ey8wcQqhdQL!tv6$UA(%!nv~3LHBY+}XFn$1}P%_ZD1|aTfr_!!Z zoJg=9-n=|u@gg@_6wm^OW3D|n#g>B17m=4dGcPx3!Q(zI`mR1)Qd?Vs*DvJ2#P_E@ zvm2^H;o918sLD7trv9Kh6t1ZWhpL;$#=t?C6T2~w9>F{c=mYmljON56m`#wYV%8+i z*)QfA9mO0APSWn!6rW?b&*QKMHSsxnM4QV;_*{MzGxpco?=**l$`L+?jmi-|x2X+Z zg`b?TXK4NW0Ow%+{1(meVXQZw=Q96}@tgRycf<&Z*`Pdj3bU?`rm(*h=|FH521XS?$C_ z%8k9td*xeWi)!UxO#G!*nK$w2Iol6!xn=7uTfX-_{&tw?dn1>#MBh7AC#N5m|NQp> zPWSllhhL%dmt#C$7Qe^d&2blhrX-!;Q0UGIN<;m z02e&>sT`1k>?H*1TO;X_d@GR^HZBB>sU}QC;2Z&qGLc-JkUQqMuKP)^#Aria(nkN2 zUfGT-qm5Gna+Ve|lf?*RpIO^Wimtz?FdKm-ppLJwtf(}*AR7vRE6atE{#fbg$3)iG zVL$G|c8L5Unru|>5c2P?BfVQ&YS*+dANy|Xn-MD;pLkiwIe%cLoco`#vA#_&O89^CUL&1mRT4|08Kax(TasR6k?TS+#g(0X|f zXkr*+J1^(ti)cLycu@Y`7mCUN|n;hQAYtYn*5jm^1^&( z>IMB42>mPwolMV-cY2zuwB50#0xSs-2WI55H=_&^kDr5QoqnKWdZS^}*;i@j4MuY+ zE3mhK&Mc-^d~zWz3zG}B4UdtuE4=EUT;U}&FAvrw!uiKe+>KbGIdvXA4zphzc+jPO zfvZ|jyVex0TCb^KtkC_L{ev4f4#xJePW311ICQWp3m)1%rG7m>cD&#k*O3iRZaCs% zX(tsK+8F*RvF}aZA)7(JT4}D-CjBI834k``s8T>%anf0PB%9OWv^n;}2%Kh0O}hjl z&S7vmHoENSJ=|^S258smDASZKt+ztHHS}j@fLJDk{K+PhNbo|Wer~jD=AGySI@f73 zZu`vs_~1D?Zpk>b1FJ5YQWFLt9zzhPxpt*=D3=;k*y z_vkFF#9JF1WM6hf)~so8s5{j0BZWsU9a(mD(x~j@JDhtO+Tj=PZg_|`><5Z=7V*KM zu{p;655{K39wJ3bHKwF)bfgPjO#`pOgvM-Xwu=SNIx#Y{@N6BDJNcEwyL1lm?z!)h z;NkP#L%8kdpsw(&00K?$@w4jj*(g}TvW_1|PV9jC9Zst4tSpIAte7@n>^k7{fNyoN zm#~W64Eaue)KX{(j|pyVl5F5_r!DQ0bXYRLV8GA?d7PQdVRbryv#HZH^xLV{B-tXd zC&VTT87kIBD;TtaexZYDShUaQM)6ArqnPDX)UCF*c3GPZ$HSW386{DwO>CbgQgDzR6DL4t5I=bfz|^T!rAd?yw3uovFi_4wp?QHmurc zoafbuHN|Cg6UQh<%F*ti(V(p^J(3idkRMAl66&4x{ z<%I=>1$sky(fee)d4I;RI8Uo{5=$ekSK~Yy#)n+e?ftl%Y$EGKa&Z0KpGSA;TOe>o`WjZ zsXCScWH{gj(T>JsP>Ah-7ncIfNr9#WNLP}pc`m12XR|fhFa$N?S8)<;iOzMKsf#d) zC5gxzr!AU7f;Dc=nl_!|w6^EZM(kPaS(?_xiR{LtCONA*r!DF4XiGa^=CuXu9sXX0 zh5Xy^7QV~e_zm2RTTfT}jPSb5mnO@(_GH{YyTI{vR<8vyDf5T*m)x z+PF+;;}U;Q9-j$qoWwgHr&wW()zN^(KO_&>^g*L<+Q4apXZo;H8#@}a<en zgvJ|eICIE9`FPjOGTw3f@yz#R9S7zPZRa!norIp-Bh>Ag?;mdGw&uj9$5_1Ddju~8 z^l3_X{6jKJpvgJD9Oh{g6emPJbK8ACnSmAXZ^W{+?uwGiEoj z2mKQUZSh(BO=23sI9xx4J&5#Ep}n6GNiXwAA9W0na+>Kx%_~6tNgJnWrD;Yz;L@7= zUCd@vhU4GjnkH-sv_!VVwObMwk|UEjtq$k(`_N*i-6m~VN=r*tberfPDY`bW)<@im z+A!l{EPsHK&x5Y(3;H07Nx2fX!hcTJB`X6dRd}DVyd~+j3VUYa3XBno)jbm@zvfmr zwR7hKr`6vhLPo}ZX_s6Kn&(R%>1IKw3LErSsILH&0HIRtEG;!nbG1pb_x;&jBwND{#};*mUGYlaEUf!U#Vp1r@1&jk1iY2Eoo)6DH8XW*dx z*NHXY!9#3*g6YufH+QfHXkn4ZFGtMo&y)qgJ+G1Ojiv>Vk6g}Dfj;VIhTi|z`cJA4 znouDsXG}98er*4>BF+h82dBrx=5x?P*X^q>=s$e%nvEM*pXr)~GVW!YBCm}+aB0rf znTI#Nw(j=K%LS=mC;9+(q6Tg!dXVgdh$XxSG&~@zOq6O1d(>@H=Zr7GQDKidkT^G; zEK+pt_9Pxo8V%JhenfCn2fFQ!b3)6F6sg4 zt{GJoRc03+*ezchTU9OpWMX_2PDk~{vFk5aCt%~Hn8yE0&l!J^U2tHx?&oLzQmvnR z=H8Ly=cpDu=UwzCUGheYY*r)1hahHTMjT?gl+Lq9(R?GobA%%Wo)iW7h8JAlCfBP` zR-93unpT#fcKibmIH`WP_eWRUXtqH6HoqEM&u#hek#hS+KzZ;fvgtj@J=XI0ukiaD zoTfI)>Y#{2J(k`CMI{}g^dRlvqUPRdl!jTH0;{0#QzYn1MQW1i>p`qw>;&~){$|Ws ztrSc=Jresjd{;j=MFQ?&hP_{hHvEg=m+|j#Uf_HKd;fzsW0=*oI8Ocm6UAx*MJ!Gs zM2d+}O%Cay0~`gKfB?my5!bMxV{GVv`j6aP^$Z?4u|j@%_3BvdS@(mf3-`lKW$7T8 zNZy48;C>l)x}OpEb4T!Jf%-28m!R=zTt5?e}a|bz_D28mfk;j>}3G=fJ z!GQ=MlazIkT@57aCruEtG`9tq`tDD8VqxjauO-Z#Ts4@#lF#BQA)Z=ef8!R=MKT z6cMeNVNc0U%@TnENfDtb=#yWI_@KVcu#)dUK*tO0FKP<=OU#rA>fpQoN5;nfIyQDZ zvEfJ83=?|TU#93mRe?Ox8-ga4V23n)D(Yf^EC>@wH`|a$3m0BW5@oDb3!-u?mUc^= zGN0(OMqC#CL|5QJpi&Kh(BLl=l*-G_@VJW7o$1J#6?5?{syULVMv<;WIsQJ)$1vgL z)ceJi>=)zDGkQDo+*vf_RGdLaCcO&((k+N1K+#0N!h&-~iL*Ioo^W6)!W1krJ+J|1 zW#p2?%L13>oH=k?68}oi!-seP$O}N-5%UM~MX)uSOOwZb7OxLREjT64?rvC;p07Ky z@IjQQdIj^Cw+p^U!kI=aHIGXfMf>1sN7yIe*kpO0yogYtlTn}vY$Mqms5z#&DUg++ z`ax;Zi``NYpuEUHg}ROY(l@k7mbCbI81U^9tor1Pawj){wk4E~!EsD8P|XxO4&~yh zGsrRQOhZgqnmrA5z)X56m8B##E$%f(mszpDQgeInMeo_oaj$mlElvndp<3Xm_@z^5 z4`qn}*NWE`mPV0x1*0?{t^7PnDFAwXNq$LkVu1$5p(W~2Vu1&oqYWm$)lU1y{yPU* z9LV8PFVQ}HPJ6d_@}jxL#i;%6ne7vr3A(+7Ik}(9D7&;>%tg>`N*cunt{vVD5eYZz zVeb5x^$0G+9CE-%A*Gmgc6~Z$ABr^x@SlR9swCsj-!LV*rcZ)jB}53dbs^eX%EvgF zzc_GYVtCJM5S=Wao|9h);JM{#ThrLT#-9`WJXtR}ZN@;GzkoJW-}@Con*fke;eVSg zumm9`dQ=zC>)Z8lQk~D)D8UE9QqaozoK5$RVx8rUFziP&XN0#Tw}R|=E<%sW&q4KZ zPuv|!`0~L26ZSX}%r1;{(+A_LGLHd?`#`(-xD=l4_?#Lc;-_^OqhIhbDwcL?{i3)o zA{j#L42mA10+1^t7cg$TFdgK=TymZT=|pId`#E+#N-i*4E9o`+GQiExO$ygS`=aRY%Z(8^Ao-{YA$EID>d~t@~uD6AUOKf@W z%A7rWaw^?g&ST#UozS?-DqYTvV(UTY;3N;F6dKmMwVy-=R1e-2W!rpW+X#QWl@)8*tx0;l7_RF9wL!{J#x* zk5Ql`5}GJV6Fjj7GD3xn2qZ1S{);3dm;IauCYdz=C=E*sG`Cxp+B|JvPMYMc|Qh+K*Yv!Tm$Ymx&l zWcc+YxejzEn-tLK$&eIE)7+#}b|~94?`V3$I~pLP5gjCZ9{nHTO`Bo;aZ*faT<1d!)NM3Bpk2HqU&Q%1z_DyTa$iDdE()|z zi@a5Si6YUM)S|_dD3UwMCp5Co#HKEx-C_IHg5A3MFlTt}nq2Bqq+{4YpgB`7%?OVdN- zQN|;5WmQX1d;82XTWWpI{xO`(zl# z=u+I`%Dq~+&^5mA-~Hr0FXP!=h=H60kNYUsh|)(y$`0uzl!61U5_v|HJ1|JO0|Y>d zoO{Z+Poz{J$3}x_JxBUXQjQIKZuYd8q|BL1XJo!irNp4SX!9WAo1DDOK;%Hl0Rf*2 z5yrskq*Ec@kD5A&v-M~%Y)MP;(T67cJ!Pm{o-07HKQ4c9@MAL*Fa3xN#3`M@D21Ya zYWa+$1(ExQ10q#{pkm{i0@o5qM?ioKU764i#JzAMXa?vdnR#%W=MnN0<>Y&5vHv#- zl>ou}WJd!3YGV%m{WAZq(G%YvC2YETlFhL?#HzQ;jLHO>zGD z;K7N52iX>Osd}e+C(c6%EBp8Jx!%LCXZbhX^vF#&C2qjma0%}5YkrTcs0)cqP<{xk z2jY@)h0|q}&h>by93@+ihff@2+t@bs4jjZucjC`C&gHd3wng^}tTY zst zK&ht;fD#Qi9rr`Zr`%TC9ZXZ(`x%m{=3_}i_CLDNFC z4h!VvkQro16$_=PD@{4UAZd{}16#CnoHF=ynWxm7u1E2DfEC68IgnOJfgmDx3cq5< z!uMrlRbg*legW^R3O-BTAowBw4F1) zT5q!E=N`z)wOPs@yv7l>6*M^<&G{J(?sZ&VEuS2bop@eAdEo}ygS?>aK~}Oyk$y$> z*9&R(l95nL{$~x+qV#lR?G&aLLQAIylSKyA{0PF9aE58QMS#xq@3IszSqpNm%g(V` zO4p%P1vv+@a?-4&^4rs!HT@gO*cI#~D!ji3f71(-2j#`kiC&Pf>y{j}hn5$U!-;7h zFcwVuCVLdR8?{~FdWymhXf}?<=`^#s?3N@#&1@3s(75Maz}ur|qY9YVzsm~}`}e75 zI-?*Chy_W9N4nEx_#_yW+90x^9wZN1zU8_yi!C?nKyJR(#2#JeZpg@Qb~u^}Y+(m3 z20sz~6^;JTm<9cz?|+{g8{5#H6TZrBoAr~!}K3SG>hBA|FUOKp1KirBX2l) z>Za4DLx1?g@BTzD-p?#Nr{jO~9I8fP^zgqd_3)WLt;d?IU>y9{I^A9o@oIcQe7-NsYPDXZv zmjO*|P+lhg7uxpU`2AA+&hcz%OD5bxjQBpjGs7~k{K@!FjpGe9m}1Yc1{#h17>7E2z0p>;(yHW<;V+lb6~W6}M=vLSYa1RO z?mN(jzx393pmU(+%QXWvCvWb0U>DvKpIPabyP&qBX|eCpzDxc<3L#eFD&ztgc`gvS zLIP%HfX~AqUm_u03KRTdFa{e1t$^TeG%Az<-%Mq^d>BAf*fw5r?hADAl;nm>m{eI_ zQWLI$0lfeSe>R~1hge9f@ti>ar35{s6+l}|{X#~IrjNaa z9!1iqxOn{Xbc0Rd{-42U^_s!{g(V>-ja)dme$D#jOBb&0U)|FkZEb08s1Nm(^m&Vm z3K28uL@f*8VK_DO2Q5NkEh3_3qAUz2DwQN7fsH~+hPcb<9Oq%x!8s@_2hqk+;E+X{ z{N>HzOP6#(XRBCV-!xD?R^}}Tq?(^uxc~62*Y`Q|hhjJ2c&FRtu8bcq^ru+ZbvRhC zR?aG@bmr}}1Q<3DwyEUVj)Ni0Q9$DUgc?c2NKGQkIT965CNt#{vWVDByaZrOdsW!LPu zX3OSv7mW-Ltyy?!|E1^VjJ*HLoS``!|4Htd#!9actH2eJV3mSEie-wG#+xrBR?5t* zTo!3uO{`WJNChk1&`Yf3JnP^TpT%x2sK_AZs;HL75_~3Kp7{8`V!2Psk5i1P#9jtp zk{ssffYS3(Cz4ck1XIA0V-W@GSjw7Z$dzbWKLxkquD}$ zHkzDH!P*ULlND{RG#-&(?^{ce(+L;DYh|5hU?2Dbf@{1T+fg5H_I?k&bbJ zQ^HE9Kr_XBe3ZbU$Sxsr0Dm_nPW&+GsmEs>`xjVD4ShjKlB&dL0j=yO=Nz#=j{T5J zi77$Qz!R#OdKa*!{$(cC6h)<_>;tbP8T-f*FcmuH_e&bENy6+$p<~8bVapJKBsJ`f56 zm@KlmjB$4v_2n$=lCw>BJOcpqn-#mnh&V0e*x3mU46t~BE;$x!hgE}H^XMtar~W^% zYgED>8qnSHEoi;ETX>ecJaG)XayjP}tF%gs5rN{!xrILzUO4hbaB#AGd`jjRI-p^D zp=~H(I&1Uv8uL)|)aX9>bZlYlwS93OQaS)yrc9igJSKg}{U{U{Mv)9;&yx;n7YZGl^E|v(CoLIjPeV6}huRK5Y z47)2MU^e?R)XgBq#Mi`2J-$zU@YX-3IuxBfcEW(zB*v&PK1Rgf6ctHm zJuqj(w~U5UK!ksK_#~eQXNOM?Ml;V9K}l0H{+~8-PsRFVyu|4J0YC)5;iCs8nD`d> zXfCI(8ED5#q83M*#e~}73F_K7!XmbABo8=gT_h;Z+H)f=hEn(dqlb+HdIxjW6gj7n z@>}~@eS(e_OA;N=qT!TGv`SjZZ3;%(IVYb7yOpWn(**Un0O8&OuzMQ5 zO$+KxBAF&BHEGFdMPr$emno=pH!s>^WCm0!nv$4C14B-<0V2$x+bAR_z&C~DbY%cM zE=5YQJK)K$io1lz65~haoi=WuSSo3^bERGD7WXI{Q^#6 z2aN&sHH17LG$Z@KYW|zeCck9P+_8M%GV}HF314l>GsZI2XQ+nR45~C!bEF~mOSu7Y zpKJO~QRqPq1;tTzEkc4eS_pnXh5$xoV%mxpk%h-74@Lm-TCE``!T*%Mzw>?kZymeQoTA7nCc^bW ztaBLHJRa2!BATO-Gi(l@WUu1B1i5aqk{~mJV5zyq7i^4 z+IiD)dvX!;#G@_*Vh-&XTwz&G;*xPb|rA_~ugxaHC5KkI! z=DsJx_=D0~v{>L0Ibr+p!m@CW=pTYy(G_{XZcSHE9?CpmF#$6Jg3hp>!kJUB%bV(F z8BXRw&Je}~y&*0#nbB`?2_j>)(`jc|>+=;+5oV56Br=hTyhYw(Pi}Tbp{p=C23gR7 zVEmH%O*lm$MI5Y;N1KsePQR(IX$1I*T=n6-auw5Q-zPo?lxY)wNFvGD3@9V6dxI{> z;imjA5#Rp*i+FA#9mR9=ckUNVV8(m~!K<$bV#aA|dsia$G|TL!Mu-LeVT zZA3@H+=lCIrfZysE6)bl%(Mo@{-H}B|AQ`_-O8T;*`olnwn#R}JGJq_S_7j={LV(W zPNzreA#@dydgoF-p_jxTMg1Rt*s$)y`^8H(Z`tzKty}-HWeeo#!-+l8mZ#G3Fj6=P zKhuw8UeR4f>V%}}*`oC_f!5tX3Qo-=h_G?sa6?v$(w?>2c(+PvC$X$4{Q1Cl}>B`l+XMgV5eJ3x+`+ z|3XqLfSoNY835A$E&Ms^DE8&oVBZ_m2UHj0`3%abpK?^YIHoAh*$;fYTc8QhIdeb= zU{nH`AqgK3di<-CPq1qFt2__D6DcT0e0Cl$ZyM4n%cTz z0fGpH-KDF1dxiZ6ZaX+r#TLdGiHj@u_{sO*|I}*TptgHpLom!E(3Nq%&Jhg z3AFqp3>#)Mkj2u>uoh4@9-2IS)k`DHA_Ii=R?{K2h)$weA05?oHs^s;+(E zv(M2y*_tfNk_Sna`2*$#D|M9LT`jq%MRMS}1*ew532PJSb3F zrj`PwE#>wDN_$It+XCgnEgdf9(U$aaTS{pXpM3wd&yg%oN$7j;`+Wh8mUQ-Bd+#;= z*IIZA&Pv0?wxb8KyFfmbesp~+lysbZM;=w`<7av!59X z;>9z^Gpgx2?MWo0Kb(4IG_A>vtYnFZ3SMjsdvG% z-s5Kzs8Nz(D%^|X(*V`UV2HMW+lrJ#OJ_9X;AfOdo1@?f21oFF`Dfggy*?(99IR zLvx}&16CoKjw3#nag-u&81}tJ0tpd)B(bmA0$oQ#B+Q%?&UxqxnUmt-^Lku!O1ao6 z?Ke3MPn@8Ab^;gVG~_bIO8Ln-*E@@2cJAd;kRqW-Aal75kN@{K-njXjr|!*O?lt*U zx*X2lkt~mjP&1ObJPLW%&*C#*=X@-p*8Ss^KB$o1Bj~tz# z@GImCz?}@uiRw5e$Tot-Fo`}&MgyV*=tL?ZT!r1J*=fom1W-}PXqd2~pkKEklYjJ~ zzuJPY{!!>y5uUky|Vr8w&aGzsET* z7x&_PN2b1jw@`EO9lU?)tMJK9Des@6vPfNx$PszBD{M4YxYn>OM*-RV?kC0S_bqmf1q(fWaX*&Jy_?Ru`Sw7~h-+ShuedJ=Ecian(T{`s}=pipi77!yJS~R*o6?>Il zRL~%dS`Fbk$on=2S42Bl_59gW(~c5&0Wm1Iy1rU?L-(_r$zc+4%6r7V)8kX$68q%6 zt_ric!pT0nU%K-2aqmrk#cFHp<`1qTC*haa1a`rvKcF38H@}CPl^?)QXoR1z23U?3 z;j`(I`k>!SwJ2NaLJl*^PeB=}X25n2Qja7wE+!+(2PX=(ftsZJR*k6HMw6Jpe(FZR zP${vt;752&alyah2XMq`Z=yn#%ZqAGNFwwkNv8^DXFk&lNw|cCT#p|dUNiMW_Kf@n zCzFdk+cDu>vw8Q%t_^#S43w_hcyP;gC`wszU1cl=x1%=n&XHfh--NPeR{b}Mq*pQy z$ZHIH)(qtRXknkDbn3}7huOQJV-s>r77HIshZeWB*2W?s%BTY!QToBo#2{8YNecI1 z=QJ^$)ccB(gy#}*&Yw8(=De#A4Vne6-UOPFf}lM+&1Sl87b2hh0x%bDedM-)bB*c@O&XJf2TuD$%4m{TdC*@Fe4?a zAe)laJ?CdAM~gm9g?-NYlbIhu3V&41m~rm@B6Hp>#^cz&@Fjf} z`plF%rZ9OPKt2o%vF8#Of|p^`{t-J zM&mC&`qCvg>dgO{W~0?K00iR2#}Ebf>$r>p|pI>?m~s<_*^_K-!E=6#<4k zyLUHWNWZ;#^b|QdHpJtn?rF%778Vxv@S;U&oYh9b53Kj)h}F80*J0Da0*copaW1uP zEpKT7)kD>|(ChHW?8kC~)wu!&W56N*On$`~G8jTm|NlDj?z=}WyTfE*=q7X59cD8V zEvR4u8?F;Jz^}OvATmWLcGzXV$gJl%eJ0#Yo(6-=?=#7W2qmd&>JhjipU_-!>f=wy zNo=v-{+7@B#DRC|Sjd=qc0aMdaohQ9J`SH zzCJ8FF7EDZZ*EF7)J8*&@=&Ne6mU3^q0L2jB3>dYec<>bo|m8}oKi+1 z6s#3PK^t!TQNp7XLdn`3`h8Z4Hb=@m4!^%V9&cTS0G0zY>M1v)>eThRF}{Re)@;E82zjRI;J292CbtyThsyTq>ZR zdqm^RW0uySuT7ybXs%a0XTK~c@s`Gd;f6p_k=0w`KbA4i$FN%}lKFnRVb0AuefTDEjaPr9qUwW+bbwz@K0jz;mr zzF{-MycrFJJPO(w>XMeWDK+*+_8s8ixwJ;!1JwuN8yUo&+H*FI>hx6#sa*xT2fHd8 zLs$Wax3G+_K#||+EZjBN6-k!+XbB3->YEqTylRQog$3c-_WoSzd)>L~fjy(>EyP2% z2#*6(#mw3~#4Ivq-zC$*feI)966J`c6y!_i=bt@c#UQlC1XmlW??I8(>g$C zB7IBtt6DSB|NOr@mabvK#`SAgj|?qa+PipB7y2=zh?P{qf-LbP?;+o2h1R=;Z6R=e zn6n7-^We}E)6G)Ga=6WTKWEfYEK#x=DfW`_ zlAusTL2T;R?0fPX&M*LNPIf=DQkB9>nUsMPS&Tmrf3DQ}JM|8(^&P~HiUstp^%wGl z8}*VIf6a70HY%0!OKMXyFFCW07q$gRXKPfsn1JN`I3C+0y^i=Fh=rKCv-%vi5K>bNIR55_csd}hGsR{TE zwLfVj_xP&k7v~llwN5t@>x@-Y`uu_2)dTglcpmW;2gE$N%iZ^4X)IO}@V76iSzJj^ z%S-KtOVK!M>OYVd)($LsudrJ9SH+=huJ?)-X)y*N4*JrWSICbU1YD3j&drFT0W5Bz z>H+E;MD)8gYlK}kW)^ftGuo~i4B8146+p^_(P$b+_p}Zwuh*5~ZgYPE!MSt)0{Z#% z_o1TC^1jvmtJ9qwZB31}(MY(oI7_Eq)|-c;HZPr0V0lVP3^LaiNIQZhOf{hZgVQ4A zP0dF_KIhJOZ@j9q$QRrlT~-%sk5xB@<4euRNg6rSIh~X=vYe8V_U}i!Qc*AaX&@8| z;PxkfWtA`JZRx66Qb|t}Q8!YRI`;L;e?hX+@;yjaD(PD~jAW$;H|-clD$}E0|3s{E zPgN9ml`-t0srOWSh|BRd0!lH&W3qDGq%&PhJ4uUtcOy2FQA={1nUMrY9kGo%VK>lK z_{ta_v}`LPI9kT#c;=fr4ff>ue>E$|sXRJlc+iQvFv;;$iUdqnR^vIS3Ar*Y(-ql{ z@^0v9d|ye}SLAdQc>{nsf}=r-Kw`(sJ4bd^I(*(hJ>Jbq_i!j$>MilSN?4883?*d+ zxdLNpJ0y7S8?NGFCu81ByphS9gkP~I*tdkFaLxZmBAN*ebv0EL;b57+sKAwP<0m=E zk|d&M?9j6c;#H8eP-R>mh*XlaUPYva>206W<=MTwlO(N=1aP4KS6rUXke-=$+6FT8 z;qvs;>YiFJJd`%CMI)^-CL%vx-Bis&zA67&!C(@MhJy}fXZGW0f&xq|ydp-EunQ&J zNux22@h3*MmE#?aw5wXOUq#m6xK2B>sF2o4)iU7NNpum(HVkjUJiwk zbn}!mqA1?E8!e-V<8j7@BhjiV?$eLR*v01iy~|S;O4TZo)KTmY#lzuv$Y1Q>YI z(ib2Rc0q`ViF-$L{%_q^17~A zy<%VfBM;jyTHDYH8>|^`?H^pRi-7CkuODEZg?yesA_PhhB!TNvqCkoU1;Y@d7BElj ze2GLi%0vO4whLd>plGN@)1jgCYdG<%BH{9~QU^-Sp%B$C~YZc9P)6=@= ze#eCa)s5N75AAh)9Xj+kQGfBTm}eIu?lDi1)nLp#(1D}hS%LIc^3eiQ4x@!;$e~75 z78TF&CBzFbKPt2}CsHwYelw#Lvld%^V0`n`3tYjgL@bQYbjji>Wf_meo5byMAM-my zCW4z=ySwX=ih9Aw=6R-_yPEeby!=CZ9N(Y`O(7*-+={qFBVsBx-Fx8zAC6OuJ-qJUW2P*-OY+OKt|JA1o&TT;o6rVg}c0h_O^ z;1wqekn5on8d;+qL<*2D(U=O^i77$(NYM_b@{`aqXzM2e{Oz3A$f^$~lFDBLnJ3KN zySuI7k+*k$tUO)ayyWt24?bAan5<@BZ%b|Me)Lg1uVKTxPCvhYf4rkZJa*AV4V|6I z@vgo)`H2G;L=Z&_;WGlY|X#~xwkEpa+ zF04gz+)m-r^dT>U);?%8iF#^zgN%qgJ=dkJx_w9jo)~Ev4KfQMO_8&au+zn;K2$BN zN{1+P@z;_qiBm%H8Ts_mEzS9=!-sGm7z%EF+%? zHkIB4v?|rJSJ1`_)X}Eu;|$U)w}$rA2kgg}vmc*&mp>5LSp`&6KKPwEIBFV$6S%!i z>Gg0i3JMq>MGzReIUyvY*AY@5@^Ty?d}AKE1nP%fIc2_G9v4z7av}EfmieZ_dkD+S z7$Oa~o&ES9x3C}IfRX+Of54cP!K%YObO>m@ht3G>fipir$l_XYl#VCZ^5hvmd-cpC z@I>vSx{gYRIv-F{Fe2eGBGJSJu&@=OSH-=7@~Ci5X*KX{K#3sv#Xn8P8^jz*Yhf#} zb#R5C#5F@9PN&_5QnD}zyv$2=>Zm*<+(uBWvR)%7REo2R9W0F$dGbq^H`ivKyy^0~ zg7C$4IE$R6emv5+@lPDZe&+d%5Twv&kdcakV9&0@19DM0sGCq_**r27iE2>tIk6)F z0&>bH#8LSKbH>EUSZrz%mJxndXhc5sBGeTpY!vce9eEZBE*~CfWTF<9HiWmt#Wt|T z+Fex76x1RX&#Uvn)<%^nq1cBa$U$=0KxuU`mqEviN%HJwG8$1kfe>Ii9rBoS?83jd zjy5!mwk}$yuRA!g=aMGvx~8i7`YPN^4P)&OwvWYE_Iy%)Uw-qJrDGQ+V_%InHb%c1 zOF|A|+{x^?D`+R4Yvjm|LCOmjW|zRXC<0x`c0!pW{ctEma|l(0Dv&KUYbNINn~8*t zx$tc66OCt`ko@mxYA{_l)XXy#u-{;oN}A)?c~p824^Swk^Ph89+L=tiB7|UchNSj; z?z#G&r*8Vx)t^EUN%oZ7dsx1j9YtK`%=@VGhPtL=g>)CzUZ;JYwg4)KzAwb_{4T)z*fBb#?ItXLzmhxu&JH zbwTCBf~2gmI_D~3S-QU*CmG%F0)R4!gM^7hkXPjDIqVUfb3SW4VAf!8m^ICq`ZeXw zl+O7%X9{~UEi=M1%6U|1RCC>sGJ6Zk6t?>1XVf-UGcl2s=E`v#FDKhI5KBCZQ)Q zdn`ouAMG$Qd_YnX7$VNqv1p~QMHl7ESi2*H0U!+bgOj!A6)KP_G z!3{z;;=&c+u6h9|W(XNzVpMh=mirLwg+D2C01zEW~XzU@uhPTsIDGts5@n3?@)k=qp5r z=3M4t0$IRBvJ8cKH}FzOSYAmANGK|pa)z7*1y88_H`XZz*<7Ft2_CWJkggpb?|?|@ynZp4?az$e9P&-p=)sUUHhmFGstb8RvtQTlB{3b`dI zYab3Hv0i>+G=~cpQ5kxoQ}@k~~Hu zqQSCjF>cBr!(bsEkWz}ebyT!LCm>=Bhxd%nX^+qIF+Oe->$1!qx08?C8FEJwcI*)5 z#F*o$Z@v48^|AHqv%`L!-5?*PQKPyt&QB|J9tn}de9EvOK1|UMUhfOuSR5u~)5FrF z8k!m?KoFI~Rx8l%`Bs2^w195{QmV(cgkW@r6O!V?p>q;?@~&^m$A7!pn6J^~8{b7e z87%icw!dg(2eicr=TyAC4sEe0<3kCG(wFQVKPz(4?=@5=qZ~{)dyih zK*!iE=nMu9rZJ2Q2Au))OU>3K&-0sOY1B@Gb`mTqE<#XariMw{30pwBoN)x^Q8k~N zc?v3AUT1(bPdswX^Ym-8Pr)=_`w}Kh^)({ta;OGa3Y{>J!y#}Qz#$9<((Ap%e#1sl zn~LZaCMExfHL)i7M{mFVc0CvqUZ5)7+i(Bj4^uCK^3dI@ppVo^ZXu#OO1e98N8-eI zgb|!U1J0m_^12F;asyoyTLRZYwP1`0=L$vO@KKN8ak~)?BlM9m~RfKWsFNn8y35txn`=i8nLnay2-PmH41esp%4jT-3MaHjESJdNU{C5tmo@hp^qqCRBdeDd z6?YT|wqajX_`UtBo;`2?`+tyfs3eDo+zD(wz3wob0=!>~MT`QysuQoLEh*H39(axX z9y@VJyrXhOQZT4r<+OsK__(Lr_gIiM0v2gyR#M?eE$4-h`ONrS?pm?!d#pWFe6_4+ z|JXXhqyzE|#pMV1D#KPV#_H)_n+@AzZH53^i5db>X_w;=+(Olq=TH zVKZb!{0yymuB||aB-zuGq~n2`2q{nY!SqdjK>2`rO!y&@Q1dtmcf$AiynN@Ww<#*h zTm?nd+vXjy!U|^%@(4@+e9oZcwv2vO0{+(u{^x=`0!o+*WJnP~PYM)Oe_)zjRU~4@vQcZkvZ1u*Wp^ey%=$r8l=aY2y!Ci1m=j(KSlv%e!MM$}+pf;>oHn#q<+i$-NjDRh<^Mx1g7$pWfr>ag#r-vw4j?R}uS zW$0nLodI|N+!kMN?_amn>yIZkBG~91813(fFVg83nKmweYV?rh3SDFQ_tWX`d?%g$ zekiWL!g6rkQ_DAYB$M#oWX`EF)~GO$?wfbCXkDfn+h8W^;Mvikpe(e>;i@X8V(W89 zPh0D}M~}K*?geJ>I6HmLN&HJ@{|V@YFpB+0vL8qh%ncD82oBEKxi6K9D^PMHXD+0F zX(9aGD5@2P4@w-JdW||7UL-wudR0tXMc)Ts%lxJe5p?7W8IjP5#U#Q3mkJdRsvfC$ z-0qO*$59SSQSLH7E(Mr2z{JtGT+LoV$HZ6VN`4J#^7LylsR}=($+?b2@C#vKO?qXx z6jhtKvrC-_shC=gsD(7wU^Pw>ONG;&Quwp3m{(tlvlq67VX2fDite1p9gxn^YK zYIX3M-nOZ!_9avQ-hH5t|DEn1k=uHgw9lB29=_O8VKBXv=!ql&rNRqnv`f&NfMC!A zP%^zjm!~Yh6h>)>URDPx5wwv6#{}Fc!jRM5nwC^(Y#Cpc{GW|g$Uz&3Z|4um>%H%$Qoo2_lT5ov(xE7 z+Zr_cLpd9zvX?PKdN_<(z%BtFMz&u7wqItb?7i<^@wBYXvt2Cz`$aaFx<68>(+`S& zM>Vp?r#|gj--xbOTNmVFP$Cr;+ ztBe;8Ag9?xmB`do&Fpw#3pwp9kg-kR?xyiReZ_a>Lo9N!&92orY_DX`j5ImrqPl6~ zJ5yckON|=}3f4DH{iJ)=cnz?a?8Jc)7D9HYL2Ee2d9qp6VP(z}L#GM5ll1j~UC(#x zN!fA|>biryck-k{I@>Aq!1(X-e?V=Q*RuU5Pr^$$eU@oYl3oD6EL_q|-If**khUdi zQUS8e>u?%$Mnmo?L#!UB%v*@%#TqEe0fGfCWs~MCxh=Eg7;5g;#PSD-F=iYA%KB$E zs{9)wX6BW;3FMEkV<_39?%1h+XUG1Mc>&U5wA1q{pnzX)xEkF3- zctJ7x*=kYoepli6Fbvo28l7zVFF7dQimjPugA;|jn--UC3UBLz4LWGsCWIX zc6*yoT-w)u`VZnwWEjjmM`U&65&cjEA$!P)-QtjQZVOd?7PqB6fV>^-;u*)?l(Y5> zX9nZS3Kykit}0}tN%`;0DC_T(b&Q&;&N$b4_!V42Te{ihL~bo?2j!TczWcm#hsNTt z(+=com>~~xQ|(gNuB1(1^J$fpyIelTKH@~mL=Y!ABlD!mkDO$e%KN_~kA0gGE^P?X zUvUz{f^M(wG3flG&`xwbZ{P5!AU;w_1t(_i!S(5N9_R*zb`yH~nqdV)zoICcRyPT~ zU-9EY2E%O+lEO!#lXlLL0JJIn0FR*^D;WZ`;u)?4&QDSb5DS9WqYMl=OW~(SAxdR( z@8wrnjYXz)yKldJ_d2UrZ>`GDOfkl;M*UR1*Si0O7xv|QbtnMMKK6fw?#LWtS~c-o z3M9kI(1`5vF;4g$Idqq=KY2u6^c{AeO84zYXYwm0qUjTMq_^iW_%kFeT;bSuW{YVO z>%;jMOjjWIAy27?vXz-FI`a+z{K~Wr08KV-!!w25!%bO8Jd4+@$L>Y(bK1ZeVU@4$ zUP(`tJp<^(jWkD?zr?xp=wT<0eky%CPL?ArKlD`VdrWxmDfxNK<c2lMPuzJY-`Wp6FlTT35&6DP@$j`;m_cvS8xGP;upeksKvpT|4q|kdV6r0GV>0II z31+S*_|k3amlZmExNjp}q@8uT%2+0Z2I z`Z>^%K>z{CrI$Ht*z_d=&XN_DQ<2jm=`~&ym!q?!hJ1CXgs8(t$mmrHo0|75`Q4Zo zfiUKtB~+9pjgXi5@nC_$Oi2$^S-fm{&t7T8fl*GAsmN%pw7-{`hWt^a`slp*`(Aiq zKMorBCdQI8e$ts?`BR1-*O^u31xZ9&GeK}NYO@}yE_i}bk4ikiy3%;QCFVbqX8q|U`6$mQ+sA%L#Uq(bymi|N6RYdHr?v-oKliP}j__Do1oF1mI0m2gK3S zZ&G{bHoLvmKlOE7amLuFzQfXVF91Q@l?9{$qVJP%)({6eN+0H)xvWM3H#e#8+%t!2 z!GOeOHe8mR;3Cclf&}`^QF+C(#hl$wU;G^Y(Ry+}2mEw^I}7M+NA9^y?2H?6)Ko(f z*TJge&7VbSm^ZO8LSNJ4OK4()-J@KHTp1^=M=if#Y4*^kZ@>N1Qy(XbQ51%N3(SX& z?hwjom*zn~VhA-Nj&K}PBOt*pWv3#a2Rn5FjsmvA`Ik_Z(sPm#Ooaf<0F| zUPMA3H;k(sAi+@T!o?R}wRKg;ph41XuTLcE8xoC=iH`K<#)DfvJ8`h1>6%+vL386L z8k=#KXrXn0uM&gY%YBtVi2{y`tSte+Z#B_cR4e7-DB;6Aj>DrNK+xlhNTcJ96g}cH z)Df3^ERr65GGs@jox3gA67ISr$A$cDTK6mATY5u$ll$x30!|TV#H-Ix!9XnaQNU9P z+A&XP(BUB8ox|;bSjp|8PJRHUsJq}Y2z*g$j6?=Mo9?838)9)cvF&1ePBV45+<&du z^|NsY6y{NC1kOM6T-<@jiJ9Yq@YP7|pZe6dzI^qUzx>mi_OqiI-~o|XcmOrlANufq z1F}L8E($%sG(W%d&de4D#r6rWWqpqU0Z~Lh>~%JXiUVGaW+}X2f`&X|(+(iCg-4`7 z;Jrvoih-H8TRc_|6eOVh$^WTRa4tk)I&?k9!D!CLtmknpemxVc#{8KOhu^LO^A8B0 zPQ&qvBjqb3$$|)+0}-VLd<0Mr>;zFdh%Q3l&&YfOi0e2)9~zPDQJr2OD_+!Wg%Ym; zN<5MM4ty|(H}WrOCeQkLBq1c3S}Ga3+Zj@$itHpcrm9 zq9lb8d)!s-s@aXFmHGVX>B627vz~oLPT2~oOYp*EkUzsdMK5p-=(CN&pHLs@N$9f( zJz#k7VV}8|YN&}%p1D`;(=<2E(||8a0xTxr_v&HA79$n}{zw+Ah7r?%a}&r~6_5h& z1_4s=06+@-p8%wkmY|-YfNp?QrBz;!qu5!DU$JAdX2dQ?($Fcv+|*V%sT@l&*PlFc z*##F|7AMc<1G8MTY&E$=(Q~^e~c>1pRSvBk*aI4TeCpnMEbWWfhIE-NsCKG;-InQCJZ|@**5! zJh)X9EDDzRDY)fQqFWT)A}pj5KtY5DqeL8w^C=1it5DrkCb*>pv_w8=9^{(jVXZIo z`x%j~V_)M@t(@2vr*{Z5GIQS~0jDrI3L3*fIZKpr4>)%?Q#T6y2e&h~xnZ;-s3nCC ztOs@2?{UcZI)Z2Ocoq+3;iQVvanJ?I4OARFQeAB{#pJ)L#Iu+OQ&N5v1+zme_5(GZ zCBBJ(mi%pqhI{F!C7~aF?0ha0N)SaEO|K~i)kDxj;bbD12weK)mL$7_QpON$)xkZ< zUHlxgN{}RY_{&Q3ARmw}Q(jtER!3k}TS;CC!d{5*(ALpn4KlkQ=m?&got2;DsZ1u} znp9wlt^yX_b=6h7aJ$*%FZAXI8WV{|>w4Vk)YqoVceCo=&M5PtCSO zDwVi#K)x-NxNU&mIFLxqgS7&-4D;exE2ueU^ta$XA)4d=Wt3I!iCGXS^IBn9Ss`xn z3+j`FaWg;TAEXbfH(F2NtORi@-!CrsnW0zEY;lsX2^HTmpp(#j6a*#IIk+T-34K^p z==K-*6?=SeD0$BNGZIoXHRrAKur?)waa>m;68=|G!*I)rdet~)V`sB^k_(w9x2^rKQ^<%G~Itzp&yv2}^3ZMsnX2T9U^|@tI zfH?!~<;PfX>d$|ZPe}dp#qiqSgSjyr?2X7vxe~vj`-WCR?`LPqsH=<&+2>b=J%sH&v6ZxFf1+> zsjv7v-`lbB+2>Y2_bk63S^4bqtDk>v72bIs--`(Cs0)2F|DKNeQimh@R0P*cao}0( z)vrGA%UJZ64}A5P?Z007xz8>A^&9=T$53&oz}3Vw^gw|3ux1nK&G58|C{_ar4NsYP ztr-i5Ni(jQ!*rxfpmCVj*7fJZ+C_3UcLbn13fL+* zgcWj#MMC-zJiieot0*BM)|F7Yi-p{*$#-wj%A42T`~ZAOR(;vNyrJGrYY!b+zpa;j z5{U6(`O%+L`K;y6!KuHp;mOH6AHM#2j3aVpN=#x7aGOGPu&cf|3u1>FFiE2TFNGOla-GgFhA_Df1rK!w-HJdNl2_VWTT+eSE}^nz zfq~LuuZJ@7a4}1O40tD*xNa8`=1BvMASOENKo#dK<65#8u>VT{`J0F7`|^v13R(B$oT*#Q|@@XriOHJ<+mEKO_ddi9&yY zbA40ivPHGF8>k zj6EO<0X3Jm3V2YUBYnKVANIzqhJ6S`z)YcLX24z}CL|yYW{6rn#p|n6HK|IZu|Vy} z2MQW3o@O`fgXVFT)LChUMZ=aTUHj(CkPQVwp#W}=QRBWE4FEN!Sd_(ZEnAOeim?T~@vat$n3bT!|A}m5w%Y-(SokB|6jC3#MtiUe`oo%`; z*01O7h?Q-uH{A|v!B$vmzr5*ASlAla8_kr*hqGmZ7NEpEc^OyMyd13;kWYwQJ@hUV z%r-L>u`mgyFp9d8)e50Y`Zc!=?eDMIwQJ4Rp{Xf;Tu@*H+Q!AO2MCILA?PVhU}-whphsD_!1U)} zU>}|DgXy6188(R@Ti`>eLx?Pficp}e#AieY7onB4P!=9@;S6Ti_3u`YQAV_RSqFOF)!R zcYJsg+iGlT2*mXkL$W@UfT~11J|^rJYd~W=qEcPLgG&AfQMd)h72J_X{RT4VVn~u1 zIB>y0K`Vo1H>w+9sQ`W_Ks5XW07Sz%s(jaoh0C1|CUms7HYXbD zYNOTVUEwZ&5t(M-A8^ZA)%+2Tu*ODG&H;g!vG2M20~Q*<@`RQrOGXWPsdA_TOy~pSC)XyTvyP&}| zMNYEKZ4G7hmd{;({j)_L56mFUBoAHCNDk}4WufbaieP})U{XPqpumw{nnV#5BSJRE zC?*H>VI4gx?X2Kg;-0;T0SRcNHlX9WqBW59nz8c0O(;%CF+*}Z5vZMG+hP%-+A^H@{rq+;NUPcPuzX?(Yx>7FODC2?X^QwcPZ-^KXVf9 z`8N^esH8Y&SrJEh=;40Cf^qwjuVv2pQ&tk%bhxkp#oS6*iB>fpllqA2DmJBTI3gptlNw_Zfh>aS8(gI9I&Ip7Opg#a&jgYa%i0myu zD##f-yHAyez@+@9^r-@c+@KJr5vcU`VfpYIZ{Y52g--3zi>Rk)5o7}}DJ4E|ECe_R z=aC7S6SAMd1qb&NjuS%XkSSx3z`Wp$MA))mS+O5lu>*yg4MGtsBEKTUW;O*5Co{l( zY`}{W(E^AF5zV4~``X+0^)21gE+4ZjO|@)sC-WC?c!m{P`kPZrO^YsF{?^bXixwRo zzCxaIM>`s7dn4lHuYTo@MlpCvD8z{v#n`Rzl%<3#(kAj9xM7rJRJszl$3Qg1e1Hc+ zZF%S`44R8ki|PX{8YpkU=?@^?Lqs=od`wYtGanjDm)3w+WVBpcm92`RITSnQ4hEcb zI_2sy$NpgUgtJ8S)JlZ64oS6Qq(pMt*-Q=hAJxfZHEvUXyJDNx?$B#dg?rl-6FR$H zr!k}0^-*(Po*6e5O-26_P4PSVCGja2koCJ;fU&Se78`A@j#H*UT(^y)&U%O7Cx{i7c|A_yI{31dYMj%APJ zLvJaB^Z>tt#sU?c>itVwAY7S*Bohz)3AO2F|7AuuR!9^NmNef8Op$TkZ&A8OH{oFXdPatLJ`)K@RwABy=?He>;EzSyOkp!9oSO8I+_cyuV97>af`x_zy&Sc}iy*ezd)rJwRe>MM82k20w! z9ILzt=bh%cgwGS+b74<!a2FDnD`pD^L?^=A>=sp7ea0)vls>lYDToAHYBKCiY!n+6}*}Fo3Ozel++aMI|ZO1ci&xg5AXmS>a9Be}esh z70ds5(@ps9Gm!i52v6gKirl9oLQY&99;&&*UAXxY4n~T^M&Owv56dq-{jEd%x_j~6 zTlja&(*f{F1j+D=Ga76uLqMMf_|d{>q?<^xTjZA>ep-I%(6^M|5FP9m$ocH`5#Yh4 zDacA?2zW{azdj3*ubka-(@1Q~@IK&JaK-^&ghfN8)bz-fK!F4#N>P_Ic^}v6VSOs+#R`HISxZZbP|TxH|2k0_5biuSyLdDCA78c!5~qB-ZOl zte|imssw=0S@DufS#ld@hnp2IvxdvCN#F@_lMXnWuB%?Wd;QlwGB}&8d)vn!`xn!u z-(u)0U7OUgmr!04$fRWdgSzlKOwy+m*$CJ$4IWu`0Fv-h6M9t3KtxN~5G5|s2a0kA zO&Gm-X^dk5pa&O*GYNg&HCImT*g0p!{m-mh*GDoC`vARMeCl{>g$L5+N@g%tg}~}a z*`Z`cRM!LEm7=FQ1KhOJsN>(-!WJH-vD?iT+l$%+s8-LW)` zHB{||u%KFHg&tR}yEeOe3!VxnOxbcxKs===I;MC~XD_C;ckNJhd7wJzk1l}_? z*3Kr=j)7Kx(Cc;-E{gT{H5yZqcp8&e*Ep!sEK1sso1vRBFit=+&fcCtViPtD?L-MX zK1=)I+cWAyjepFtwAh!O;6mFIHYJR4#o7yTS;-BDIeYcL-QjQt{G}B`(u%IG!RqkR zyJk;r@4%?3IIpm{yrgqkPg6MM4EJGf+MJwr@_AQ4@}nwzv?de;0Dyod5akeol!(F* zG@i~Cg;}8)rgV0{&*jW7uooz4`{>L*Cz)Le>hyt%3W==k0L<=iV*~{h$kpJ1Q8FV} zf62um_DJaBB^ICcI{EtRtVJx}^-9m~j;RmYcXt26IU|rHwxYUAyIkg}D|q|ug1YBY z_44O*eHZsGxwsegf$E@x#UbY@CSgXd!gzXPS+N5>P}v}9Fh+zM$m4|nc_4x-GBigr zOL!A+!mtQhux)ZulVC(S7(uXvEn!Go@a&wtVk__~xeOX-cBXeB1I^Kswei> z8xfG#n`-wBNXCJo<#6w?AFKQt^}>g*@_21FZ{d~lqvMM=M4tP8QM{)SqsX08h<0MB z*J%Y4R3}X|JUAs-x{;WZ1$MeX6CY#uaA@bJdlt& zv~z%6ox2Wv9fXUp}m2;2|DR0RD%Y&2Izza!m{f1Tqo3zTN&`8e2g_!sQ3V; zX7O4IkvEm0v~4z}&lLd5jQ`^KqdU{|o;QL+d*{sa;vlDY9V!@`{mf*j^@+e2gY&z= z`8%P~qFCxd!K^dSSqDC6WgVdI>XC6^L`%XkR9Dq)MUcy=1EQvje!yVD$@=Gh0a5_Y ze<2128L0@Cxt*3eYu$nbgSnisAV{j~BNf3CUEb}YcjUh z)U>wNNK)OpPpzv%CI8G`t&0sc#D`+Bp?Fb?JMD5sTU(>i*7hel44i(a#4b+1h=AN# zsCP)AF=Mib`I*RZJ!9kM5{z8ifC%e!rZE7Wc~sD(gSQVb8&%$6t`TeGeC9gGTAH3? z;R1G2&iv*YNhIS$p-GK>8igc9ZwV0(7@z?>5(Qd~x=HA?CY_$N0hkI9aZqVsyVAx7 zEZYQ8r`77ma5%LSn73ZL!i-!2iggA`oK(>$X>J71H4?nTsw;w4!%-|yuVDsGM@7yN z)@)++zkBxCzn*6lv#-DJ3i;qU!k4@I(^Os6}hdGkt^^;DxMq@@W9a5Bt7~Rs&WQEX0jg6U#vi| zlcfNGf{m++mB`S&Lqqqz$7Kpu@#s+_5Ebjoj?(4}{z(-)?7@8Ar<4i@#m8zL@6l_kLN;1<#` z0xD$;e7+8FHA8SxK{SjzVNZD|tu>-w@%rmXAzS&Cudod-zbt<_&g@_QGSLoL7C~Gr z6+qBZms=mNM2PIx0a{M{ppv2wa3q|KW|Ra+Q35@QW8g2SOaaxuF;M**n@z7*lFMx# zo5y9>TlH3SfVAgx-7VzCiZW+JPs{7XW=DkrxMDqcB`&`0yuV!lnZ=tRp7s^F|cW|+>g z1CCR{z7Flbg)NAp!;6Z-XcWJMO2EBpv$;UX>_ik|h8RK^9^BC3 z63&wlo}u~=e}}(|#5Y%6dsB_L7zB<^eQEFBTlVf{SMGg$;CFlXzQteR4f!~GOD;;c z*Rjt=iRu$IP3(@z$w~Po7NvVUkaV~3%Ju7eqt z!owh4ixMh&1C<5n{}mBHHD%)(-EG8o)D#5 zZ@&5Fy|~?c$4_3n<7W2xRij_{!sx2eLx21u2Ec^TGiPx94{-g(=|#92+=uX;T?~7x!alN&G$lF20Rez3@}_(<oj^CYF;y^%CpB59rvv&7QI{Phr{p8#f+nE-SoebJM^;Q?qhE zxN&gSOEIi`{LJoN_4Ds%zNh@*!oH@a{@wjeO?`dzE4ZKW68z*H)DhZ&{cMNV97s$r zV%3@=2rJb7cfjXL&CHQE05J*^84?ZJ2N3vFYk<7L5&?Q0z+6&YiZBDgXu1R_AP`po zDHenoPU_)Z{wZr14ET-{FO~liWqo(v_gtKzQtX$1$=|xv7fA0x#qdAxeG*r~IUB?s z>1X;$2Y0U6{Xs*p6no>i957y)0Ntx;}kySU}FeyftGrl{O z$lUBW3=NE2l0-|yjvM%xLPBr4XU3S|C4ts^E&RYb?X@{BM!5>Z-TaqJd7t!0NNpmD+pCy9<-(MQc9?os-{3=%fXWCff~oj zfUnO}po2b&ab_ZW@VBIz3ZTllT{e+L3j%=xVhTNnM;qeR?nHjR-sLTB+|=H_snK8L z(&yXb?i$pCJ^b5{mwm(=3VG#Y-q4@w*B$I$bg-sCZ_lsy_?JaH#+#bPJEF_{p89;d zzM%ThqV9w1>f!5D=d~A>PzW*`c_W!jPSk&+fC7Wn5N;^;xxmQH zFsYc660E3xZ?cg$KtSn8o5;~uIh9Ituvf(TqBKEFqbKv&zO|)gYkT`dYwJY2N(2+l zipHIJ9B4ts3oywL1Qjq1&V!J#?ij-Hn5T%$khc+WmgjO1@; zPO)v`4(uhwX3zCa41vR_C+1PUT&zhw7;7OVUoUu6qT6W`UH`MY=_!eMTqAnnZ4 zBeus`BUH~z-Nl4XM=35iVhQg=y9$qbn^>{Eu8!5$M`}v6wY7CuU*B2~slVhSm#nG7 zU45jWHk#L|C?Zd$Aae`5cT1kJ2Nbt91k|{tMy`+OStpq5t+6$Cx zMJrPD9u^ISUh?~14&mR+Hmm=oV1F{2=uCEX`2zue!0E8r?KX$iZZ}smDHJLXGILFh z{5N~KJrt6ss##f5Y>vfS<3I+5o^x7lNaD1jku`iGB>#WKYY&qzjzfIeB>6}Py9-qW z&Jau%L3`k>sc;>X1A~4CfI4O~imL;CZHA8)eoEA^1Jg=Hfxv1+BLy-xAW^&?7YFaw zuxd4Mf8K5?uFqn3Q?&Ns^H_2FWtXvb+^#$OI?I>${YxMIr}HNXrHJD#=JRh8`p9Fq zAO!%&02_dZ4z6l=AyJEfk`wTN>;%-IMs`9+dkiG#S=8Rw(U(j_+hT1q319^S`Y_ub zpUrj5&Oj`l4?!Nzx@zIDt!{0thX3fC)H#&(+%^owdf~NgBgc_KE9{jtr&SYh%X4=N zm1jUAN_Nkf!A6N>vBLla8{*h2r$ zqHE8GNQK{P9~^8yaFD~p!%Xk0lz4eI&1FuiTo?t#jO{Q_hc z^+7cuD3hfPfnfMz7;3GiH~pVKLph$d^Pz@~d*RN942pve`$D0CvM+jQS9G-` zVmy{Mix{&JPgz>vH^y=x*PM^^M0?s`IM&t9JVGks>AZP}v_RPHd}zTIipZUl!OR<| z({Ns^(5Xe}Mqbpu^zMWb7ds2rRZq~_!(&7;Y9iX5Zf}jYM4M+HrVq_}&k;jAAM)Ae z53ik*c=JcyGW$$9YaIe`b;nk}h5>@J&aW1bgza!kAPId8L# zDd?zV^c6!_@sqVzkPjH78%P-fQ^Qei zsAWc}HGbx&)Eblv%yhCM6Ay(ukdU0DqO-87SYq$dqpgW}YfEC|{ZK8}-#>c6N5wY} zOw^)NV5%Xt{jUAH@3QP$mztpd5uLE>ywGdC&l<}yG)fy@*zDikAzCZz$o7ZaGk z@VdkyJVmayC*-Km8ObHYctu-iK*$-xcI`Brin$(7q>rgfb*tIE}|XDabxMVfQE|juo;Y6qd_|fg{;-Q<2=+H z_OVq{wH-P)lvA3C;{eIdy^Mn}jo|8m%Zb4$pp$e+AjQH^c|%@gMI#~RZUHI) zjwiTM3~)R-)kF^ozF($BZN7oTP_tp}D&&Z+V0je05iL9;^d|s{4g(Oagl|OXmx8zo z`+=&Lcc;5@x*(y<41BsAbqFjtm8%y}r-wH`qSi-qn;sdlQv<4Mr1s&0Gt8~}2PuI* zo6XM(<}^HFZ9A2IM`!DGgslTnz5L8!v0U1L^A$$DV?;C&@j`(jv%$jzlY~-3u$`#% zHjs|Tv&aRbF_Vyo2(>_2)dJc$2?BXP!>H_xU-R2bxbxU*EC%j5Wao}!IFEVCssg*B z9(pLOpD_Wzj=6_4^z{oa=<4knST)kIc>SgoUA@)Y(F8BPeOIchE4Ax}8^qUcyz$1H z^EdbPtTq_NhF0vceFkr~Bx_^IsrTCwi7tL7mSJ9g@#}~JA^$AhUgj_x;BqM~aw1@> zgPVoaq20K+6B;U>e zNDf?j06+{UfpYQX3&3;O1Z?+35|RvxuDr#r`qqy6)!nO$ z^KI+oQw`nS4e_3yi`Myk___KU1UDs~ibVp;SCIE800HDZEmBYjy8N!pB1Hi3 z^CaO0BSxw&R%65s-v!y`e65l?lq#hZNoL)8fE}r=a;)97dqd}@J?zL}VBN+;6UW1$ z{^M1(wN*D9GwdA=HyB>h793o$W}gAew`a{j-j9q02Ue{+V7Ny7YN*ljq;2YquAw5_ zWW>Ds&)manvD5lRg^L$sbfsumo?qYCSikKkdm(=D zmf)6)QSB+Vdd-^EvUyii$KF)DFrL~~xa;J`jhLuS&Go4xZ3b2XlAJY@h`Csp)q*-Y z8Y)exkzi_zsDNiBKqTQpAig2U0iv6LKNNNtBsm0ABoojpA_0}p%T%Humr%#*Xjbrh zQ8a`iM7U)_+?ni53Kr2Vo298^jeR>?I`OY%XJ7Bm)}yIJqWMnzYfdCmM_YHAk`vv& zRimrI-4jVXrE(i#y!6o%yoI9_#oVIMO$y=tL|wb=b_J3fpw~(88{k_|zW~rL*hYGz z4)r0SCQRU@Vs^*~m;n3iG7WjXPz)Np4N)MOa4-Sv#ZMNxgN_Qj&FG^xFR2W&Q$LOD zRC)pgxCa=lld_x?9U>KFp?sIOp$ehL%Qcs6*?h3XyG6!$?zs*yUpD)^ykA zrZBs1jWw^Lu&kmcyn5T(fs&+a&1cuFu|+&()o9PTx~^)VAyL736Dpd6H@$|CTq(tN zux#+1P>vYVPaw&V@dQAdN!g3b(eBUCVt_!Pv>l4#QT=lg2Nq)q6{t~`Xxf;ib;+{- zt;_N6Hy3^EV;60E_0?_3_V(nq>o4ft$vO{COdRaJ;Cgm@OFRx^9bO2G$piY=khH}x zsGu4$Ucoab5!eHZ#K0(Xjq6{aIAOmL`Ba96Z`A;b8gxYL9upr{C!Ed^j7{lWG$Yrf zz=I*Wz(jyfbuTN7hr|Ot`DN~wU41(~qWP!@U9Gt&lz;E_)t$xc;Kyy%aj(s~(_Nhi zHIK&kZ>&rvs!D3A{5uV%=%Q*PkBd@lh2j_V&kY88rmPiy+ zR7NA(!J{cI!u9=Bv(`eKl`6$yF|mtxf^G=R8UWr#*E?z}3Wr9Ek**KH?kl2|EL4^g z{f(8yl%^phRGo|;wNSGC(MLc2wPOdY4oPFTUUc=>KK|&Vj~?B-scRFvWU8b!KfkqP zO8z8X?%hjcT85k}^o9`vw4NxaFA8OF(V^fWI6uULW)kWfm<3n%0ZbJzF2cT`c$&6D zOQS=rEk`)$Lg7XqwX~fjv`KkVj=v(h|FV4j`sGK4HcD4XZMBKk){0u^ zRi9Y3&;IDcwhM-9>QaeMCYtMNhA*%^jB8+=Ef{AhS~SHdXEG*YoZbQ+<(wWTvi-C< zXe+HWj-jG4ZO~O5bfM)gG|dHi>dxO+_^(I%S6$hPcDj+!1uM`=_~@DouIg!S>Z;-` zXosa^Gdk#AzG~(EZ2RE)@>I5cF!~RouVlH{5ACK@IKJ!=Gb9rlPXWIzq7)h=4t3gr z6oMmo7iJ2~wS?X})MAqN@8eNZG}npKSCEqi4vz1jg6rftzl0|2et47orG6kXNV!6P z?DCKaz*Z-CwbD~uiMF9Gl0=ozNcc34EUIb>`Tg#S-NW7LvmNY@?7(oxGU`rWvo!nU zbs!!j!2zD!3v7|nCj(iyu!eX!I;FE7Y{M}K7Y)idA#%Aw>5MVACk92KVBPS6k?sIh zJo{YAdA^+jeYmmR>XScb-ng?uX^~fvd6d^i0)YtKKq^tVYxV*?mguRi(;c#$e4LCQ{f-6xYpty|yZmsKV_k zws}gPe$I~7S5_wy)s^*n1epG+K!^4}86crQ{(e21))A-PDC||rdTHx~) z$aL3HI|NpX{+~h__{P(GtwO?`3g@5}IK(-CyI+8FVC4+7a?`zl2zF-#3u()+5dP)) zS;#`)2KKwGjJQczs4NsjGRq8}LNL9v@f7N}vjAU_3&3069_k!GwHze>fVc1`{V(m^ z``(^CwElkZ{&vVHUONE1ML;MY0ssd6I(ifqXbp?hU&4!U%!t49$OA_@A3H1+oqmgM zhv|1u03fzUIt@yD(!epoZ2=bsqclri96`zMLNx>VC2i%&SMfGJf)q`Rr3Q7?9qGJ$ zK*KZ?!4)obF?<|BE8YQ@7zuZ3i0u>muiGa6r7(#ng<%n*vX`DkVRqlRdiCn3m50^J7*1dydq9tc$4!$y zag=(Sp?1WX)40$^@ii0(MOp;ruoACz8G52W2x)InsTFETqLY@zaQppn2aPrRCyBn*2BfYwDBs}g@fm8Z((K#^gC;AU>>wg}`0Kf?f9 zx&x~A5^&>Pez2kSsqX>)S2A?CtLyL(g~JC%M+fkDva!9rk?H=P?{wz>J>7ZHvMtLl z>RdB6uw`Iu&0Q@GtqmFrRk`hQjjgVl6r5(XqD|nBAW?E@Jn}>lZb$ zhII1N_Gq!uq zdZ|D}j&5L4b)iDD9?!a9Ot>7x`VEuM$|C~y%h^sQ%iuY}&Fop|M$h2$AF}R?89A_E z?!@44W^wr^EG}L=GV-;N5#k3Dcug62&0+q#W&yd2z?Z{U&QnyVBs8fW(=FVf+M=(! z($#%V!*rg~tX_&Pw~{?34=Oj&xM$DbRCyr+8#RIY0SRb1rG$W0eDg-C(FA|02t5Z} z7FV6wgEb!r{`}JX(qiP>xvBXM#V>$b&U9d7 zJ~r#@E9%!=#Ve;kHs3QuoZK@nimIkH^)lOhkNTW_A{LI+)kVUwD-R!jDD#}3F$gql z36i&joI}-a@)qGO8s83_bjV$NZUL#ANCLF2{AV9vE%F~(f&2sZ`2Lsne`i0$5&d)s zcT_)$`ktcJ9BWQoBHtA>T)i-`>=Bou7s53p9)vB6 zWLel?BnPPywFc;DSYk}t3T7-JZw>{E_~a+mkHYj?;IpVLXSG@@tneveC!rsmBcFIq zDycM2iG-6{H{%*&iI~WiqE>3s4zi#tVm4PgF?{@zUmTGyw|5V_eQtC&phs5?6d2-+JF=Cd*fVdN$9SZUc zNFXRd_1%^|V#0@lz#cIjJl3jiJl`kJ@k>yN&93rtJ8t;~T?zMha?Ri4;n6D`71Th1 zRRxC`YURFU)>TmN73-UKkJ>g*do=iXV8$z+)=lRYz;OeP@p32`yu`XG3ipq*d#jluqRqTf&sy|d%F51#t~H#(5ddoF(v9_zxF8rYQLNZBg~k3&wXcMae3-qQN|(ol_^ zAJDF_*MzbP3$yTgx4yJ&T3K*Xu&h-4jca0z$j5aHe;Yh%3M+|*#2Cfsq+bJA1nvCj8;88|eMJ!1aI)=o$AB751SU6V?c zkB;tdJA88;*%XQ*lB$@Su=bu{W^X7BRl0re#brN zH4rZi-Vod~E#xkG=d2qwfRLaOc%88Rsm9pDpjqb6V{*`n!8i|pr}>^W^qvw>O2Cs$ zj#gkkEI8|^Y77`5O5T7nBzTL}At6OCegq-)NKZv0>?YL0Uq15aqnAGV=)+3M>c@5+ zKfdcRA_=S>x}8;G{7H}boXJ9tM0yG+w;~aoI#$zX?tQK>(~+0{9G^RQB=)g>)AgAS ztIKiK)}L?srRg);9^QVs6G|iOH=1CI&&>4ut)|bY81odL`&@dSBeU>1?LE^U^}pQo z^Q~7oTvkWs_2?w_S@hur{avEdU&@R}q6E=jqv{+b^HpS8aiyEGR)hW`U`fzl5bd<$ zA{T{dfgLqcanM=gH(67?`~YZf*|ce8pup$rw3pX`>`p5ytuG4}nTW5#=3(G&@F|o@ zs3jW3da_}wBShtD%F0J}G%9051~m%nrA%a9#5Hsi*U;(pQVJoOMK7v{=+Rt)EE}{F ztYb+X?DSCP%K)!=)t#-?t)11IE|_+~rkD5aE8VvbjLmU4=4Em6%8& z`x-_(7+yvT0uaXwq5;G-r86#}haXx@lBq&?ua_{FesI9w| z4T3#Imj%cTH-pUI(DI&Qt^)rPIOk{<`>Y|fb6#fvQTcYv@@mDVQmJRP0ogihQmm22pXJDsq@ZwPBLLG*nEGi6vyhcDi!iUK? zjf5}A^0Fo!k*Unt4)&F-DYJ*QlX}e=Hj@BJ5^UwpGi?HAAatV`swof_g)d=mAv%sI zO4#g>{-~y2^7@q17L%3f2QNy|`{WD)c8C1pP(F><*{9WCH~DLCl;jo#vP$|onk%Ol zr#E#3&ugFKaO8dStt#%)p0DVrDm4{Co_e*DDo8qZK)sB#IyS0W2{&UT zkVG{=I!jmJ4O}6D5x^Ga!iHr$kUl+ooXrF;{G<$i%0L-9s+ z0)PzIG=l;mWPtI|f?ivC-ZceFyJv1$xTkh$&H9_W|ET>Yk5AX`Ez7E|>|Qv1_SUWa zovz(0D&Vbmief;RM*&%3r9-e@AIV7X0CKjw(X%MZ~{iHZ72K zEtawATzVR~DN>Wi=$u+($mTUf=d}M7r*fA)5~Fj!kyH-4QYYp^6>*|Y@UWx{$Pk$c zAdG_+K}t8#Sm9dO4jwZjHKTy?9}?%G)(O@FS*k#0B^$Z+S;9~M_6 zP*<$D7kShfGJEW#5+)93D)R9-kdFuc11K0maXg4vwfA9kPLwG|WD7hxsn&#J9pp3z zv&uFU-z&s546 z(){ORK4#*K$Q3*C)~JIj=oRWalJKwZNSf-{j!ZZdP^JMX>%kATqpN#=~HfIDjjr_QA|S#MeUfI_5S&(f&Y%jC`H=69N;Z zkpP082@?VhAW?(~T0f*YhhQ!Vv~gnL>{3CuK%k-00V&@-b#^P%`R8NsR~@bT=|8G? znf8P9wDQW!s;1XA=Ve7!HQkiGS;v5m68=-*;8378eK*!I&KD03t+SN7pvOWMH6*H5 zLdb)tP_k;v)iCm?aFC^vU6kAiA%>2t81@o4HbJ9NxeK+2knN%yn<|;uq%aF(Y75*! zx5rh4-$ZNcu_pku%uq-plDb`cOtFiD*yi(Gv^mQw)VWRbmz_0d??ruC9{J~4OXtk# zUfnl`w7Snc>1ozKe*JOJ9MIN9Ltj^%;A_+2Ru+l|GN~dgRbYmS5uSKP8A@=GdwfWy ziLlQ)6#1wF#aI14D|&|Y4-RO+_@bNYzx}(;j`MHVwAPNs=d$ys2@*Gb*PMG-?Y{6m z?a%Y}U;OHW|8OqL7Ro_7>lfjSb7D{VLE$fqx?!eM{6e6aBbprU z**mb9RCwd!jj3x$M*x4^+-QMuhvk~lZs>}p0k^_E%9o9U097KfS>PUpkywFHzt3)* zTHt?iY%SV0`QGn`Kj5yWj`@Axn#C3EC&e9M9!Sbdce+FUhgx^7-nwn~z@x8J??3Me zI;*~mV7TI>6o9dG=nJqT{1I#NdC5E03ibGQsP1i#76;KeI>SfFsU%XNvQhiS`Gskmxctl2I?2$cp%pbXoF*@Fp|u zC}Mj;^-Fh`-}hVvJUmJ}N|)`a_+Q_u_&|HVq$5J?-gTSz?-y?uK@v<{Yn0Q*?Lv(f zn7k;(PkgNzw(WG5jzk;&az84Y)}vcEv0hUDZ#v?_T>^ zT6V|$TT6|hVQdEPO!BFuC@u&( zg#82}Plk0zvS64e^pIFUw0dZtkE43@+|gCeMgS)3y^_7AQU3a1knLm$rbK4 za1Elk4yt_ws~tmBJ^?wEAS_KrV8@69L&eO{Vd0hR{O;NFI>EKJHnz{W?WtAVuf3d< z)Ho@+GB|y9(-WE7+lgg;=pka``4{=kOR`bL6->Osa=|k+3i>AOdsN0d2c!(AgFr8U zArpWg0yqXN7I_+2)+E^!B({nH3gH8=5}-KaJ)@uR5fLhp`_wA!9bN_K+T$SebOGtM ze(nH#ad>&^lI$S?DV<}Ds3(`NOov`>aoe#xP~A)@Ip7lk^9vMzR;z+EEJ`D497S_M zw8pB6At|kqnUY*B$pzKW8~wLX2cCDl|2Y1mR>&cq0UwO3pkMwx=9RN9wu=ubIe@|T zsEz8znd{UY?Hm~By!^_}E3a63`L3k{SK#x~D+iWd0e+;1h4`OgyOsW8hJON908Aoh zY@?(wFq1}=+&dEO+`6mBhy3Of`bYjkRY`dA~EXi1h6m4gMVw0ptkTPwc#+wE&Hl?BZ$Khj@D# z?JQ!`!85Fa9DurjVxG)q!vyo-bh9mx=Ggo!BoSRVYbf7`E7_-;HR=MNaQ{KyWGfX? zSlm{M#tlu^2q|osSu^NMfRXh2!)%r$gyF0h)>q7Kak{>S850bN&ty2D?#$!TUe#s3 zS?DjI%m6R8T*iguLBJ)o9htLLwQ(^tqs+b*6^88%cI?DFK}X{EBz!F^4C4?bGKZgJ z1Y2Pvnzmz&ujOH}Bai6n`PJBywd`#8@;e8~{%J^(^CYk1Cu2BMDE=|QG?z=%1e(BWhJOKDEM`?&peWji?D0N+k@haX`GKo< z{SCK^+Qs};oM@Y5oiNhJ~)64NLayz12X6qbfF0j$dG)6)=*l^CAl*E zjfq@QktA7LrvMbykU6E#@o0X5LopZQ*azD89Ho}dRwx&}lSM?eraV$jajYmPd{xSE zLOyFy4Z`$>fm630It-1p$aBB6@1fo2C8a2;GieP!n_r_{{99kd;fVNtOYKwbiTjjU z@L$VEtU?22J+3dxcHoSo*Z}AWtjHPPONk#X%7FgKR~2Z>3p?jcDat=9 zzexNNe+nA{gAIXVB7zukdvsAMvcs`SWqG-9`OSn$%BsRO1INF}i3i=h^o{@~@D-#i zoM7EhVJMxUBF2hPA{mj0U;-lU2$^DPNMsZS1zndVf^H*r*g^cHU9j0a)zD@^j8-$f zOR8sd9_m^!tD$RYZ4*AVMQii2UI~>Q*&WwB~A$X8>SPcN_LLCo7DeW{r^KWmeFanU(X^qv+)m+Qo^0 zwMT>mix0Tc*It$=Pf>4Hh`nJ}hEcnTda$_t;{U(;evl6M>4vbDJfz3N8~{1+5fp+Y z?JbHRlwqD8<_?B~WVP@~R|1IQ(%4VyV|PXMxr-mXx2|D*%kjNa%9Xs<d_bh&YNm$dA1n{(81n=%m8yzSJTLt_FRBEh2kt+v=JfYV z-(b;hy&e~nLbhbEY_LWUbpZo_{eX_qEL@^uKCKuUd|L4Zxy!n|G&FymhZK`R54hg? zlPCFqX~$0fgLfX^&X@C*+jnRWY7bE;IlzMvQ#nWI3gC~C1SEl;oz@=%r%$%@W~ot$ z#ef~52GN&bHQw31{+lb1ZyuEmZocFd{`hw7o_vb=$yc(-x}_jq@*~vZehqXkLQzPh zLDG8zAW%+Nda=zV4Zm=rdAZljpRk#u&`3)FLG+K+T!F4xek;_4GLhv zmI13Fh?lMoqB7gvE1Fxa%9QqVCw<9#*23ybdZ*THm|9oo;Ct3D?Ag&!T~yh%Y;Klw z#)=&+Ppt3XGQDo$g?QCXr@D8GF%L+9h50@NTq%q+(x{IGsO+pLT8IM+gdd^an4&%4 z^chkJQ+Oh2rYVaRS@`J1SM1l0Ns}QtMSonsk8723;fs?IMLigWL4(QmAf7; zbVD^0l=AMI+7C=#-ncLO(qnzfw+9=S44e%`iRcs0FTff>?k}uW){1o{%KBi{D8MMZ zYoPP-r77@wX}_tvartDp(|o({*rnN*rJX&nWUvwa0Vlf83j7)1O^J3ZhprWV>?sUz z(Y6zCWfENeIz-qJU^ZJNnmf>b(>~D@frWPQI6W`?fe0=<4{f9LXwMKH?PN@ZJ3Fc) z@Xa(RjE!KTAzen}BQAr%&u%5^HKiiKUm+s28rfZK$Ev_jRG_dZr9hynOnQ1F2D4K) zZKUao(-0P@u1mG0s3OuXH(EQ&?V?y4Jp(p$I)WT=t9Vc(wYSZ&Aq2O4a#b*ZL{$h| zomN}bIJpt&Kr4flIa%&PPhpxX4GzFqv?vl!0jr2GLTo?T-uzgqDr8@rK>Arr>PwPQ z!{($e1E0w=bdYy^Ywn-|V% zp0!Z?(zcVSn*Y2ow>cL+Zuq4Y#J^oqm?JOez~jp{^dWHBAH!t>o6pW>UwyoOlHy1q zy&Yk7Y6iDptb|pttjLk&u&4By@al28Rfj|EPIlSSq1;0dBPzd)`b;8np?L>#<ldPcs)6D5#S6|}c=nv=jQNeoMI}}QI( z5Oz?wbeh;9=`d#wp8@M=Jak}s3vZrLKbQ07Z}YErX!9EVxjZj3D~IR)aY`evuPL5t zu}m$VGGlsmc&cidn&ZjN_WJx;k5=$(uQwZs?7lX;ws{&-df=C~KupcwbN$VkdE%F* zMC+QS&nhk`D2`5Rsuv$-7G%%PF38WpuX?=1U$k#_4Gr=Cg2g9A`1BCP24{?%4Rl<9 zPsbZ1M6lDN96%;<;pD-CJqHi+YgZjSxawN5AMfK+`PY<%Xm{eZGE-3E#MCIa^z?M} z^zf;@^Lp`L?(?be5lT~5;=Dob6mh~pVNO{X5V{M+2+mC$J;cVdZW)FkHw&IMa`|9V-TnXruxWHu9<<{)gmToawL?A!!=bf3U%;3Y0_CKSKox zhe)61Sd+$~nuMORNq^cl@G&0miiV4e0|6=*RU9cs#i&3i0K-~twztq%2sk~(!Qx;# z`SZl`so{h03=Sozgh)k1^Luh3#mXJLvA(&vK0V;e%JO-$vfj`g|AVV3jBeiS+M34D z4xiiYqt^pFcfL#c-f>p`0G@%@DN~@+xk;?ae5}ct%2yCGluf=pgdA}0Py;~7hnJ)| z7=hM7Z(q0BEIuJTY5*7}B>Ne#Fe;xZ4RT;d<-pMM0IUejr z8!rzadIG6w1Ur2-T%D1sQV=?P*F}WRwhs4k8wEB^Wm6-rh}$3Z2i=s!9LefykaQAR zLcmEReuSL^3{M@H{*d`&$7ep%xBpsL9qrkvEBf|l8K3UCr*DNLCpop$m9^jabnX7O z6DRId6(l>(xX^e%abjyKBGS2fpYcuvZ99CaDc(=|QWL!<;-gbMqJ-p2on%LJi0(`6 z2XhU)3jXQ%+?r72z`AYy;h_4JQ}dUu#Ypi4crm3GZFI_U^3=qghYQS;>x;8pS*K*HU;?mx=zi z3to-Z9vo<+d1nGdxL3D}m3n$zSL zO|ZrAYw{(BD-_k9w4!jNtA=mZe)p*ZTX(5{1s<2`Go!~#GpH&?&JeQ_Vasqo9j?)B~GuWMmt<@^1A#n%WNW)!pZx;Q5& zW}o7uAqP6W^{1D6^2%;Tkb4TnfjP7}PSy`#EIB8kc!dx_b_>n>Kk`Wz15~H9Y z6^#oQHqwjItK;>wq<8%7mTC1ZE%nn{W(RU|1A*L}fc6_n?G$#Ly^Jd2&kDGwo;HUv zfoBHENO>)oFHy)6H#om(pY#3if+U7rY<1e#4?s|NEC zRsukFNi<~kUzE{3Sb++O%kjA&IgRA;8vfz_zbS#iH~AN}vvEg4TZw*(w6G}21JV*f z5o`D<3dhcx{eRo9r6~dJZ2rZ;H?bEPE5Q2LBjiR8LyiJmK{%|6C@XR+rt-eDk7S)v zxDudHDXPygN*UD6axlH-2I~?+OfYM z)gJxA-yh_iqEmk0$jKvo>Cp$ZV=ImdnK0HjRoWm9a7nZcU%+((eOjP^O@^^lwQWpt z!j{DCE9f1iyW#9<=LilaS)L+DWr#5B$Z+6~N%pA*Sal&^5UO{N{tnO%KgFj%g&*z2 zQ%_0AHazppGnx~>Fkp&f?Zo-tf`vyqDqI;$krQ8;^{6+1`9^x~fObCrTqpmKOp;i^ z1}{M)q_aEL8lByVfsK-vh|i|J@Wt7!H&5GM?_%&xCGfQDMn9XuCv<{OAh@CRz&a47 zD2yk;9$@T(Ft`8|=M5^2*hZhz&Q}7RWThm!Z}1xi%rW+GEPi8jm}UDe$zg~d1kBWn zmS(_woV^Bm^b*#dsAJ4p;gSHhf1%KNxZT3-1-wDMAFd*iw+a@o%Fkc5xM1OH7fd_< z{Am}o0G~vQL!iYwFk-omP{9>D0=q=$-*Cc9M2|&_olY4{DwolSkWx{&R42JK7J8Y& zN-4Y)xa&OH+4*2c$59#6N^58v)=)S2ukvUq=0N8lK~QKL36T#$=7`0M1(sXKWElAf zW^r30qlorfA-5r(3A<=@+c7t|`=Deo5Y)reosiuuSJ8o2*@k#xiEHJh)UdVaImOGZri z1mi@rSynLQ@&w6MNM~8GO6l-3v#t4jYHhQ2F8_RUEtMUi*znEsqp>h>JuDofjO%D! zloiO8hGG=l1`-O|2J;>*5g74EM~gSa)?kSXC2dAJD*H)At$moH7PU~+kVmR(k3(s@ zNSoI@IM{sZcs0L`AFi&_)@fU+s`#D!ZfyhYckHKa*q5{xh@QlD6FF1idy~#vGV7!m z9-Ubq!KiObWYWt#`G9Q0xlDJl zN<#iG^3Fmph#^jMD3o#z9c=^yg2eVK@P_I~RLULYkH|qrxL8rT?}xTRqT>4;bq|&+LNWrN_pO-Ny`@B zC!&>mdIn3#8EEIut*?mjCgwR3(j%ls2oAl(_@k%@-c*G`&45$ow1H-3mBE;5f{Nh@ zj*N^){Juy~<~2fwqoQC*3A}2Icg#yQ;sQ&QJ0aoLu*Lk+wqq{PjP~qpx;x?c%nD^X9@Qrm3MmrzWRn>SUQo5av(h8xDos$QK{a zi8CVB+b&o~v%|HF3mNNr^^%sd85O1e^1d_qz~9fB+n!UDGpQh}h2F~x{+QL$pd~@-DH8FxCOAp(M7=V9-q;UQ-h{2yVz zHrttrfl076Yv1E9Je6o~eoFc}*?@DevQ8NQ2at_9pA5Ure1;4i^XANIm^QVlJQ51z zWoLTQ#eqsX4V7+7@RVrc2#5Js@lQ7$#)`ern${z0qBKE`hrF$0cwDW z%&?;73QSKZHDj|OP@1A#JBxw@eYiM?dZw^(Wt(frk5SQ-YRGf1KS;9~v~gGpg3z5N zl#ic_^6?LcwrgLbI%dk?DMT9KlD0Ioeb|7IQo2z)g#_VlytLobpMpjh^FZqr9A8n; z>yfgoWWwnj#)OG;n03Rd0NZsgTtjn0xgoz#;$K&F=x!+@ORMo7vvPF?l3rL)omD1`dom@6K!|(D-ypzX( z(3zWQLlNAYKpH%P29!jsL>I9AY4LzuB+fxs>HLC2Hx|qL>RFwnm(pnrujpDnoH ze=2@DsArFd(v|EP|6KgEnSLeAQ#^b+VXnNO;ssb^ZP5kUE+{=ixo}ETq&8ZRq-#_= z_MJspLGKD`B_v@NTH14RDD_dGFsC4=Ak&xO&G4RvP!aVG)__L4d_<~3WC%2G@jF^K zrt_v}(l@r^gZ3@@kT5ZP`*W^Mt*x6}+V}Jiz&Q-xN&|8Vr!ij?<)fS_Fl~$YKi#>4 z)1d&>Ae{?!VXM;cP?}HOt9sHNMoQ~6e&Ow@1&+b*DRqu~=Uun_{vFIM2c7=8$ZMNM zHdK;1_#)CFVR{$h>qL)MJdnmxT@)1IsSmpBq@3pwP{U9~FF&}$X4U>{WL3 zYP)xG#a)u3eRC0*X0qiAQCh{GQs#FkUW2L zS5~gp!|!zDI_|#ySHCWME@0oPdQ-ony{Y{|jEO0Ed=S72`H#i~Tp^G*AyyJM^7dS4 zu)!LT)rHq{9J?;RD_Ak;El$?Hx}49p+IDCc^#U35mulZkDRDzC&?xW&e)!8;Sl0Oq6Z{*r=%uW0ZN1Rq4wv8(>$0DAEx4_cYc4%UCw-mQaAWL zM**o9&}I|nw_dW;53sIRCDlD)VrgSsLWQ6TOS3M<};eJ$8u2Cv_wTmzt zeD-qftC)@6UVg()O~YjHIZL%CXfhBIhq;(5<^pyAQj(p{9^>-5h1NKXyizq3`~pQE z5Y~U~uYYy@-IxS^r`D63RmO{W;5(^ab*nuf#xt9}DQa}4G3cZ98K6R|3A(r(*c`LU zIk|U&TOgBM#1-U&-}i)4_`>Dd(+Fqe_bNLSoA$&~-jWh=KVQT@C-60NKgMtzWAL-O zXtmo)Rpk+uD>*Qzbs?z3>yk9pA_bR1p%=K<@Ai7#P*o!lC(?1mMx-w-7~1ORVeyIeuv}?SuL5ncJ(}&L*a12F z`d;*jtw~J2&ss>!z}S_vMX@3t(mssJ1@V%;J8u%ZwE~Od_M2#{iaEOvc|X2^J&P<| zE}SYzrpb(m>@xVcaAcz0iNhS)2I#B7tAIyAf-YpPByR-^ywo6tK<70{+>7f2F zP+Xpu>+|L2mW5KcuDNC2Ee{0B1Ni>{KK2L70{9QrvT3X?w&`&Y4qCP{2U!uVUWISR zyn$FzegRs*q?EI+MX^Jy@Q&PQKrV5*hlw)T!*7vOMiqGJ$AicnOc{zlieX`B*u40w z_lnh5Ub*JV%P;S~mOHeMu3dZiyN3Yip+ko{4v`CC0-W8CBas9&I69nQTPYfHyA1Kz zA-TyFq}5?-NxWkKvrtH4SJN$=qC&t(B-{bldtCt~viF3^unh`Nc5jJs{)fviUwf_g z5qDhMefi}8^~%-n3g8IL1o+Ze_g7N@P#lqkIwG+W2$69@uoj+20*f+-3PiO;LZN>z z9rKYY2W+)RM9&!rB;{Bfdz|gxbk98-_v@&8^f6#AzLwM2xnFexusGrhFqcq$ZKt3L z#z|YR>%!OK17VFKE)cUz-y_T4u&=wK$)X%khA2Z#3OZnlFoyNX|DX-+-+0eGoAz_2 zN;-YLA{QLuHe3i-MMn`> zg16p!>pk=CzV+4)-gnjd8?N7QrFI7;8;g%Sm92do%N15KeZ2_xQ}`8--!UY0#Ag_K zk+q~3!MOFbD&hvS-b8*crvva{GF);x$RaIAULW*1mhw-EL0_R=xQ1^M!xoF!JZ{(^ zWMr|_e8cR14S+T{94r{H0~oI~kCN9j0zR>6-~+vtUwZ4Uch9@$)?1a54(*OBH(Y-3$S(qv}tBRH*zI^OM2x;=H?Or#k#>*Lm5!WhN8o%`F2rBb;?i_rA z7u&s#HM<^oVAt9tZ<6-LrTm_|RZr^P7hk;C;lY}axZVU_>NRk(h`*vpIa20hOpFg; ziBn{sJO)7i4O|;KC_Gb85-cuu3-dw*3mS(M`ES6!ARHkCPaYzk)qm{spWnGA*=K(^ z+SwT`C|KOz+}ywT;bc$J+D$LMxYy}b7c@03+&F7<%ly7Z)#JE$7;NwfFCgF+8P%5w zbezi&9~2Vp@Et`)QX)4CuwNW3Db`U#Y7g)PQv4*Jcaa8>IBLu~KeCfB^YEhG&CR

SsNENH7!uR&b=?9)1je7{Lkuq>~^xTf!#qqYTASp ziU40k@+yW1hV?!4(R0VufBbjzH%{#pcpmy0@VpcryL+RqROHA|(=fwIMmjL9>W5Pp z9#No%eYqD~uq6fDkfSf}Ol zx7O*uyYUm*+p)L5WAFaC7w@0D9}8jVFYpInC&rt?V7_=O)m_uJ<_GVq{Y_l@TNK1FtN42gwcTiF5Pu;p{iHJBLJSw;MX`kT7X3y3-_>R8bw|^Hp*nyve+RwC~iLuYY*k@v7 zDKy(;6bHFO78+?$^<@Z-vxYXfTI0eN=NDFEwACLyr_MaJ7h_fbamtDnkT~A;k0(PK z6lWlfw-~k`9Mza{alk-Xp{SdN$NI+1i-1Ke`_XO!S*KyT^_5R7HjZe!AHR5K-|yb; z?c+PNj@h&MG3^r}$XgLHSd8-+=HiH$3lCOjvL^$2H3$Lvgn+9j^0pl%t#zWy>~{#5 zC(*?8^=Ys6J;;w>s#eaHz>8Trc0HUnT$Y-(% zghLp`U<-py3~SMjz*1=uE64p-q@S_djO0RN-z^!W43>dG&<`<4by!}sy7U!>oFxYr zYE$@23$=@QYYUB}W#5wX@$Vd3Wapr71@bIY=Bk-cF?$&Nh|-*j+Da!F&?#Lc6UiIW z@_14`E-zAr(UOL98}eC!#?`q(+=KimkQ9}3LA_iJx`{p(L|xd)9pAonie z%dwtqk6I{x7Gkb7?BnIyFR?W99e?Tl`$?#Khd$sBE1UHAlT=y9ht4SE#A-zv9~}DE zkRu^ZU%VlaCwv#^{1r|VsY%F(kl@CnBJm%rtB65BgrYs;N&ZckMP7gBor|{Zpi1kz z;1gG&y>i=a1RvW6dYuPLEAmHXiUkE`3e_*iG~Z@Ujp)KHojK%Vi8T=S!sPefzTBDT z^gxyIb>5s)VzXvtthnXazD?_v%1^u3MglY1KZWchLyBH+WA`=5g+GqU8&(3%Ii_($+j;GY6eFQ^V zHn-t3Qm(17<8rvx;Igspd^CYj(-0ae(>VPYsfFQ zEQ7zP1v1LWZ}z&jp`izyId%~D4hx6teER((;7G@u`O=^`0ha=9ew?T?zyYqDv{vER zJepUOowo@nFmVC?ru5mpuJ;Eowh1t}+Cfgb00Z8Fy00)Gz{9mzi+2DGsB=A0WST1omQ3 z>%%aRiTw>H-IYTZf&55cT!ZI^HVbe%%=T<|fZ z2Y9RYIMGGpndc_*%pWwD=^4Ih0?#P74Q??#qohvY8MWcmcg@c{GLdI~ajMAl4B0l> zShZIS99XXW2ROeN4&ZA&{xk7g=tSDRi= zM$9QYJnsF66Yf7T?)@js_w$Nz?hm@?h%w;{i*E1u&+Hxk%(LS^^X#x^l+Zz7gOUYevF$+Gpah$(4rrNA-<~%umAF z)W>6G`B6Bc;M}0&HWqy@Q$FB0qumq7#o!jXzaiJh@L~3przYUWND3NN9W9O)&1EaH zJqgX=7Snz?BbbP;(X1LU@u9yYJP+Cle>5mXA<)5LTZpkTkvd^etk>ri#4;$iDYsEo zCQfrT?i`z87kuW&!-JSa9kGK)M9)|PF=Xg;8tYol&)8!z&yO(@#_CzqP=-Dh!*80h zPRZD=a#&((a8Dzf&065A(Z<@@GRO<7kg5Lygf4An+t@C)mtDdxV^^|+><0D;_DOaJ z`xL57eU5#BJ;c7u9%Wx;UuRFV=h%1I_t_8FkJwMx>p&uLqLhZ(^g{it|0}L3c};`A zX)=EPogdX_j9#0X_WfHwV%tD7YFgvc2mjx(mVU|JV!vbWvOlmtvA?o^upz`0*m(+1 z=N{PcauFL%@^U$!jCkOB-pFV37CxW1@pit9ui&fr8h!!a$T#zCd>7x#FX5N*EBQfw z1OEj7B)@}yihqWGj(>qa#J|iRTr*ZVT{o|A%XX*23_#5q6b00< z`@#0|KF&+=wm!u_vclO3ns{(*5b?$H8G{C@hIz7;o7$N1s{{Ef$(cWz938CKE!5DP##Qkw0f7(4o6J-K4gbIJ zL;QSMj6Tg|&Fn15<%`Uw`>@oHP`v&_D_HFh(_MhyB?8oe9?B|TR>S86Y>#zz>`$|0Zuufen9Mv`5!YzeI4-#QtL~j`#!1 z9^d1)cBgogH`AT+8{=KSBi7c)mgHC1Ke1l0pildA@~4)5{RY}Z^z+ZY(^>)#iFj67 z3j>;Qu*Jgo$Mzr%ryW(?($!JC&k`J$s?F|Jq-&40S4cWgv90ZbADz!yvkluStA3va*| zk}T->qD-b4`L6u2T>}Nif-X45B2maNM?m09J0*}d0$p$@6=g`UCd_l_lNmaok5SkF zrVVL?Am|A15w#m;Y!!ju6m2Ns8O@EE3s@57P5Yei1x+ZCai z*{!^HW~id9_5D;=(CseDNJR_EZTj77>Cu^M@pbmhXJQSfhO^@LhTG6dMVU%XmF4Sw zK*1No8TpV-W(%Y942v*qR#0OqqTDp3>ak1lltluQgOkk^WP+u55?z?h3&2cKTI;*& z>bmM1x@zjW5*9%Mb(xJ@aAQqZ1Aa^Yabx|`xMs6uQL<|3-mx&aS?#?k+(soMY!-@& zC#r}y>~c7eW0y~zA`1;Q)K8f`b@s?HQV{%bci`bv68RDn#yI@@ghfBrnCr{uMj~^| z#qSA@f3k5uJ%zy>XCLwV`G3Lc>L2GU_^0CPKN=BNA3N7R1D&VsQEV>cbwyr|3hj9i znrYSeRj+-|MC<52a~)~8m5DOa@_z04x8HtSf9UPE-+}j?v3})qRCnHyL^Y^UcOL!F zv;IguJbX>hzl~DVrZ%WRHR)5P;8&^g5_->N;b>8oACA1x-6Q0Hve{wjTunZnq|Sj| z(?hPdHEJC)FZ$F-4a`&Zb>v7GBCkRpyh~p@di22u9yr%(&r0j)NXxQY?K#f&_S9@X zZ5^Mkomi`#Sdw0seS634*>&j|by@h3H4ROp8^)SoKfLWUI%107S61S#!6Se>e!s2{ zqa#8^{94SM1@j()Apy4$fNuoFQxX6p_+B)JfPVqQVdP z9Pww)2o7o}0*(-kojh?uv{!wC+B1&}IpKjB1^P6^!wS18-VmoO45uKI2&~81Ck#6n zk}B{sctlxQt(~HAiOdGW+k|&5`Z#l3S!A1(96^jb4yTR5lS#%vQD@Lc#->1c24V{I z39#e#7zy^D(styDZ3)%*v%8>9#1+Xljr&dsfll5v4?PLc1w2 z;KWLb!$>+g;s$Y&i=l%LBp`_$C~S% zo8|bZKd_~uW+8H1xU6!R)##9rDgZ^70#%oCc(p3)u(1w{@6qPV%1ZrvL60jH z+1AU4aR-gjcs^l_R%47eX*;R0<;#~-djzA=o(aZE#aSz>ISUU~3;f z@`!;ty4QgHbZbKm+Ur9!5Mca^C-}72L_hKI48yryjA~lxBRa})KE8+Sjaz_ol_eui z=&a}*haJhEMfpxW3k66P_}zr^oepG-Mj^Fy2TEYTQ8w8k2m;zrT`Aaxl;j8G2CkS$ zd4o(EUS&Z0kf$BhUO9?zmiv2pPQ9SK)H8USvZZGbdHM(j{w!c1zpv@hIy*9P!B#^; z1n3Z@EQ6OGxf`l$;H{;$8y;{(hVTrk&?ZqN0wn~|JLIdioA|4uJ8ipK+jDA@?26_< zAFsJD3we}^Yc{OwPF3yUtjd}og~>1fXzJ!;_8r6)vJV!y=4iGDy& zEjTt&2?y~I=!Xs^N|B`CWTNA4^mS2>UV^?be%JU8_wb$ChdoE%P_H`mX3q&RF1aSe zc!;;0K+auGE#d_vX|MEX+pQm*%%B(M#v=EVjvJCPyQ0g5i}Ka!DQPO@eZX9$T2bcQ zhBQHlNQIjmO89^;MA<<~uH|;inR5Hxq(tyIU8oL^ZI(D^8JIIwppIs&CULhN?dj<` zny}$$>cn6ZHlAEdwD$n7-2ae->C{5NhCK-x@ZrjiYC{&36~ThCT5aTgNYzkm*7k8o z0O5!W098Q(2v?Zk9uNSDFziZ#3IGWrK(NTYX@&(fTh(E)ky|}rK`LaVv;r6_;J|5H zLxnNNgT(__=w+bka2!8k?$lZ{ZooGPn#k`5H3An%4mvC$5y|K|w-JbfjU#hzA3myJ z^I&O!%z?xyFM-U-c5xov4@!$%DyTby%^`@KSjB7&FfTKyq8GZ+GyU<`-Pqk?U?aSp z;P+#Q0%Nf@ux2G}c!XfX@kvWJR&b0?MjB=UYy_8Ruo|a_XcEhCE}e+_m<5eL^W3`< zg?igGZqYhcV!Zj-8h`an<{0OzFX%c+X99vEw=4IaI5D_Rhn25JtqlW@67fsHxFEEv4_==(z+S|MuhjT1D*_nuI$KlLy{M)Px0%yAsH3CuBI zqhkV-ynzO&uag*TIBWFv2SUjd(r${qtB94HFvXCE=vZ}~tUmFEM47sDaF4*+KRd^A z*rIV7xZXU59&2ClRXYD6^0(pKR1janAjiQY608n1n3T{|&56k;PVOeT0CYZX3kV$icf~$m4IVC;A(}>RPqaV*6*#D4Q%Ey1Vk2NR&_A&v zP%KES3N#C@&6qz^yMi+ndRXW%h%Y5QEcilMoge}GSgrI=&L)*15*(Ur7!MhPLsWRZ5tl z>o>rV3C+ss{f<0XAu16l{6IGNw06PZ=lEx|Zn1VjpE%LZvHbz*@e{aBtraBgteNu4_I$0c84_?%pn~(6G-T1tU>ZPpcj9=XYjQi^an~$>xg~;rv+bRoX zXaE>7UqtOO^$G%FA$vmluv$6d6sf-eRh zu)ooUgaeI`&!-{V0S=+cH1miwoD(z}ahxW=F&-|Z^r4TRcmwqN@pvqWRS>H;fLP*s ziSf`+UwIzV+{)8{cvdkrW=5Kp03kR5MM7qxCiMn|Scx0yZ-Ui)y5P`5dofJuI^Tr$ z2vY!20BHRn#vAA|4%$nl`agoFwsRDhCY~DMLD04U-w?7Bd212bN1{m*ib@)MCWsJq zAvkOk9;%6x)%(HTHS-0K0?-4@5ZGAY4#r~M!4nWom7JnHzky?;U46}ncE!5G+O>dC zqgl|`jle51z>t_QjNbsI#xj(&ns{SDtTAzOiC)L?lEP}EQw3gR%?qI(B6ohIdV~aO z2HISC$s@E-qJ7EFh#ri^A-MaII3b>fI3+q|V62D6YfPSoe0_w_@CHOADQZ>6`N;&F z5Ya+wlwb|b3Pl?9XoM5!wSXQouSEVsf=GzP%+sn#SlDx7BtFol5vDw%WuY)b!-@DX zS~hJIA&Lk#IF9dB2D)xqhoj(|D7@G(!&Y#4I+e|BGox>Qh)010YMJnXrfIDVtyQ+D z+reo-zmWzx0(xAI(t$<^Iany9$2(H;BAlKy2lhG+b_OL3eVqG3Dl(&_YJckZ;DZkB zPdpXx_m%QD`J1KM3jScJ7SIBv{6P%?r18E)K7Gh9z!1z}I64e7EW~yV6OyGlh?FA7jNWcrY`>W}HMj{A>K%S|j9#?`jQluCc6Ek1Ic%gq~`+ z{BTCPA3>*S=jnO@q4N@Rr1vk*b<>_Br7c-!N9cYGDR3tU=1b6};|?jmpOKD6m-uK@ z`WZxPUf^MZ@&l$tz)yQ{!q0(UjXfvj6)8WAr0n1Xq_}E2`K1i(BdtgpalT;Z!NimN z5QY3eGT^^Meh}+XUy~NB$(dM_|4#XVmNET|B|l7X9u4_H%m>L2QOFMl4*!+%gN`rE z&Pm7*tN#b&2Z<4wWS43WYrTAl_6QQFb!uOx-E&euBTmorp6oHn55z|bp5M;K=hp>G z;37$u_7bTNb$u-DC&7zGCM<(UrpqhTk=Q3f7a7}QXiLxtj7dM|K;xMLjX2ql@G+)? zmE4)hBiq1*@#P1|m*C6@F<=oy8$_!B$9TB>SIG}V>PBG`Of}{w^i%YF8 zwJr3zFG% z*o07p$pNktWC(pfPE4+n5=2NS8gxwq303iR^08&AQDg`qY@6jNtZOMlkeWuyvPcig z;5YyV6oLiZ2rLZJ56)DgC?l4k(Fbr|O%lD-*Xx3iE<@;aPt+|W;}cRwgKf52bxo8= zSI9)VLbD76-W*$9V#h0tvpNBkM38VE6*kyp22VEGF8Eo1EA-K0iLpkbqRCi`x}Jy~ z)?pQG5_c^811O~wiS(jPk=Q&z42*#(Or(-J>Xcfn4P6KEar7mIkS3P2fnF1}lWXtH z&lMf5q}z&}Dmtv@hGPqJN!1Km6m zci$4|9;WA#zCfh=BkqqHh9?mDHjxY-Kc_t8g+@$_LWxTuq+Mf$A_I4U zr+-c!peFyd@__Vch|j%;k^KzR?rH~zmYK51l7}8#0VpvPwh`WF7ZF!NJ9OkG65HR9 zkg#2KMp%#nxp4)1MqUDf-t>?Yg0Y7M&5^t&!JzB!RVS0g(sXFpbE@GC`*xa?>q>03#e z$ENco_zjZfQj;mse2x9b4ZE9_8TZ{sS1(kq;P0xR#HVyb(6oqtr(XOpK`^}CsjA7ubG zC-kVMp3P8vbvOjd6-KwlJI#l71^O=${YOOaWJfj1_6pV=DtS8`mU)V*_ee8sRQ%Ci z(aUJXTSuGq%GE;^6Pja877c2Bn!9>ppC6()CMM9JJ5H{PNNM)D?%GT z6m8g9wHV#Rd~FP<&?2R7*52EE@hVuEn_fvG!)6o?seVBFF8-xc#(f7_z~NlQC`?$&F^x zHwhlX0D`q60XwjW9OuE}_!7l5=*6(TcDwf3>a_fh-ikVJSh>2%?er|F2nVwf)y0&- zp8$qy^!*qXYamC`I$g9A^hTCOOwN((KNP94;-x*cbJxYZZs)FxXba#R(6*wj<*@gu z#$KYs1IN2@B*@d{s0EuQNH zDzFYR@r~IOs(&GI;$X?{pn(%;U8?KgnuJ`fe<};DpFe~D@SITF;<{$huXe>y;ZVuY zG39kH1}5#S(-2cSheLtY-s7wi~nQ$tKqD(8t|Ht7Yyd1Huex}sH$q9S6!|@ zD>u*Y$3YI9YUdAS3`K`NJMndDC7N{pnya>baLX-te#=nl(Amh6>t@r$3PE;s&?02d z1o9PZHsRTfo=l!_Zd5Js2Cd22V#y#!jwB3dw?LsO(6%hf%5SU-wRrPpl`m}kY1E(U z%js%ReI@xJ#A9>q9Q0WW%%!s`(PwCwZF;S+O?Pkkg!Dz0pBIwRWeA(djJcAYRtrN!!$Nn9>Ub&5DtX_@hzdiKpp|_MJ zc)n1;=mp?$gR^;^Tao)|@Z0>cUahS_*`o8NnrGXggL4>X9!%0Q-ap9FR_-`b4&cZcxwZ$`VRrO(2@Thf-9rbm4nt z9qMaX6cEfz;`^iX!9`UN>YQCb5! zYI>2yR<(9R$2(nJg5sZq^RRxeW#F_oM<`ADg96NXw2g^ZC! z{k$IIF*@9*8IKvqcf#ZH1b2YR9o}iQapDi61gkBn3((rS0eF%PNbQOiQhB^^aZ#Wk zKPQqKN#qNhAR($K5931$Fi{&P#uFs2skaLw*_q%F8YTxy!mIwrBODM!|vL^K^V9CLd?;8cZWyJ^R?MQ(E?h`wg9D((; zlnx7aA-r#B58AN@iGKxoCJyv#dk!4XmVpbssI^u3+o40dBqJ*N82W?W$Lwg6-iM^t zm{*!u^#GQ$QL|b29jns4eMEoQPkMg`uQ+hARy0O`|D}EjOVr19KZX)@BKtdSqcGrx z1D=AN0cSjAJEaV_AQug&kkGkz6FEX5qmsn?EC)uvu`B9J$F4~CrDIpPGUT2}!k$pg zd%|uY=qr;`^XHyDdAc{GU0{ISn33Xbs|e@kRO;xGa{?LQ3G9Oa`~G8vh`Xcg^|T$z zAQ#w?jDEn8q}c6OjmnZHOO{mbhbgi3Im&|=q=}W)pNrPV9x+(iF&>REw#YF-jBQsm zKL@KhCy;~HoR^!pmQzl%mi74@xrnvrC7RS)PnH|0#wR!6JwH2ha>JL!+8P=%cnQUZ z{QN}UeQGIWgM0=PmXjweBdetbOJ%&HU&zSlw?zFK`jvhQW6Q^8`6MPAQ>#+& zM_cvkB*?~(V|q^K4@3u`Arq^Ugvg0!58&CY6M7aKSIEM~vyg@H><`7W6ZaX@kKsfa zn3|L_u;xOWcc4uh;pMclEKYN?97}BoITrLobq@VXKaXCemRlhW>w+ z4nG)4@y%UTR_)8t&ei9%uhfx}aaJT$;LcjD&n@&P=Cw-_-k7=*+K9BDbiE1IuAw(6 zQH#LSJU($fDdvGFgo&qpWy#v^NkUJm7Mb*ana0R$=S;mh=sUdCJA>l z7YJHw?!PfQoz$qr^*;R6Hv;t-yMcSkd$zCb zn1`4Hu=@brq6}cNt>A8muMvLr!`XRMY8-a26evc$8LU588zfYg{ zaWws(`D{JC?(Z)x_4O_EnjOr^3DS%33f-St0^Q$};Jw!WTl9Wo@%JNjern06IzP1u z@ZK{4UP0IOsZCPXrxpw~sXqa2o+l;De^$?zAO@0-Z%SJJk~NcOctdIdbbRi`mEpo1 zUB?HydX3I!AdCKK^m}}SZcnfln5Q^hMVtT}_n`oylii3wz&=p)z=k4s5qa`+X^#k@ z7#(W9X(elyw67)e(jQmg>ytvq=w}n=m0`ytGG`5{0qlr}pj^p~jarj#>9%jAYHt67% z3%#GHh&XYb-zfUNZBpMi$CEc}(t^|K`$op0jH>IKxSmf752X%W3H|4p*iNbW1fIaK zo&L$h5)l0wsu~&?x@jDGKyfEJJ$M=902P6x{nO3twti(LEoWSN4K~pOu>09^Bq%EYyOv1`4!?+@bI#j$Gx%I*B8uvyq`m&LBd z@4F;+ZNIXgzb>A=GE z?)wt19m2_Dw_O{%cA;7dyYLgZc6;pFYq)j*vYy>`ICkw5>LlRhaa_A2cI{PM`;NGF zXYAU=>LlQW?z<~?trz$GL0r3glxv@fT|1x_0SEN#eX(mtl)nH6zsI!)V%G-L65!xT zT>HG~S`)6_gKJ-q*Fc31^B?kSRLZS2L3ir=LmfehOCc=l7Y z7{rS=cYNjwZ2-UcUDUSTKEZewzecowrZ9?ju#gpYE<%4+s?P()9LlADs$2L7c~OZ) zWI@LTS94NvS#m5n?p!@<48<;?7CowM)zyVisTY<4MW+`0SqRfKQJ&fj|U=+@G} zr?T_-7G?F|KL#uL{)(>LE$&Ls(W6I?hugAtd1~{un*c-a&>YP1#fTIrVDqDMLCVQ9 z4jEek2Kg00x2W2|u%mn;r4qBPfriIsvB{)S1-62mY$RK9r#g+4Vw4rjBRrlEUgbeL z;+kq12NzN)ktgb%yLvl*{>Sq#clxXjo|M#f-hk6@b68W5G2!=GciZ42czZo>zJE$H z%8zTAsQNYEpW7MG-lxs4vg)B@N{PA|7z?p#HaFT_U7}D8NDKuz2(7(z8z>ita8w2?H4END=X{k&*knC`Ve+^phDdv*`eRnES^-c zsQP!fWl45ODgP~QsvO*LpS`rGbm~<6veURw2TrfaVWGNL;CD&eDNT0B(I^UddJqQA z9o5y!5?LijJQvHAKW;ppi^I0U`LDI&?f6xI?rv2M$7=0}b_T#-SQt1Ta!nZ)76>0; z5f+%NpeoXm!AF!O)o4O5wF5|vnj+)Efd&K^1^n0BwPJuGS{dvc+$w1MqM<^iN-0rF zxB~;U+pbcm?l5PIhDzx=%c6SDSgnYcxBy6^jGap3Vi>vL5=k|fEe#fOr^#Yj!7xQ! zyONXh%PCX>-uvW+XPCP}#2?fzdg=%dKltsGnw+fWwKL9Ix`WT%p|$OB2+P-u%2_pp zWZ3h5ftANC$@u&fUh*@C733-XvjSVdPxa3#o5tSMKU-J{&(uF#+1b2F|7>H${AT^L zovq`?_0LJntr+(wvsR^4zn+2&nN;YWU~rl~r}83&;zK+C zOR;`kVO5q^{j4Wd)Pd{USRd}WKs?`!>J0sOavQGGou#7Pe)O}8&0v%9ZzEcvJ9e{m ztO`$ULrsUtqx3S`HKX6MQSOgJH3w~OVLcc}6|2E`C}VjmMnJ!ZkHu)Q65q?GM&1)| zIS{*RIa;C-_Ts6n7^k_f0LItPHUM9Qi)|Py;f=6ciWdL|s~#AjM_bni0cG+l)Ec8rA}3$S?0 zixOI(aSfkmnmu~S{V`g<1~lCmFYTBu_`ObGApnTw+7I9w_1lc=KVFXr&M%0KMK_8Kk} zv+qI5LXrmLTEtn;lh}8`vp7&q%E?nfPt(|!P**C0!`{yxW;b&;3$qe7$c9)Cs!e%e z$Mu0Ozk|KF9<`^k5CM|Sb5MgSkLR=Jc>ynE-{%1ylEH|!s%khKzJE9&{VsFZacKOfZP6~tiwZ`$4kFskDE zAHR3z-kaS`vadWy0tAd9fe`6FvUyNTAuo|4Qba_GKtd7-gaq?Iq!g7>t0^i{L`#(- zMM|lqNUf#RQc5Yclv;}vky>i0wHB3HYN=8R|IeH`o874W`Tu@@{<67e=A1b*bLPzB z&g;(1#=ST5%{gX+*=Wu+n~Y;-v)O`MwC0D#OwwRBZTg`8v-cB&L8SmnP z=v5b%_HAhcRlH*WsgiTZYn`GmR4=)o;uPnzE|pEAF1?l!J5e_-x0CYe7p zpGH6MN9JDh$L2osC%BPpGR|$-Z~oLgVE)WlY92(nyw&`<`Hb;d^B3kJ^OrbV>{rHe z^I2n=`D^nzj4zIu&znch-gl$QA=cj>r{xVxSl#28(<#L<|*|i2`xC zC=|oc=PJfY{Ubz)7%57{73gh!3?rzph=`~t6ERUPDnzBI5~Ib(#g*a{VvP8t7%Rq! zYEdI~ zO=6aqE#`;@(J1DMCebWf#5~-h-6|G{g*bt4k!TkkVzIbcbc!y~EqcTfaf?_gZWYVK zXT@^yIk7_ACNQ!VE5+xx#H?h*Hj`@|Y?Kf1bKMql*-v0gkV z9ui*>8^l+|!{QOKQG88o5|4__;_G6IcuZ^+-@py?-xS-$x5VS(+hT|Kj@T)l5WB>8 z#gpQD;wkZcv0MB=>=8c{Pm3Rkz2e7WpZJN`FMcWxh@Xjr;^*QS@e6TC{8AhizY@=i zUyJ9&Z^RMtyf`X;D~^d5#BuSWI3a!~UJ}0-C&eGc%i@pXl=zc)Mf_R3D*hr~6Mq$_ z#p~h?@uoN<{wCfMe-~%PKg8SOpW>YOmv~3KE6$65iwojE;-a{OwG!;lmr`x>l#Wc1 zF5|1xEj=<-rpa{al|Jdm*7BgtkeM<|X3GIGN9M{rIZzIggJr%PB8SS$WP!X~7Rq6= zNEXZCa)d0ABW0<)LXMIjlOY+F5gCdH z%kq!%l>C!?MgCd7D*qy1lYf<`#Vwnr~56(fsT#vEge zF&5X=3w#1%;m|e*w?MdXQn?2wT&7v+me)97{0ujre9w5w_`c<{{J3vzyRp~!u@%6m z%wue|f>wr=X=Pd2)&MKV%EiRRKx>dS*vhwtSVOJLtODzDtI!%|6wbZ)RT4sILT5f<4CL0 z-EQ4st+YOGt+Kvg-D!Q%y34xT>b1UPt+wv5?zQf-)>!vjYppL^>#PT?_11&dL)KTU z4c1q!hpk7fjn>z!P1d8!4Zd#xW^`>dZ}-otI&g{z{zWc1<${?A+ct)JpMlGlut z)&XO`^)u_B^>gbP>lfA`>zCGH>sQvZ)~~JStlwBitmmzx)^Dw2)(h5g>qYB?^*ie& z>-W}4>krn;)*r1?)}O3btUp_?T7R)#v;JzGwqCd1u->%JSbwwLvi@$Jwf>NAS&a(&FgY3a}zCFYqYF}m-*q7Ud_AtB1F1Cl;BkU4; zq+M!XVUMyuW{2#s9kHW!nH{ss?FzfnuChnlAGfcxKVgrtKWUG($Jy0(ja_Tk+4c5S z_IUehdxHHbd!l`fJ;}b-o@{>_BjvvtZyCGM-@MPb4C|B+80&B!?l+7r=!K3m*4k6- z>+Gra_4YL5VH}se0XGXiW;|q1w{Ns(*q^ay+Bey=a4v((IB3tY8|+4VuH9re+b#Ay zd%oRjFR&NdZT2F&-R`g#+c(>tc9-34_t;D9TkNIwt@bkev-WcPbM^}RHv4w_4tu5j zd3%-p1^Z6>i}qdi-FC12C404fkA1IwpS{Mu-(G8f*&p+VsEg&YCmj0 zVsEs+W^b||wKv;ex3}1j*<0;z*xT%H+S~1K*^k@bws+Xyv3J@}*t_iS+E3cwv!AlR zZ|}B$VDGViXg_WL$lh!J*xqOV#NKcJ)IMPU%sy!U+b6(LQ1S&VI@My?xUDgZ;AoNBflhC;Juq&-SbKU+mZHzuKqm z*X=iqAJ}i&XY9Y(Z`psh&)Wa6-?sm0pR@mEzhl2^pSS;QU$FmUU$igbD-Y8Vj>MFr z?Kn;f_8+<(4-RclbJ88J<8%B@zzI4TPNtLPWIF?#94FVwa|SwtoWV}MGsGF{T;>!w zmpg^dFsH~Vc7{76oDyfGQ|esdjB-Bagq*MwaiUI{6LZR)3a8Skaz;BJcdm3k;f!%U z>5O&8In_>$Q|r_@_0Cn!c;{+og7Yb7qH~Qi3DcaDoliScoa>ya&h^eT=LToGbE7lE z`HVBuxyhO3%y#BD4Njvo*J*N^ofc=FGv8@-7B~x?HfNF3?sPbdotvFbr_1SfdYmQB zEzVNsR%e;>S!cQPIcJ4)n{&H!hqKc8ytB&rf^(m$GO+J&spQ# z@2qvc?5uMhaMn8yIuAKtaW*(#bslyeaW*<%b2d4TI-8xZJ6oK`oUP6`oNdlGo$b!I zoX4GSJ3E~3I6IvuoL$a$ohO~|IZrv?cXm5JaP~Mqbe?v8R>Kt%> z<{Wf>?mXlC!a3yp(mCw>%DC5f3G+ku7=JL1JI@+_#C_1eH(oSOU^eV!=hx12&TpI} z&hySu=eN!==LP4u^P+RY`JMBU^Lyu{^9Sc;=a0@Q=TFWn&YzuEopFtw%}bh7dfHpV zb>r%^SzXR%h|LI_Wy%bNYiczIZcXMjZ+}6&yJ&Rh}nwNTNnmW20=FV+y?{;eEHh>#OXGcS~Q%ABobwt!b zSF^}99CZyxRl_3JlzHmI+mu6rJ>YS#ZyhG3WY1{mA_E9zRbCbVotp( zA*^D{ZSHLDYVC4vnAh2`q&fA*grgP=rSL{_VwCbG6p6C->noiZ z6s%`PUx9RP7&UiEs3KLzM|OWwq(t!c#mu}E2icuZDWfut@+(?Si1TR};c};;Pqkgtqc7Na?-*@(ng z+-gcrC=%n6RUdQaQOv3H5>l!lL*Yo&Gp{ceYrbkW<|lZZ);{Hi!nGlH>!`MlxouOY zd`inx!|~NrrL?jQJ! zE^ArcwOkZ*twe*!MWUwO?nuwarz7E!lZfUAhJCa(OPD+H+ znIw3u!s*gU>w1rtBH=2#t)qP&1|X`P=}z)gPQsv_sdjpjQ_QbQ-KHY{sHK3(FO_(k zS{H9`D(s#_O6yoZ^{lVDc*;4&b@k~z{ZiT!Pbs%EJ=D(h=yv87ov2$9iE`i4*UsFc z+nHMvj=FfE*xs7l&gk|s5@p@jS2@clSkJP)66~AT+_?y)sIjeU)ZB))Zl_JP72#A} zq}WcXBRe-JHH5fT$zm4DC9*q2inx{56(N$URZn*9zdV{ikO_o1^OOoRB6$KSYU-WSYiaz3@Do8x+fg)$7 z`f##~Drw=k_u6Eno{(U_3d-)fmNKHNDj^=KuVhWtm6q(3HQ@>vU0sn-s?I2~Q~O*N zrfx{6EKJP=Ox@=x3+pziEX1bn(T2*Z;_kX}Dhum3a#<+uUw0HZbM)vvO$ zux`Z4Lb?$v3sbWM_c*R!ePv;K!-Gx@bEquBaz;3w2+I+P%gOu^mMg+?MOdB)%M+pQ z7s6wCLh*>xQN*tUFz*OhT#SP&T+$BPWuO zs$>^xz;q=Fm4)h5dD7LZETYPB6{{x7qDDDubU!jwrrY^YSu9?J*u9GJD625aDvibc za}uJg$|$Qc8jp`v5#ub2vWlZ|oU6o-JoGxry_V&v<#=`92StbFs$+iLgFq3|ubZ-1r79>~p12dQ%TqMD)WrIg zCwiOe_eWV-qESYvNG?xhx;)wA+7{1mu&SH88|4RUHj zPs;T=C^>n4tE|B@xvN!2Gp@=*@ppH$cXXw3jD)xlBU#)kLha7sYlOoj#B=SU=6TGd z*yAq|A2Y>q9cfLyhvZMx!Dx+Y{X>;@11X~s>9pr5W!O#4ZQTti&8&)LaFEKen^e)N zRI!j$(MGD!;aAs)_8zgcHKjxALw3&Z;Fu$J7b&G1O6g&xEQUhn;sI)RsC2khxH^%_ z78gE{jA&;Lf)F1garVsj;Mxa^M2LrC(^g8jX5qvV;zDA{qUGn|cT{)$L($rD4<(I2_X`)oGv{i+=q9=a9x}E*)u^ja<%u88K?SBH>7xMs=hy9che- zI!?@Xs3c%0LWWLetV&}#!kCUQrX!5$2xB_Jn2xYqC$d~8vRubkuH!4$@s;cN%5{9@ zI=*rpU%8I2T*p_go1SrX(tE+V>S={Zd?agHKG`G&1uR8AG zDtAlk5=D%JwAZHgjzuoa`;8)t3PV{u-SaU3C9AuoqofL)%mw#HZpQJ{<>HbsWS#`rJr1uGMdY{46bs-SkmPxeGY6&zj# zhgZSjRd9F}99~70dJ7@k2ZLn?wXr>8k)Q_>ZV+K!_~D@$6q&joSw$2udA;YHB+of z*H4`2ZfNi5ZfG27G7-P}n|K6X;$YZY4t;=mQ)$>B$MAHXx{ zeLR|#;t3`k;~K~#(g*4>Q#mbc?$!dc#&m!K%YSU2JUb#0$G!$GRB#)X_9s z8$`rJwtQ% zi)3JbTL1E(-4X!=H6gjo_%x|6B{^Nq=-!Z+eKr!cpbwq>0XAX@a=s_!>EM2(_tu3# z+_g_b1ClAT6Ow1f6P=tJ8GS?&SZlP87Pk<2NF1B}J`1@gf=a|Wq@fEnv$bpC?Dtv7 zPgWl}S)mzyR6T=4%|@ck&0gHliRs;V-O5dJ=_~#e`oQ1vyFN zc;YFPyd;N&m7Inp@Yx?|C9cp1I{hC!WF>h>=s8)Ap7tix&AA<&%@jjnl81!TdvusV zitfuK@;InJt-qsRJ0>K^P9h`~x}YW}=Pr9C2a1TjCI#?*a)LWk^EIO7N+R~5vlGPp zq~xMCoSi@^{JaGIUZ?EjDn!hfqzYFJN+u^Ik{ornL=V#=Aw5iwgu>-+?Fo~}nj>24 zMTZfS!>R~3Agh?H->so&y+;?vxeZ;-*}C~tE5}$xYohg%nsHMHelQtx)LqRwG(C}4 zU#~~&FllrhsnETgNJTln$t!g4siK@l>5-_OD1ymv@EYCQiH3A9DXJ%sA{BadI#R{G zsjAA#^qYH-B(?P3+11>{Qs_`aJhu|neW*~B`%vZOTB`E$`d~x%d-EWtVG)wqh3WU% zeKyL9f<83ikgyR;W`ZSALVc1&EgH$xm@7zh{#ONrDZ~1RVG^FmoP1*9a>KpsB>95m9Pb}cuzCGR{jw;XoZ{jJ;@IpsMa!w`5I;%MD@t;j z{N!8LBVyC3|A2~BK%7ah-S;QMODuhLFS`%z7o}EMA}ce9O&6E`Nzk+e=inqx?UtMr zoyqT~>kQF!6#ykuYc1Kk7QT-`E0H2h$|xOHLJSH^CxD23-gEm<^f3C3rgRYRAr2z; z1hJpOhP>A?!AJ3Hg(i9kS`oS-NKRpP!e4SF2quvm^uTUF5-B-^yd-+UN6#H%%7OYr3;_!6a$ zLdeqPD2b}NPy>^wNh#~=L4_jqdf^~auP0WbQ9aQTtoG6PX#_baTL`HCBV6>qh|?$$K%yqrD!?JqZdFBKfCJ% z6Re@L+~ph&PVi6@LQ$R|ik9n%q^RziK_2Gko@2ao6s=(YRh%xpK!o(r!VdiD2{3#& zuEQ_mbVgYoy+8%|B%7O?17*4#a+&*UI0Tn;(Ch5$MToQ{d2x>*EL!mkC*kK7w8Z^cU{YM`pUUWT0Tze zr(Ey0id5(|WSH82g}=^wV?xr;l*N1Krz8t)Y^`1 zolch`G*ODf;iy|pRV>2fQ=C$s+SuII#(s#aOs@$?qLtO@a~s;{HpgAFuot1TwP9ZK zl;-Z9&h}(KQN7L`iRyJ{n4HEi>nL24)zr}4FtN3LVO`hUhQ)Cy)3GSi+#Sbqnwpoi z&TX#INvg+0G)FR^pTo87aSyru=u_1s_6PcydP_$?aVy6Ko7TNLa6hBk#C8*VOsiu%eJ8S6;hXn!}9!xc8Mm zZ+r7Ciksz0ZSUz{s-pF@h7pO@>r!7GuXXiYLhAJZFB*zvcOsO|ruQa4wX=CK_Axat z!r0Q=HNUl``|5b=eS}`y6VJG`|-I+5KK|b1|;$Zp9|7_vMwh?+rw+9SSW} zU2ipEdz)3)6JIZ==J|WQn=uj^SLas4YI;$o@|#n=O38mjufj*rnrWOHmk2gAtHvDL z=d{Uybze$H2`(gqx#?-YS9^IR?FlITFE+In-&D zOFCR0nMdk$21i2L3{}*m;?>%^q@k@Djg02yS3)FOmD;?t6*F<|-K`C6E^HRUQfNFt zem_O{y;;TYvkJYZBNElSS7CBF9;e&w2)1d`drj{ZfjhV3+}=hibo(8t(`{FT+usPk zS0nr$j#P7kD|JG~g|yAftGNVP8M5xo%}iq!E%_`Mb31)T^l=tMZ9Bb@OO z?k7Y-H6FdHkTTs%CKT@=%S84ZY!u0V+tmQzxYN5=SE3ibSjUZC%Cnxr*z16~77i z?G~*ft<~4Ds3GnpMfyzE8ozBL+$%!&A+57neVm|E;f*6|%7qhE73LOB?`K91vrKv) zGjf1iM7{4B?s0xD;#@K!+yha&+q+s9wPGGZr(JKoiA1oYf^-otIh@9bPFp0xX^rUb z7QoNhqj!M;=aR;gQjzj#+G6bDMM{!#sG{1{)iZZ~_xuK(W#O`rH_nxm=!pIV1d(#_ z3YEFBr2*=}e5w|+tlAB6)jU^&p3;lx-ShCnt%cs(0>N~qMK}{9oT->LV+E9ZRUaSK zyQi(Yb#dFROs?{1vKk+zpqJ2bOV46*3RhKl2&cIQCRMtL)YNf{E>q#EDos%{98s z!Ah^-OsL@WRn+Uus;Jkcwt~kSFUImQdhvC0aYT^A>D zaqKZJvoUVnVwJTiZ7oTB{Mw6E>bev|>($wvQ7~^qHjK`ZX z9^b^c>k-rY@*^=G_r-X;8{=^&4yxAS>kW{RnBF@ZiRrzjkr;OrVmy|J@i;CPVR`kQ zTkz}p8w;_X^d8p;&al$q^Y}Fu)Ac9DM;hgfaMOPNxgCod-Sl~YvMH?~wCYrC){MdjDNLgh3OC9z6(iHfx#@eE zB;RG}^E)~gHZ*oD!RV_mOiEEG%De5O;p#LjPhujYk_9b?K;a~ND9K)xWRJ2vlEkmn9GZf6AO?aS%p$jS%__RI5RckJ!$lPs*`)k+9!B`~7Z6G$!;Ngg9Q7R`(7 zNCX+tS->vInG?Z&>%N$yI%X=g7*_UG4Px;kR{iZXp&tIy1)e?p+h4bs8tm9`?Z$I6;E`ggV#$I#4R`RN*Wv z6K7jB0yg1PDATwNKhwAaCv}+kivCr=)5ZnBi>70krVA&KnP!eT6tDoNiJ9gooc(3u zH_TuCzQ=K~nc+@-!cnqgbnK)1#A{AfH>U= zaG_WTxJWDl#0gJ;ouU)4M=S$eF75#Q0&ajZah}r`0q+*QfcJ=d0Php`0X`re0DMq9 z2)IFP0^BSf1KcLI0X{9B20SjHD|OC{A=F7Ru=iL!fNQKZfM2z~3iy=u6yR=aH{h?W zUjsgGJr8)?f`)Mt#xD&MXE@-lMx5aAT|k_2fE-i@_2R|~JSBLb0VB2V1c!BfCokaK z1(Xkq{2TDU5@D@&zHIn$mczGj@)1J8X%L@x9*51q**Q5lqfXS;Pn>FmFz?xJJZy+X z4V?>(Ig1u9S_n5c&h#n4sd1xmTF(SJPjHE`2|m?{qz=wGcH!I~17SVP<|a0`u(^%R z$JyKoZXA*e9|9p*>Qq&v$;3G*imq&1@o3vN@RS*@aWm?M8fDn*qTmz-KQ(R!;5FAy z1e|j1HGose6(118PG`ER6aAFV4!Tf2%uxDkurHA)-^NA@6l12j8L-zA? z0er^&Hga)m;1$@L%rk(?Qyjn*_?bBI<{`i>#y;?@#A&gnF&3vvnK)xj-2*IelExEc zD*cM=(b;2xNqxEo(qd#+<0&T_&Kb;(kgLuOD27P z?40b9?0h_h_?2XbvdgotL=6AE1Sq#|XDQUZ45h=S(&44jk&W}1jI4*=^PGJTo^Zb+ z>kxj=WgW|UDeDydUduX@^>)_zY$MyrPQxz<>GD}Fk}SJAdwh0p_N45o*>kdIz}B1H zg5MlGZP}gKOS4yGuL5liQV^sT%`n#Dw5L3r|MW8>LakNQyb1SOUSlrCsev=hr_ox? z#<_NXF*8e@H041 zdYy3-$<#a=f?hhx;C!=`@0Le+B7&6i(fAkOMB9ycsfwm_v;)-eJRxDCJ27<;gY3 zlbJYk@e6p9T}QcbgZV1T#!T~doOw74Cl=bqY|4{4lqU_8Cyk;JZ;QEl5dBEMF!ljE@_+d|m=XW! z_zrWJ$+4?Aj%S=SZqLa3AN`6liooysAO7+(GXEDpZ^lRUQ)yHZXfqzF!iSw>Ww=fq|k z_fR?gDwWeGs14giZO4zP?f40`8b75T*D>_6es8=$Ey#=N5ByfNWLt4I-8Phm zjX2TjRpWUo6~8n7jZ*PC^~v9)KKVJE1&mAURI7#)h0!y|nZkI};GEz(l$R@1|J)3r zZMzxg_;%sc+Z)hJe;nuN?l7N4D~GnNi%r|Z8KxsS+-N zs)UP-_vNS>O{pCH9L}Xwqp4+-m!GB4)SWb%dYN+WACz+!)MyH4K&w2@6Ooba_Fs6dWF{XlkF{Xk}F{U#AOnG)%jj5nhjH#egjH#egm2>AP=l(@G z_b%n!ztzafyr4!_=6}@43i`#!3ZY|6g_C_TroxH77*pX)UyP}6o-c9^=lLS%aGo!6 z4k!8|=MWQe4(IwJ=MW!q4yXGf=TK^pb2!@D7l>|y^3;lJmu)sl%o?UN3WqAon*X|_W1wk zw=3<#e}~fk+i!Q;|I6Q|wExNP!zDS8_TPUW?)!hnu_3MhFWGm(<#E#fZqn~=q*ik& zDYwF|Tn{Cg%AWk&tI{K~Qn&DLTAxi(5?I0(=iHLII_Vdu-k#d`^PK!Z`xa2Qcux2I zUh$mn7iT=A_s2NKbB=y*0bc0yGp5g`eJ1>UkS!j1!c6v`uqSK@&JWW^GN-mDBzV8u zmQ)o-T!*61rYH$4VN>)iscVyd{liR-E7`s!HPY{=eQiul{ymh0ElX8DNas1OeyJ|x z)^XOcSJBxXk3A9B2if9WTT(OOFOPmhlR_AiTExH7Bx=H!4zn?}F?A*$^-H*YkZtn& z+;w@Zq_UWZtG`Yt&79s!N_GFRRhnz*H=!^5_s0}3AOB}*l|21JSNW=bt5dt#Hb1qU zZQ7-q@;>fsZJ(RU!)2-430HZhfA0-HPN$mJ$CaS|ciWxdUd!c9aiN@u$)3ZWYR{yU zRq9Dum=xFhAsls%rplzpHdOgWr##Iwe8IORBTB0l;Zb~O?X|0ySx6!crKsC{wH^pvE$ zqPSAtN;!>3{m%7;a+oC|pYNC2bEPX2Po8V2tH@QW(tuwz`5_zPOPJaoPYr2>UJ%2kA>}a zJa@WQyVklM^6YSJbZv2Mhq=>TpR&TU3ZWl#J>_~D@M+h6=ta?b)z3W<&lLA`dQ zwv-j_WA2wc4%`oWf{^4H*R!sp^gH2t+4ZXH4Nsfri0iED9rc49Ki5UK<@R{m+lq>%2BQsti6?LCyJm7jy3Au!j0!6(tWhrR!p9(>7 zP4evUO!dt0%z?xx2b7Lp&%>S;vK<5!x=#Ek?>otDX$pR-^ze_gN-n)BK1hsO{9k?v z7bOpIk4;bd_7dAb6DE^K3s2`cd5FsNlnZ$8l$` zl0k)Vl;8>CJVe@fO6!5_C&_+@DbOcw%vl7OL$E;U&@3c-DcMWej+G?y4&}?-sKPNx z&*oTxG-C9+8gMMRcBoY)b1tnd)hbThP6A&TwKN;1Oe4FG`1dMpoBI_s{zGBDOq5*& zKSS^r1Sb$2OE9W(&@53oXktbda1z<4l6?ljF!795bj);O9Se5`saWnO_$A_>Pn?Zp zFTHpcJR`|oM83X9oTrKMCkm~U;7|?8?+}7&_SQ6u36@Ye>ef2b98NGyFh+0;`5i;{ zQVl89BUMhBH>q4R!wgX>%-LkWja;K-pHBAAFh%)_;=WPSOd;0_RR%yoc>qM&5LXa= zzk0MqQi|I~ zkt4)kN-~s^uTqlGAfB*9y+9gOnF*_UfxA&rBan6lr>lAaI_d==>Y!<>I%Srt`elYy zT>~Ap&uk!ijG^KjL7KdtDC&M$)2vW6(Hx<|F;-KmSIeJ)vVdI25DXHmq4390_+wPP zHs2!pN(yr&*+-F1&XC>&!JCL4W~hAK#;H(gxsK8@iCha*IJhN6NmWd76;LcCB-MHG z6VRv2{{WT}G}!(}P$rY!MpIgLlRUdAoCwh!lBA6&V>KimgY037Ya-c)k_>N?eHhV) zk_?|FN-3qilyqB4^fn5=oWhS%j_fD<5Q2pS3kXgp&SJ8cl6?Znu!KUMNcPW=eGKL4 zN`i$%-%a-ES_Xnq4o8)Nj}cEX@stt_6J@xj6ATl7jO?XkAEWH3U0BaG2kG@(YnEEi zwHl3)4cHZ9cdAos?e%J_g>~=Txr-LtD{+`x*jfh|u{HoktxbSs)>gon^*CU;wF|Jq z+6`D~?FFo|4&cj(kaY+!Y&{1Uv5u*4C9IbKW7a9aa_cp~3hNADrS&#om33YnOk^AC zY)0E@!BI2zYIWeVoklQ7Fo$41!9s#11VaSN30{fMSK94mU9Igcc6C=*C~S`hjM$UZ z2Zi=jb(E+*L+$mp=XBv{H@iih5@olUkKu-iBe+H4RG=DnMVz(I?v>%Nx>es!Lw0DZDZ+J4VyPc`5K&lL2$8ev=D=>%NnSqa$dS?k$=p3ipAF7$x*rwY#@ z^leUiPIyiMp7or^9Dye_i1BnmY6-^oqf@I>C!|hEoq-YZg49lo3RkA~V(hjdbu-3? zyHfY0?oU0GdN%b)>IuM8siy(YW7;||&65@cEJ!O!i=>TCt4^DcHYIIF+RC)Xv;}FM zY0F^iO8H|9r=Lwfj|p^- zH|Wjt7I;g%5$|YkwReJdig$*$(YwIg>0Rbs>FxEd^=|NP_HOs?^6v5O_a5>d@t*LW z@}Bmd^`7?$pT`&U<@pMHCBBGnw6EGX!8gS>!`J9r;Oq1)^R4vt`quh3_%{2t`*!*E z`1bn_`HuKb_)hsw2Uhsb2IlzAV?x-|_Y3-i{ycwyzr-K$kH)XsKfymGu)sef(Cu&Z zFYtHzm-$!vd;M$u8~mI7+x@%zd;I(Thx|wUC;X@Ur~POB=K~_(2?PUqfdZ_iL;|C| zjfUZ!Ww_O@4DU9zE5rM?+LhrOt9E7hR;yhZzT=Z7)(-VfQ+qS~g=%kxf3ez|;eVQX z0AC_V{StF6*}qJ19l-|(t|#~)LF_m{%lH+78wh@t;KKwTA-Iv?*9dMR_$a~61iwyj z3&AA>KS%Huf=dZ5CwM!-&k|I-0ZiOOsNhOJ_Ia51_|<#&Ucc&x-RJiLt|8Yu2!5X6 zYCxfE%ETH#FHDJ*n=agLsBEj$-G#7i0GEZGF6wq&A9g!rVT~~lyAbkeb-Wm>j3bRJ zu+|^O?Tazot@v@{6UHa83#S%qPU_?Ex3bdyYX$@^0>?R9_}aF zgZs_)8b85}TR%4r8HaJhZY25z~a<7(dJS+Zy+&Jw=c&{Vd~47=MlNcE(S#`4We-SL6PDjPGas0OJQ4e}?fxj2~wF zS;n7Z^9Y+qwY!(~litZV>nDA_=1XtkaCWf2os92d{7J@D9~|lP?`9l5YQmpp{6o?a z&(Ac@hqUwBpO^KN#(MR#p3+{`;itXA;d6QL-^KnWvHKy$>lxq2=3e&KsBwRj#uVkFlo88O&gP1>`@u7?tFkZ-b5#yLGAbCp|#|}}#M=>5^JT6Ze@ z9K&=jPae)E59gcjY0c-`tNAjy9A&Q5xNj%B?_&H(#-Cz*H{*L4e@}S6eeAxU-48H+ zknv|2Kg9T9#-G)ADwiYoU7GH`Q`0>c8DGx+wz0qM?9TPn&Gpwkhv}>@H`g-{>&wmc z)6IJHwQ+d-Uhpkuyp!YWW_$^Ud#}dbes-VF?hDv`iN;;$*o?RPQ<*O=&op*F!RB!e zkMq-4&Gfkbu4Mn5&%Oj7m7m~2?H-Klm-F9O&f#)9;^TJ2_aWi=YB|1o_CKESxZYTg zzDb(yD*RwRcU&JH?H(*=IpcDaa=6^y__)3C#mj9X9aMi(`27~-b|x4Pm&;Eu$nj)z zJlyUC^BFJD?%uaF?&bG$3g@r)4W^%YA0O%2`!>6uWA}F$KhO9D#<|}642}D^-UoJQ zJiztr!@>)!XZ{Tw{)fmH;Cdb4dhO+Q&ig9+e~slkt?@t$hrfW`Ut;%@jK9qIDaJn} zy#Lqw^FOQQ@gHG%68S;pBWtC0&wN(zKKDPRar5tZZDh0jF9$;YvjK}RPhd)*F>pFqADj{B46F>S4O|G~`xbob zVq*NG%mVcu#0V+=jez>nz>WDWbz^WkzJ>7_e%w7A?02hkfpIzZ4G+Ut3&q%}IRZO0 zRY;frFG9*i{2x_Vc^_d|13$vB27QEK4gLtj8uAf_HS{A4>#`3Di<%h#6C-|88fNem z#_b_uO(u;tS40q4lkfV(c?+p6H>m;Ma6 zRaO8_x^x_Hyd10GMa<*}t1lh|ywd!>f|u~6P_X>cVZf056yWlU_!cX;M6Jlcj#dHo z-vZ`bQZOj%6}%$E3mDOhl6ilShL zv9hCOgX`A-vt=0k&t1Z|Nr6L`P5>T|=qCgYT$&2F_tN!%yTuI(Uit&zE_nsuIuk1( zftSrE0JmN|2l$M+6L5oxwlXjZUjl#!wc8BbiyCfb4#)RAX5gVq&9D!`{D_&Ec1iid zLbzd~U8@9iFr#7mFJi94MEio!f)_CxVxnDwJb=F;Xl9&-`4;%m`ufkn#N$WHKLBC) zPa0;fl4BHb6=M<8q1ypyMg521j~(Cqv1J93Ei7M9MSDVS*<&AExrtji(dO7`TNDkufx5I3T|NlGi#|Maw9}H%mtY zM?lNNw@fdn(xUdk2M%Q9fR+uuPZM7z_@45Yf#xB;dEh&42!CNVzIw`d7JRQMzTgGW zGJ|g$B6t?G=ZW?XXjbqo(9VGNBGKMJ94&Y!3Dg@E?^O7G*$}>00@Fb|2EG%-chtb< zRv#exwTcfVEchI-XM6_$4-#z(Y{29f;X?%X5baLLw+S+5`v5c66YX};RvRM6@58r7 z87qml3bdv0wFad^WV93Q4#cqvvb5zcfy*+4ynzi`yjtTOCj1GgttBmB_yyDG>77F5Z)ln%!mN;7pYY31=1VX;Clvf zAoZY40BsC3z8AB_B3MSWt3WG(#`k(v9wEKpQ|*fvaa3n-2F*fx&r!U~K#K%asL-(d zC(-IbE69a*0#}0eHqp+47R-S}f!83Pk7y@B6PZdrloTt6XeckfQQ6Q=U>|4!MMHZb zGUum*+y6GgyNhTDZ@__cXu^;1AfKv{;K7zlLO0-#^!BCeKg4$eez#^oBHweMEhHMWK45&>deBh*AYVJeLr7_N z`k+P3C%(Nw-JxP4Wz_$n3uCx}wokYWTz&_|XZCd6=(6pZauKCjHRV$iHlAqz0&R>T zQqKk-2CWP-pC_52=hRcdwV*-I@?E0kgLWhn<=K}B+F7C@CsOwZyFoK3yni4(a0C~m z?g9<%0qXQX%x9m44qi>&jFPVPjCa5HB(P&?jetkV??lj!z;9*RRM4Iw8s2{1gP@H{ zs|W3A3U4s;6{b$>1T?1MH(bj`jU(w5Qfk zqJbkd+uIA;U7%e?G;pLks@?Fe0&N)4z~MRPh3>s5_m+ofierW6RlwyW^DBrK9G+vC z#TVYC;7cdI6~LBw4gx}7$fLi24;-GSyq%yS-z@&ZCHeg< z{DQ-M&N~{kO7Kl1K7{Um)qNVYGORzT@A%P{8UkN#%tR}-+mN0~-Vpi^BJeoAO@2X6 zR=xsHfOYMTbOl5bpv*rf+Z`2|(|GS;QYC z{+S48uWz4wKj@PUX$?|7z&{D}9qwJKUI(fv-it(^0QyGv<^c2<7z2Jk^IwU0)h^4m zpoci#QwV1i_*eLrx>tf;!1Uvw4+Xv5-RVPl31pHysT6(?^jYpkpVFs8bT82@&?mX4 zsCEIXN-Dj!qN6`3-DBLa3~haPaA1Mv5S4)w#@HENg^SVvV$Z zh1#EK*y@A=Yq<3yP8<-JC9}ofq)+O;7g~sSiQGIC_?G*#h7Znf20;VgEd5Gddn8+(I;QwyLxZwqi*agI~azcB-A;Y0HXj1@dsYyGjY59_V_&8_A)%x&g3F%FW7Ct#o? zyoFg7<_4ZBhsdGwGFc!mmxXeeERw}?xLvPevsUrb3%}&wLaS&kv()Of#?h)-3#_I- z9IcWy!SSd)RP67Ivdgiqod!l&>B;coK>_*U(QB1>eeFZskbg=!^&hEKd9 z-V|q~TY6-wOq1!-D@Vu@IZ~F&DhLmUhGbO%KSCaaUfcCO-!GjvFZ1NdA z&oV^>ycqfY)Hq;%3*R_?TLkcRTL!*9n}%=Eri&ZJZQ^#Ex1s&5L789g2RruOaXRI4 z!S&6dr(>wMh3pF{Sm&g`?0yt{E4~W{Hb}s{29Ks`?>iHzP3AL{?a^b{z{#D zk}=093@i_11_t3f)6zg0zWS^UOhl=`QvUS?@gPJmh=@JCnZVY;qoRwmRQ%wmIK)zU4fQvp~M%>~x;Mj-^MPEzZ}Shn=rF z8=Xg-{mxIF1J2K!gU-*LXPjSP7t=2tj0w84LXG zAb3$hwHp&~2f;@OKB%BQl^3d_Re2FCNr(%rck(yc+_q;&}_tdGLhr zCNS`L@MLNnXHa0K3C~dA2rUb}+8|owU@7cqaf1;&il-ir!qxb?32ullh!&ps7sEum z9$bRwc09d!)@lE_iU)Hk=$o5abMUljx8O#Y+wknd^E94=c%Fm%Og!`PbmKu-Szz#VDJdK~5fJZMXT=r<64686{doW*kiZH$A*j|U@h%(<8uqwrMXsm3!A4>XH* z$jpFj8Hh){Q8Vts0~s?O#1W_W$r}4 zX}Jg|cX96RfW5gJ0Jr2mX_!NE@D$-G!z>dtl?x5z9>Q}B&nY}-@E~XNkm9@`o_std zc*^l0wRw~9Ajk4r@O0uqO7nX0tjDt%&kj6$@EpW*1kXu4r}3P_?3qw$7XOx(N~x*^ z;xuelW_RZ8dFKb>T%ydCnfDG%8<+>%n(U^5`2$N~TbsFYU})eN*fwR)A6PwbGHjbO zcMP05uo1SM*=++`2BKdwq`SydGzjMMqM{#<(1Cc`DJE)cj`5iWQ#&Ogenc0$^J#Z}XeVgf<v8agM)jAq`_Z) z)|~^9TkyBjMvjqus@6h&yAbZ4>}dm$OTh1C_j`5tz~PRQh062ZR_+K_``@bN1P=GS zQ-d4xpVa=PYC(kjvZ80~%{-TPobc6}Zrx7wU0KiOJ*(-!75^q}+J*7(VamKG2OZBx zorL>VPH!Qb#b}pg#*(a!s-&RrRz!Gb=83#b%&*+rGnVCTRJe5;^H0k|9Rh!$jUGxy zXU2-W4dnhCrLT--S(EY}VjS^jEXiz9r4jr?m3!vJtWkOE7+3C%nM?B4DqKED{%2)* z^VTq~+-GDq=G{y9Ch~tM^GqJv3`(zZug{pGN<8pKP>y8Aq>LGPsOyA(2l%v%#=JWj z{}%9B8N2dU5&mVV+H~a)J;sbF8He(g5q>}DDxCFsO9{W1@Cli-@|I|R(!YwgoBi)p zrcq}OAoHS$T8EM2X)($Wm#@8i3*gSnG2XiT*~?!BeB<(qhCS$&L1zm+g?WV~hCOg6 zbXY#gLxx0c>2JZvy zz~Cdp9v}8JY{v$_GHm~_Be1=eZw)>->^S_hQ`J|8Vk?nAj-f|K*p3@hKCpFix- zuoJKqfFX6oavUoHUG){{Y4l*=FZIBkh?YiAZUk%x#T9N)2botRV~h`-Utj9#mWcth=~m#2)|N;>u#|R~gdje>&~XAuEdOi?MrZ#E#;r zIeSLz9r5f4>}eR%p0g+CV9t@^sl|^$A$nj#7b`wTg~ZVy+5qtP|2*~F(vJaPqp-#k8J8K zp!vvV{iZLJp>Z49#CW`$`fO;g|ABlV`P8%?$ILDV(Tv#&QaPljQ3yMdT zwD?vkthf|B2L_Dtb(gdlmiLVJZS1Q@zrcF|yX##()GF+;_xrMagJ8??<@*YKC5CH& zm2Z@c8IWJHvSh-@jUyj7TqC!Y)E2#!eX;0b>DJPxN)H+-B@0TbOP2VGurptb++w7R z+&psU6~jx$3_f1^WGVW5I9G3(vm7Hb_5QJpyNxy2%Z!<5*wyKr2hCB+F6YZu`8r0$ zu;bL9JalAgh<;T5Rvxo%huxI6;VyA`50tt_&KY^ZyUBf`bV=zd$TU5B>By#$i$^XW zdDn=SN>_lga^$@uH;mjma@WYc?tP`pK-ul>F5RLeDm_ZRRNIbo5b+3{XgJDnkJvnN z^N1bZS*0gS_mAA+T~Mm}3$}d4@>r=jSp}=5nE&~!cvng}SgO)xi&w=tOz7nOf0bQ% ztX@SGpSkD0uVpC|N?R&j9w@SqoTd*|98YLD2Tm7O*K65U>&o zc?i6SKnsy*2r(EFh(rw`iWQMXkR3rJ2m%FvzjMyay>t7Q?Vh#4KT=IA%m$A9{g}W@+KDXy`iyRA9hw3 zq4tfvyD*x&d-vk?gByDHhY8s6{vJ2#VNZCM9Q~aZ7ovP7GEeMvMDN1hg{a53Ax5m- z%Fydw+53LjPTz^RL+^p!gJELtq29wd`H#qmd>8NVSsItc!FYLG9#`Nd|C@Nn4&H@S zt*BO$qI$7{te-DoL#TQ zt^9R(YuF8VztVVoPw|v$5HI?f-H zJ6WZ9pLr-9E@#S9c>WT~FV~}xAM43U@-;x`$v5QU(5pXF?^I943@6D^VFXW#WrbWJ z7uJ)hcgTdi%F|SNPfnIoUsmw;NUzV@P2l7$9g}fyTWq~{^&&!MT_VQ18Ti%y<>xuG${H2~& z57b-19&978)O~V6Jw=wI|E)}C!zcLFH2Oyy%bq6|1b$NT}$jUIDdJ+1m8DRjr zu_8>XRzpK*7p6h-UVxtbqk0Ke<(2AHSen;iDb~m}@Nn0|VyuNP!`=tp4t_%JmG!bg z?yHy8gY_!-n11tC@KO~tatmZYkUT)LG{RbpMbtVWPJo85i-=udISzsC`V#DGkMALK zeIKd%zJ+pa8|B&r<=RB$+BD_b?r41s^$xnq`Bu=sezZFP+cr{n*ITPa8Lbv&j9Qej zI`^s2(PzUR)YGu1@?mf2YaRB1&JKiqskdQ2<;)D|@2s%D^5+1&7i%@%C|Bv*WSJRV zgx?4svI@_x#jn7EBlgl6_u+dqxFHMgGB1pX#~~9l65p1YAGXll@RlkeJM`^^{modF zlW{uZ@jBy)I^$h+#=Gl`_thC6P+x=j?Il;^&B9gh;)o1D?_#&@U@=C7b4RuG2I17O z1?^Tq*x|QudlV3SU<>!T0y?OG;1yexvkGWl0i9VuXBW_50WB|}RRx4hSgSwmVOtP# zUo8k(v=($z0o`6e*h#f0@%`Qw^iTo)s(^5>t3~-_0XARXxo?dH-}!Gt2Nux51vImOzF0tq7ErOaadv^5Q$R--&{qrSxB@z$1E*P9mNtZQJ9zc^&6vYgK7)wt0=0iA!J4HB@C7!* z=rHn$#rPiYz&N2S@VMVpz_>-Iy?cAH&M+g;5Jz-iM1bx35jO_CK6fLyCfLD`@4yo} z^piUHgOtyf*|s;XV=b^wmJc*tZehOGDgB5j&qroBRmn?w7Oi+h+Rga zc|yy4lKa(NCeJC@m(t5xmhsu4K~9v2qZN@cec@$6E9^{6~*zEp!Zs%XED)+Vc4>Doc8! zp=DoiE2XW!h_+mR^+YNl4l4086CS-#>kjGQ4@-EJk)TnG-V9OB@vZLOzK$>8ZT@BZ zD-+(e`ic(z=8U)g$-8#@KkeY}>fm2Wc&s4D|8j|E1Y6jz?K}9-W<2#FL`10V{$q-= z+`dP|#|3`>T%V_TAs&!nvtmn1`hZ+!oSGH*BRlxH z9sJx5{)7(xgoKBs;`%M_;1@d{C`i``m3s3_fmw%^=56UX`X<~dDR5I9M;jMHe}Vgr z?zbX@q~vKsXelJz)}BrTC_{XtoGnE(QZsx%o^eX$C=zMbcoSOh>IM$PNB0L zQhvsB3^IEi&vTqgR=hx=N_ggnW;_;Y{3Xve`J63zT%zry7-fjht**xh^v$RNU(0h# zFEj7Wr@A6V^bMVt7(NJl$F3XHSE z$;>^T3p5&3`B=Mtd~~)$7VQRnE6*&0=?bKT|8+?1PxIuI&7LemJ`m>{9!V>QwJeOK zb)+5)yYhNIV0T+_RmuAw-kXhlr5LAu&TPyVz1N~^DJ6Mo6tF{o|(KCd{lIRXC>z^xB*SVOF)^-xyGBB-hr>j}$!{iJKz=wr2>=|w;m%1Su% z&oQO2tdLu#BuhHKWZ&9tKJ0Cpwfi;Nq$RJAcT0N@VcB;dq=GK#p1|gSvhNTm?d_OW z7~i&bDwXd~v>wa#U_e<{4)-8@FT!=CrC$#mDNWm%XHrNdGX&&>)H>LG2cyK?^K7|V zkj*S1(5jZaq&ClX>1bOAp`|c=*EOevQv-F>ESo+Wt|e0{VOgt6c}x4nL2V^L%X6C^ zBj^6i`qBI0UPayupiO9fvH-p#4i8$JS~uW-;~3A>4u{`4_c%YcsdM0%8@>?AII}n* zJZ-Hwub(kUt!@bK6gcFIY}9(r2o%N)jm;dS7G|k)8FYJ-B{Gi;Rxojb+K771I9XQ6 zB1_ne%xl|shS=|RE7_!^0gVk5u3l?33DNc`&+wcYj@*@&sy5CekuzGeWj&K4-f2m# zK<3?+H@#;ZTU5%-vuuIDGnXZkj%=0Hx1`y6J)Wuhs723Lbu2DD{S6Klr0fspuBo1$ z!M5tb=QcQ$6-u_Q?Q58Q*IhBJ6MaN~=7W{VOIR|u3Djn4IG*ZE_{krWY|U>ex5;yR zPU%UbrWBYzXANp*k`L8y_v4CV*~W}d_BrhBk#`z3*mEmuB!s&;d9N%iXNcUlNMl$> zkRk^=6DOa#{;t`lBpupjGha3e^*9UjT=XuZHf!h7#&0UW0{(l9dr6fK*1F8vA?!4l zZq^R-^tKO4NSa<+M)kKrXSM!2kMLw7fOQ5_Gf2$jc~&ePR|u(`$u%zPx)f=yCEE7^y+a* ze@utI*=1R5f1EG_qf1K#+LB($} zdiC(6-=zAep27V*`BB57!|R*hcllVa^~YI#jRIJITx#F>Ip6Vi*D-|o2@kC&{Q|?Q z|2Meg-??7j?lOjOUaD_CAbcy;FX60j-^=tCW!{wO6I}K;WO()E9M5f?`i~o4y)^NU zyS1dpcdzICvH5Mx?IqZ(E9jGjba}GzJeBC(qd!%yAF}8wpK2-5KfW}>IF38Ji|E_=Vf8JGZyug_Mw8s*+HER&4)e5ne+pyb59JlNseC;Mn^#7CWWf|xe?<6J zgEcN7{0D)B-EOq6+HvY5uR#*MK9L6 zmz77%;IIb0nw2>U#xX;9p32OZB$^>D(@T-d{83J~BOM)+_hmd#E?eBy@S};+!70cU zT2jfdog3w7w6@OuI&FIUiSk9C?C3PAN%~|FM#k(J0#BH*^I!~Yi4HP5yQC$4#X zrU6BNvG)FmLx1@0NT#PI09(>;Y4nOC{Z_6OjV;ywfmd6^69m-_%Oi39h_A^%AAhUA ztS^Yj&9cOO3ag_#2()Otxxc`@pe?8P_eF=OI|2X6FIk-?>bE_>5U?DPK`HlW)Io34 zH5O@Y=8b6!>!q>a#)poIv$YmeCZRPyP@sK1&O*?Dt#`V+l z3Z-}(oOfMio{IC=HnPsylSa?D|6z}5rIRi^k=n`9^aRJxJHV|cX?~>yB#3=s=i&1# zr656|!5)Gw+a8T|4nv+a+X3TS%QG!jYLl!G&UfCYd9bB42Y3RrxXxi)B~m6?OnC>t zol&e`Kl;&eVJx)8UdAbPF3S*~+WOcHj=p~OZ)LaLDz~WBJRfSxCO1ych})zq$x~~b z$_MzJhBNAI)4FkVA^hA|&FY8n0LO>Oj%!RjzMs>!7`QW$@T#%-4u!S0TCs_n>T57Q z&5nk>45#PcA>4>LTMXIoHjUxk)@ml+8#QFWvsDdDTZ!JaHOh$R=MwDa1xcnjHvTkg Mj&EQ!95U?x06XQQjsO4v literal 0 HcmV?d00001 diff --git a/app/src/main/res/font/jetbrains_mono_regular.ttf b/app/src/main/res/font/jetbrains_mono_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..711830ede02a366f8b99f88e52f3148405e67eaf GIT binary patch literal 270224 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

{Qq;dYoLXG*oL7!{wrG%~aKb=FoR7{s=k| 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

AzPi!HK1CYdMasJ%868x!dF;`ny|>LRT>eY z{$PQlAj6)4tPLfnc41B_@hX)~aV|yQ<-7b|?`xs~--pk+ozsu&=HA9R;C0W@ z{@#(G)z{4&=GR^~M^%9h(nHq+fb<;Xp4^I>=~O)t#bge_xQIx7Xd62a>k23%1$c_O zEw=J*i%JhZh5A}SdVQp*@mC{>1N?PMhrF7-)HZiQ^vC*R?5lFLjmdLwcibO88b5Xn zf9}Tspda1?9wC886i|IbL{u0K*%%5!U51=H_7PP9gBrnhH z#yI^YRi%}A#d%=D?p!yR@F&u$0JfCA8A_-d!aZ5jRYE0yDV-P>12DW1L8#@cT=^!Avg8;M)0z5$A9epjG0-9_ANIb3dB@C)gC13n57G^#Z&O#YLiN zFa$Mlz-kta_Ds>BHyU<8l0if^NsqK)NMM(Mt*D1uUsqdGjaV0|E{Gy~>OZ9C08ycm zdyOQc;MEKf1usQ@@q*5~ZPeEtSxty?X=?WBah$z#@vV0G`jh0B{t#P|b?brHL}TMb z3ONz!W+wEILfH{*YmGEFHPls<`@E&a2}+>U zt5gb8!;K{%v@BQCV@?5UnBP+LP547qg_FS|$N4X?m_z?H2=8XOd@=AJHS4#NlGzBM zKH45_G&wp$$TVeSP7&t02>T+aWu7dSGELS(8U(u-MOHxP=6(cf{&~>30_>_vaLWf2YfA)0 zwIm1Z3O4W`!R}@@Ook8+^-zUTY#E8=FsGF{Q8rPhgQAcrTCJ^CP`M>1UFpJ=Sx~Cg ztEU7PUFuqR=~%3$x{A6)HP%-}sw1c+0CXx06@?b%E*@-J?}DwGs%oUl6dYp|(#K_t zxtSD&BUSkj-&2K+IUlyHc(eLVRX;Q>l3?DZEM*uJT&~QC9H-l^*mPRm(RSWs40P*95H1daJWkZq~1d?mEeM$njD zu^m<_Iw_&RRR&^r_~@bN;>z^;}1UYhza-wFVF|NTGnwTapFm7hn?fBWT-UV1)oqwv)R1 z2~#NWX96z;WXh>LnA_lTwYmVCk>Qmqh6V?gFYE8?RhaV9sL(F7x3v;lCBXK{FsoPu zW|}ESdrzga&^uJINqOfmgd##P^r-JM&!)kW*;HUAJ|xXQ{UP~J>QVIrhcC00$$w%C zKBfXY9pLm$1-b%w(?{5590v*BYlKl@BfREYKr% ztEmc?2i+z{3qh-35+G1bQAIYaf{|9CiPj4%Ve)udJy?gS?c26)Ib(A3#ObGP+BiP8 zVRZevwQE+diqraFtrsrQ-@iW1pJIJJ%{po8w%B@U>-N!f`nu(6>vqzIv<3XVwtx#h zD9e{y1}A@%HVs?sX5b&CL@$3|sEQ!XUo1siZwxdC%onM)<{iK~;MkDXYQ&CTpin|(?X zeQ5g%&Kz9oTzDxM0ct}RLiX{8{Ge&XvlRo%KPLN-!bfxr?+fUBa-l>!dwg~m)`5MS zJ+47rCE?$V$B!MKhgn60K5_@oJ1>GQ5!L|`N2rM@i86MGG8v?2NtjrsgV{1|s@WJ? zEN?$SRn_3ZBX0&*xl*hHPT;ZLb+Wpp(H2;Rhzh^bmTn$arL`*Y#Ox@>zLm9T#$lSm zdqZqLky-dK_s-vexRD;tamHfnYgi^53iw6C;NLd`;y1WQ?LRUDWa$xaPHK;QO0)Y; zF(uvJ?iB-vH^okGYB;U)MDHTA3*Wir{xj~}<5_24cVOiH6`x<5rOp@n^PizN!e{~= z{3zKm;8-Th_9cq;O=)Y;({E3nGec1r(ARP9C5dyh$a6sFJ|ZgbRsHI_pFAl zARK~`P^YJ==_H@-fEic|nIx+f8H2Q@9!BIC=}|6OHTp=hgKe2ODBe6b7#06C`@v2) zGvu$s3MKy@HYkd7`oHv?nWOX^z3Id+qWUc-E=qfjoP+1QhWTU(zF4W<<{$zi{ulS$%FEHTVEf)*^FW(R`dUv8zS2i zd@o#Q3`$48Nw$uo-1{ZM_N0t~Y_aG|K~YG;A@Xf~9AGmLYM~fX69A?T5UBw6B7kV_ zNe<-K=PXgFVfNvjbN_+w@)wh^R(waZE{x%y6?~a_p5p??8`vX{{u7p2-j3t9;kb$7 zw@{%+gAk%cl<+N{Ij=*?H%5SWRR2lP-nQ z+YvBCL2=R+P@g4aw5HH-9%MxUZ8(a6Hh#iM(I%ji@UnT`vt{C;aq-aBA;~!V(GVU3 zyX@OU|K@)O9jQ20fFHmD=yxjPf*2@#B;|>c`Z%$DTeoEE5U$j5yW@Rbsq#=(qukXQp2hyHYwhXg2=&Z?c+F?fW3R`1{X%%hyOtgW) zaN!EfGTV`Q2CF~%7biw+wOZf>w^+I@!zjfAtG_bp!aQ`OF3oafA&pEy*pp`9Oql`2 zh|w(gdzyW6wvNx_opaecGfy#kJMqLx^Wif}n-de>fQ|S7{D5fMg|dVg@FcB1FO;k} zAtc2NMd}bY2(YR^b#(wQOO-EBQxou2(HaY{!VmB{;d(4_vw~yTg38jfrG#LPlkj0I zW=TN_5A^9*BfI;LqD9Ip!1Z5XTo-X44DZg!eZrF%A3SCVlR`NlQS214`bC68;LFd|V<<4R%ja|o&t+mku~ishkjW?n}x8=djU z{;(x^203?JHUvf395(c!G3kX&mXR_v&H05Xv$BNuY8ta=w`m@!xgT&qa0o?_FJ36z zkMU5HHEOKzUX{Y635D$p@c3i3iV7(dS1UeXE3g+JOeK?Ls%hbBkUItBdVx3UV&Yq4 z+Bf!YPGMmVO0vplsvo|lzFUg(`Y8+X6n`*G!0j#UkNtcfJA|rO1y%jjcC5B$-QqB; zn?i%sQEn1dItT|N3) z>Gj>zJQ?(!pJ+IUTP<(}r=E#8E7X7%it^oMIbO|GK^24}nx!Jut(p$7SJM0wOOudG zcZ8}B3VEO@A$G(d>33546!%yHuDtOO_T5tIg&Cp~A;ld0gfqut zd4!qXk^(N(+RE%@%6`z&FOohMY*s=6v^|Gl3!4nW%3!`7K|p_gG0i7iaRiTJrYm9> zVwLjpeY8?@w>D39wT;8gtIU;rB#0f?A$I($kk_ul3~XxbxQ%;9@Plw%_$#}cy#u-j zo{jGU8`G#Cqj3Br@e3%3snU?9-rlC>o}Rz*zs*g(bT2=AI!*A?R0&|D6)5J84g)r1 zq_Yg(MSY*k$v~GPJi3v>86(JQ@Q}nyoy*W$B@*$#K!}JNkH@H%fP18`E3-Pgu*12l zaJOTLN3UON(C0>+y9;+YI|{RFGP?S@GRiXW|5W{H&ZcZ#ma%-K+>md}kGL+p&=tW4 z1AZ`O>9U(0r`4bCi00?FJ87&ht8$i=Dvkji)j%7dC19m=qDY(G^0=!D*=N_2B9w!uT9nK>Awrd z@24OjNLEVx5#sn~*!$SSLFqckZSXdeGZS=+@{p;1RSs4XGNv2KEQxRl_ad+6M4|^a z0RV(@DLP%Z4i$3*p`_4*a?rRk#~VWLGYaFROmC$!O=7p&#WdpftITn!E~17u?A&Om zP3+c}|GIT+hoz@M30)Z7W89ha9*TkwgKfq*NpDBBXHvTDa66E$Zc}{*Nku(N^)g_L zu!}CL8#|y^LUBxU;b}T04?)7gHECvzq)JQ%iUpdm>p1N@MV>%T2KJr6z`6b)p262H z$n*R^RxYH}c?t53Mcgg^fg%;C_Lt)8lmt&Kn|?T?S@aU@oF_AxVsB z@{A;|`uoHglD>k~)j{>0y<1UMRzWX$llBRdnV+9OkC_p%`v3zil9*syy;Pd?RnuJd;vRQO%OOb`9<|avJk1>0P+dxw!Pe z@bQrg*V;#wn_(Kb2lfE_2z$scERD4(k)OnSDV!PDl??nGqrgupriPqHCZvaZswL-6 z5sI7=?#W(SJi<672zgaxjk3u~IY5vp29zhc0dXHR)G?f{$9NHQ zyb2$KLI61?Rp`B1pcMA}=VLQI2)-ImGz#Uh06`o;ok|Hlg(i%Q1sqU9 zPN@Q(#tDfF5)Xu%7{_rhjRz|C!i~WFFk4cqaa_}?)9Dg zK`zs%aK-nB__OxlfyB@Bq;EqD1z%i2V@Qxl0G6kFQ9^;_`tzYfvxg3`UF>b2UM=@nXyAdbs$H_hTeBcKHX02~e-#-OYh zR*Nsz-OBUR?m+{hEFX~zDdGfdElgZ^50ZPv=^nFptLuFjd-GlF@1U@0=wTZIPfAh$ zlWKXTgncIN30x3I3SwUM6m}5fQ1yZIbH-wsI206uRo+S@jq`MBRsV+VLlupceBabM zcBrYA_W5Q=-NxQX58uzQvzwJat73qQ=BsP ztx9i|FH4U`yeQ+O)mHF$&>a=5nk#0;Cxu{C~l^K-Zgo1Ep zK?MJ(A<_?lU!sN%RBY~@Gj52-Z#ZKe;xf<8+I7$N<5PF<%9?#t{15;2lgB6cJ`p(& zc^o*V+{*!vIqv0gVCWdTAp~KQhQ4>p_3`-iHr%QITmc9!eKs2OaP^ScPOX|pDd(iP7~foCwiM=V%gM7@tB+og9kv&>y0VuP zWiQFw!s*rY{AN*)=LMywuO@l>>5G5`fXy_!kNr3Hy&ZBaBrP&HsR@{Di7=d%g$VAF ztP+UV^kA|NpfeHSJk?Gl_KV1~^zZDyQ%15c<+J$41+GdytkyyrR{A&66Q|iR(9mDQ zzW?<6)#3M=J^L!gcG`&cz525BD>BuibJTOQv?^MS25q_V*@4QwYsbGp_j}hK$nizu_58>C_jX1YT{wbf6 z!26iIf%-(q?19ym+#@6r!4pO;!eGpYScR@6ydx7nqmP8c>Ge5WUNaJn-JC8HpTWN8 z(T;AE5TSZDgCM0)5GP);z$t5^7A|um^V4&KvA!mVXfnHq&otEO8Z5SQsl^xe& zm*N46yg-H{aguoZsv5sTqNIPaKk!H+qR>V%{1AyIqt;d9%+ONN)<4p}s=1;j;PnP9 z;@k2wq1v^}*7fYpZ_Cb#Rj#92BzSHY9!{}9r0=5(8O&&m2k+#fF&2fTWm=FVPjyjq z5E(}@swR2CRYZIdCFG@jt~{5Aa^@q=k!BPwgmr?y+(vf4%YO7igG5@RF&6P)Cc_~MM#yfnT-Nc zVAVd9i6+y;%%PSOwxEq6wfG~TqqjvPoa7Do-B*Ssk^g($fxaULK6h~a;J|2qtfIv% zt{)lSy6ZHlcCHY~{acrPVFS}0zWu_zzZu@Re&C$JRi%Z#m21w~AW1y(D+*gSuZ*mz-OyYW zs4e>5#;w<0w`H=jY|Y$dCmm#$@*`q-YkAX!#kG<4x!-D^%u^d$ejV3 zbk+nzf!VVke)3>TxMHzmvYC#oYV1DR-q_mMuDv7XY%kXHG<7|M&wE)Md3H2r4&6%yzNtX@?a-mExq`j7}Wt(mD$=GxCjwJQJ;Mv8Tvg zs7JBU$<&`|iTgg~4Gj~{40Ob8O}SM;X1+0xkcV7PD-T|D&fXnU%7P#I!bMjcyyCq5 zdk>s*;LKf9XYV-sjLA*o8%EczUb&)w`?BpHj|3(E*N{MUJN^Awl8P}epM(@ijG0+- z(m@Sl%1(7;?kWv~#EVY;^bcay-+^xlypupn{|B){Z$sy45L$%q#OwvkVh)#zR*R1z zSw^KKtxy+1@%0BmQVnKGsX?w25}l?6i;Y=L7ArAU;zd9`>h&m08j-3>Xi*Sb`gdFl z8r`bi03KEatwN2BdH_`TareTS8x)bl zYi=llj8b!h;LZJpF3UmBM$M{Lfkf>LwC zN%9U71Yr&#u~xY)Sb%N^iMJ>hL{0{7R6>0?pEKLxAhTBh1_q>W@QN3_nT^*y^Xj;M zqgwQ0nu=hYxW^|B&!hN;QtbkzFXq)8YO1+N`QbgWm|}H9P8`Vv5VumzZlsIrd44zq zQsjpVYJNDb^?3s@z)`G>ktDB^3~(?$kW&uCoXWfSC=?{3WP-QUH#94m;Csim>6KjZ zSV*$yw~fgcLA$4_D~W{gluU8%n^`Oe{AAM^&jAO_7R>=SA}$4)9u68(l!cH2e(~Hs zvMP`{jUIv+qu;`=Py`qSb(cQ}sZ)13&pH%m?*@LH&GEx3tXHBJ5DK9n0e>jUMv)JI z7iKBKr&KJV18P|lj1BG379F03Ff=@Q^|<(hxwg3v$2Gi>dQh-Mn*HJY^}<`+S3+?x zVUb zVE6!Tv)TRQn7bs7i)-c{WKZUVES6A?+yh|DKB>IqYqrWa>|i#VTc`JAGN+JSBZ>pt zp{!Afwnl{AsNaM%eT0Rf^Z_j3!V+@42Pj-jpR%LX6X}HH4j7B&oGM_92Jcq?!)JBn zQdVykF?sWR`LKSk0gviAe9fr(7>`+;HjBxa0$saOIZJ@eKn1Ovh{H)b?^w*q{hDSS z1boa6=AtAx(?I#!I9t0o98ZE_k`7u0wS6Z!z7T>`DFPMRR%+zZ zlY^=`8A67`2?w%SBbiho2vOQ@r1dID7V?5_>SlJKI?(j48NI&UrIZWGI`IQg&%=kQ zP|)GSujeYYf)u+5>QFquc3}PV(Db@u7MlgmzLeF=NL71J9nnPgQ6A*xpb}*Xte3xi z@;aTgMw0A8t*?)y?a10}5~2cXibWgh(Zv$Y&9lt#A2nCy)>T*4-&)^NTa|Iu7fjRZ>pRdT+~nCk5Z__C z^ior~{33;TT9AVTBa|sSTl_H7vuAMiZu1w!$Ev$+?-*O={%nfYRdr|TfI&*iH#~)XnM)Otk-rNJLE&MB_Hh#E301$+i=(jBfO!GI z;p-B4#D{oN#D_K*f>t24j)VN8KmKvUAOFa@l-ORSDns>OBx4p&Ym9k0>vFQulAyncj|2vO=Q^_6)G^0G_ZC8-gm3LK~^ zQtG^^b_76?R!@SYaQaRDsS1GiMej$qh+gJWzt7$)dc`1qNC64VB`7a}Iun%X3Fia3 z{-H-7ou|A+vKNvp^Z!GvEKP)BW%+wr_>KGkVhi6@rZ)F0Wk$14CSq`S-X)@cPmY$Q zz4ak#+n7+>hUP`JK;e!eAv@Jw%)>z!Dm&I=5&j7q6)X-Z2i`{Y+ zUf_xVqIPE^%tz_bSqLpebk?cFLC6Z)qX+^s_ZUw9=q}~OW>>8EIscz(S*W^-OW6IG z6DAB^-;0{W1;BbG-Dio%3;MHY2SSV30bwD{7g!MU;XEBBIS6{xMqK%;;5cN0vyQ-2AuZr|a!#-Zb!Wdl?xbq)WY829XJ(oSvMvo}lO zQ;s`XCz0p`;Atk#c8&*1ntpS*%r-c>) zW?J3ET{HLVZqf!gvrD(02AEboaUqc3PcNXz$IWXT+99uD+-^svevxtW2)~oZ&E0vP zEDsup!$0OAxsArn%N;6%=d}*CnullaocW?I(Ahcj@4Br!dBwwrw91EvX-^)eqKAp% zhivA{*p%k~MJ*Vm4h)C{?3o}I{3WmHn9GuEZL^O{+hfaP;uCY_ZIW~LU(<`hFgXMJ zM-tD1w1~Qr1Q8y7vA6{Rh>JRx4o5CUj|4O+wb4V*lrs{=ET0#@KiB?(Pg?@inZ!PQ zRNbd})N8__qw>Qk((Dx^Fu&9-2id6Yl6DEo<>&+ERdGxExs+{iF< zx;lvB{P)sr4bY<}p+}EFPYvmVfC1Q(^zbXZDv(x74d+~r+CdkAYtuPRAaDlh8e}u4 z)E9!z|Ks_`pf7!g$8ejznPsz*Ut5tfNPwksQLH)OmVeCNb(9#5B@Q`9JY()l;u$xM z-;3V15I7AFY`Uj$sjLX5pGuA!PPKNV0> zDG1BoJUqT|GSAO*kFPQh!YdO{1~o-9VfrWB`dGt**^CM>HZ!z&$_~N|C>S-sXFym@ z5jfK9uQ2TAr9aKuUXXX;BM2;w6i7yIq!JN*O+G&plm`3VX{X9YoK7vO52kC=HfV%4 zm(AtKN&u7gyEYW~Q`5(@Qx~84LuaSXIdg1h>Zp|6o*WqJLY*meVZTvLy5MXsMylYS zfE!#-eO2TYe=7IRG)=7 zeX2V}$yxlK?`UjzZ1v| zk9r{EQXfYN8IqJ2az>1rKP%CW;N{Xmbb2tG%zD#7M3AFOE$~pc!;mMXF-yb+YeQofat=Tn!uBnyqZSzBoC~&Mqsg;tkSuIpFzr}f+irwW?aZN57}cbir(jEb7jTFO9q7@Hs=@uy_S%|q zf0?%^A8Chovo=g6b3OiK&WR_CXDL~bQB05mwKk5PTQhLmj z2dzrDFm`?s%d!KRZP0OT(m@Y%z>wjv@?u6Vq}@>A3Y0C&b&FZdp0&fNX5hfi#x=Jk zD|6HgoQevnR#8z^figu@g{RP8=!d0->OCxG16;IlQA%Op0Ozu3Tpv)&R?K|m6gfI_ z7iaU+@+~j2!$-@GvKw@b={Y+JOODTfaQq%_w-G`2t6||u*bcMoHrNicfg^vv?J%NR zTeIAwn-1kEtgyNr${egkGakO4TsLAou#FrCPyUNKrhw2F>jwBRFnBkMvtfG) zpkPL3202RpQNw2H;Ph!$&U=-io{>IO@$JQ|o~Z*LPcwloqU%6DfUcuG*o|N%y#opl zt0(@fB2gXzk!Sn`BRk%9ppp7M*j4g4q&JFP#_^H znhr-PRZgUm{zfjm?b6MRkI(3L-LG#u@%^P7R}LSZ`v&|bM{7-V zA6XSYjNU0ds_sm>%>J0!m8I&0%0NT#F40KIy@ZHF8mj7i(&-GaOdE6)g3h2rfO&w< zBv-5|2rqsfcRQdh_X0&SO)d^0pww$T5ysx!WsnsW@*{EA{l*KsxYkHL{At zd1bwMUy}dV=Ak0WS5;Mb9LN&r>OS%B9V)skG7nYecae^sSPug4Cwu=+0a{H52q7jo zfxAa{PXGS*J9o}Z?z&%h6b7Y<1ZbfV0Egr85$ww*#O}TXc~%enFn!Zz;QO)}`%>@L zYh|nT5I#NV9`q7>Nrpo_+oRLvI^+lBZv`?k0{)x0`<|qy-IgU;US-R& zBrlQ|d65^{mY2x;5+_b#C&Y0Q$U-p0gtb6jAn7u+bcL2MlqoHp(gJCrbh-fj0c~lA zZj=IrPFp&ZPs_9&ejR8#A%6LP&wWp_Y{yBM-|znmSdwKuy?f8S_pIM@PQ@F0-*{v1 zNA5GDypP#*;!93Ra>CZ=ho1aAw^c(+ghqz_fPNPI+~S~|!?x$<+%l-(%(lHI2Apbg z&JwxF{#EzL+sPj*WN^Lyy2{&!l~bhWE0}X?AF-FYjdwQN9%oezsh^*1uMI^#kQw8- zSXcA*N4pZbOWuC=Vm_)r{2}9XE{_X##ap1)qbj`+Qz6?vv~<5pw{N8AmVy3hpsGGb%&9Lq0$OTqpv%oLNOb03UM#W$4qVXX9EZAprP_G7- zKMr*hUB=KW`yE|3ohTm#YL(w=w=_rV{Al%vf4&?iMH`7M4N)&T zqfmF0%x5+A+h0~A`OED1$#|y9|F}?c@)iB0H&|a*0ss?9$$(48b-_Y8M2`_!Ap}bj0&Gkl;puPB_jf( zWBG7W>7)@)E|gAb2vJPrGYzWr4a3StQS1e@pMxuF0hBZ$fGaVoDvN%z(-bZcb{kQ9 zO(%e;HS}?mYg(kw;{%IlzW^BP!h*s6-tNwh_SU9`+DK)n+>b`AqlKdugbvf13Tz(z z3KdDuH!tt}1I+dMn|$f4I@GE6r1tYbTc930+wCqc^kK)Z-sO0=zI$CwtAQwv-Q3=v zN%|GrThm);A76xuhL#*=)@7k|kCnR46EBH@V6X?xI;&*HK1R|2)k)2^qina%dH|It zk;{puO8kW+>_$CA2TVyKc}x0Ao!N?geBPI$zdRFm@7gi5W%I-saTBT&-P~ALQw3wq zTZD$e+0fW7Vi!>qHvoc%R0~w~>1ERV9WWX&o>R}4*P-;N(_|HO`Mj0ZfzK59)9mK| zl=aYYs{dVHd9X;b*d$#^0T0|fV-1B0GnXgx^@^p+`~$s)wy0FW|3J0Rcug|$IDplk zu_l>U%~+!VLdsr8(tkzu(hFpCIErMs$brt~MO5(XFmv;QWDwz2(`(`zJQ0z-{TA-^ z5bnkO97TCN=|a2ZT0aL$_wifmV=HgDvL@Qnf2|!rhuFSs>nf$n-`0%h9_5u~qv(yL z9?*e$e!YpFU*6@OUnp8Z>;MWYxB&r#1p)|zDa-*91E#=xexau#Fg1(Vu&X|dYz_6p zpe8=cdw!vkAIhmBFk=RY03AuG=U0fIcYyhkMX4QlB)32lHyHL-wp5mf3QIy$Q^~5< zN`I)JG$`gMwa(-(sm)igG#>AspjQ=M{e%y=Q^eE%jQpTF;GX*+*Z)LNT3J=#t(dAvR)^zgZC&0vV*c{4}GOMcUB%Pc0>gYgDiX3W$B{5pQK_nTHgrXb{SkmsBSRm|)w$RISTfNGcas zs1SB9Zw#pXF#FHYul>I>ddobS3~HdQq9T_025AHYM5d@3Q??4PAuH}7)O5b-|B*On zLQ04i=2PQnGb9%E6(Di0?#|3JoDv)>HPhLY{5=n;{2EREoU->eZ7{KqHS` zOhj(9CVN|iY;*Qe!Dtpt#)TZ_V2)!b01q4{!15-uZ~(%Vv>wwme_>*~InK_6%`|+% z1J2NzzTRvU7T?hqBoe7>Mc}j+!JAB?U|RVds8%knKL%8$EyH=46Xs@jPH)|`adfzU zLw6TxQ?xzlA2$nP&X8TVLgim!0<5!NmrV!}S)bUtf6mc1Xf$OX)CaLzB)reEor7?N zm$);xv#Rr|TRYcVTwe5f+TJYaVtM8Myvz-q`FhQ6&-`OSAYlF(-}s`^D%^)YLw`r8 zHAg`85=6gJ0RjQI0VunaXra@m%1(hn0PDh=gXUx*$591MI4GFS&}9{3Mk|md6S7HZ zeR}h8_MlRDl^L}}ZI|5nRvzJm1VE%l{a`T8sKV2fUCV3xi#I1&SvP_ z0z8JmXi{jqtVBj{T-^v_02HFb+LJGWB&klE1vU}|&rsVs;44K?TTkGsHE3GOZUc%o zg{budzQ9WZWfm2PDLbgw|u8b=13da6SU{hMYdXSQB6GnVKas;|0q!}!io>E_Q` z{3}4T=Bln}MXROMb$E3066*u^TMljO*wEh+>9v-)_9e&mTWHvbeE=H5=Lvtm&kL!E z*rSeu9+(=skT9C2nv4i@4cZfG$fW%Iqj1>iJM?4tqGqAF%vD^&1IV7$VUHEH5;(Sa zIWcgV2!Flg!-_rS;6=RS_=heW*sRTNrhm&!Uz0ZXotqy1qM_@TsZfX6_)zDDw&iJk z=5tvW4n(`slb<-4bpj)V9{)7f^f#Dmf;>r>t0Y#30_8lgfhJ5{LHLHrC5&YirHol3 z%A@Ph(JT`}qM;tMG@L1_v38cL5Y(#{UHRJD36GAaCftR}hZ#6(V{pW;O@GJKn(1e* z>SsP@UbPQgxoKLAU#!wXqJB-{Zp5OasAS(K>_|-8xts=25cS$6X4bQPRJM%wDM!|I zwlzyKn~{iM7X_hJ80&~PHZUR48Sm@p!-}>ywxh8EY(UDk&G+QGOo$csoR)FHGWF{X6ot11RvukBBezg5~VHCh^N(?z?7VqP73ho%h|>)Y;w0 zepp(5>CT5A=C3D?ochE<>nfzXLr=uS{H?%V05#*vz6#JrscO&yv|bs|hKU`dPGx z)$3bE^PC<>t{!X%Suk#xd!W$H3#K-Y3?m{od%=Y}FWk0e^Ny(<M47KWT7bn^mF=5;MbQSg`gm%>*EvZK^{Au25L zQtxOU;4ku{jwEz#yHzM)1=JyrZ$}i3Tth0KBb3^0gpyDmPi?mQ?{DhtYL*6<7JqQr zW!PXSefIKYv)aDea?xZ*Q)4G~(uwQtD)(felq5V^`ejd6hS`_;F#Oc`EXwXOAs1~> z>{hgC)(QD6pX#1bSwXm|M9TfwtX8oB#Q>=MCR^|Z3-TP^?r4SfqW)QTk=yU}Mxv=_ z8aMul+pv#$ULHclkeSG9@ohsXf!e8u$z(Pi!(yV8Bq^g9S%GR1`=+jWBLPBO&0Se zcFwE4!CFVTBB8htWn&Q+MMZ3b;9@SLK`l|J+MMXs!huq#CKXy?S^oFLg%?h2nAVHU zn=U*w6=#O&u5e>x7(bSMTOZ%LZ`;6a%3I3s?wyz$i#I;r80QZ(ZVBVwo*s9QcH+54 zjtmc^ZD3&z2z-koz=n(+l*^IF{eGH*Kj=r#uhlcLoZn240_t9WHt%J>Im?88PnF1N zo#UuoR|KDrg`@kSFTt{vbjNVkmn)+$=k8{%w6lq9N*M4?zZ8G;f!PP1y7%_k+fkel zQKQBQvX$Uc1!&VjB zDp<@(&IS~u!f#a4iM9rn!J?uWB+GNz@nXNf7(Z`o&y(9exN+kLw@u%&apNu1Ls$0o zT{*-brTVgBuix)2E=xTxvGnI~O*HjE`gnT2;$7gRCFoBSBgp{jJ;g)i+U=E>F5-hbMieyL+_c4s!z(lcO2~NJJ=x_)#v$t zGHiO^=FRWhWca7$SM2%Z;iLh=4o~!I=_Fo(KVgj9p%bfn%ujY7Q5Mz_QI-S-Dv@QF zo@!1_Dc@D4{!)LLFO$lq^Pn=7u3eYDKN9QKC#>=}mH(jDkgy-|=peYOECB5b89>$x z1#e*Jkflc1`wW?oAoH3WBM_M+HY{ZcWmuJ8P+g;!hliC9YPF3ZBcnJ-L5slgV7nAPbm?Dr3MwsGhv) z<8&D$$tR&WUCilo=H@=CF_8bIJ^$ca?9E5xI;ZVI<&Ot!jef*v(?^!l4I8)OJjiX$`3B2j=DAeea+ zCB?E$5YGB0zeM?<4U*q2Nq(o(&zym8y%K%p(ej9H;WG;h^Yh|Md-pB{=b4Yapj5IK zK>XF?AoPu$>ROS&7LU@M*-;DvIZ?PVcsz0&QN)ZY2p9y6f+TcWMbJj!kif-C{zkr)EF1ZqFQ*@}&h zvJdHoGtf-YaRz0D1Y5QZ03bcZtR#nwEd!zD4EQ7*K!{`7!_mjCc~rTMx%b&}^jM*n znLoG4EHCG3eZhxA=rYgwG$u@WkD)|4#5R_~APi-kUT{X7UZat8?tEg60h6eYg*fq1 zIqe4kWHu{5`2FvHUk9dvUr?Fs_rL$cAC~?Lgoj=ohptd0p@*>H2GXmM|A3VMZ=1mR zFyef8sE%ze(ng>sVoQ(`L;4F(gpi0MdjyZ$jj$NUa-w!t0}5mjNEpJOUK3w-&K!u2 z^+tQgq6cQ?zi+%^C-})X`)7>eBTGZZnSQoG`S#8$j8rfU<}t>w4I{}H${@=(Cg55D zR0@E1W=Gu52$^sIHEICYlT6|Pa5CDI#Lx&_g-z*)&-8@1+9%6%oHba`)ZzNC!v2nWA~n%Ef>w(FjKlG(lSHrCeL+0|c$- z1x*`x>gnkj3_{#CAsUzdKJhs^G_Vs;5uNM^yHK5|fHaZ-f5Y1ZkwC@@ED17$aePIl zR>(&V2PmKqH4vB*i$h?_1{V4L@zePB)5pzUxOW0;@y&^QG5!gR-y=qVUab?5i$K0q zVoE>~h`L2|G@!n_i?|u~BeX1?!29Y52GkRU#ej$z(h;^IZf2TA6J(=lJQ5LwNL>Vt zpsN4|DD@T>=DD3Y*)}08hDp8BuW|qZ!{~?V2rG+sZcrUQR2`ivQR^LB9(o^gkF0UG^ zs|eJU6eI@kA~NC(Od&r(!hS;j0k)rPI6@}@?-L3oi2n@Y`)OMWG0+6RBTvOaToUhS zoRJg&YR0zO_q^OP#EQfVut{`LAr*_<3@AkCqgrz6^0H44Zhab>b?JN2>FV{VBx{{i zt|$vl@|A{;D_oCip)ZRMa-H5U)V>3qK2Wae^wpK+wSn4_;yi!8KU3_UL#I!R;1yc! z8eLxNWzj?@1V3VJN_S5^EBB<@T=0{ko)ZZ+{C1f8Pw;sao~K@=st{`x2u-g#cYC{ukdeQQ`5-w^gPpD zv3vIwCr?I{FGT>qt7n^cjEzybK=ecSBj=TL{=s30CDK`kYCxW9;s(Sc)?o%NN4CLG zgmT&r=m{~m#tqo?%vE>Zc@>xe+i>^OPv6bC0lyda{$E8O{0db?K*geq2Lz`mz02Uwk$8x|qOQ4y7f-z1m(C&+ft zYS@DlKzaCTTi8Vx~bQRdPsI1rl6DI(t+knm}wMk~cRyS2-SJA0QC4tKcttZsMkpVy(DwV&HIh_u+y5!#zERA7XEY zZkm|5Q5(E2*}k+C->~$Hp2Pk8@AP;=X-{s5ub7Vvly8YJk{BdF4q$1d|ZB&3C=Y_!x+VVPa?n4-_JU#9%xQoBprAF_q9e@5t5?ShH=aS*_qn=gc zH3Ea_AWn>Gx=6G%>W$|(Pj;I&tkij89-LSw>Fz#<%y;Wa#d;E|dk%Z^4Lj!kd-n zQag6(7wqQ0q<#TuSMWVA<&L`$GKadK0~wd;;R3Hvb?WqC+l4JFGGr8ci}UlWJci4+ zt8tkrQrTx2WtC$nTpVhm9$F3X?>YSHuzx6tW_@AquJ3(+Hn$i}BXvJaov@|p$9q?`^J5GMfQF4CNID_JYq*2G$&3X>&I zXUdQoB*sLC2`r6B?U%s49=7D@^iJ~&)MN7H&X@V1-Y9BwINA%v!T$KEKZv)Ik+72Q zk_D1Q^g~jA`j8X5#Ubb1cHbIqOM3u8N9^Ji$K9N<_6%nR<4Ow`weh7UWTZvqZ_K2~ zcPj=)jTu+*P0Da?PoR!G@>F3*sK*51i8IO_8cU9YcHj!~ATQ7%1srNx1vZ~vUAe23 zo7r`g6j_FooRWFc;&mt4mCF3Ll<9ApP({qH^s{SDVp!1ewX*}=f16MNhdAY-8^0`w zCn~6bLZ&J&U4YdRRg!e=B4#T+SdV{3Qly{~8HEo3f~Y z)|>jt^C_jc5;#9eDK{1#RZUSt1TGYavj)@+R44alcBRc!VBUK0&N~lowdKpU%Iwq> zBkV?rndE%i{L@cglAUibi4uFy|1We$W*O5%zn~;)$=4K6hel*qZsvsFmqB;s{U@(i zy1&IfuhD()4J-MT8hI=f_9gaaG59tlJzUn|Ls9#?(Vz!*1qpP3=;b@Lq98m zhWz>A6IOVy+N{hdn`Pqy%>?^_HU(srdhQ@bQE1nOsEipEchjs3<}&FCsL`!mtkU6~ zU5%s?=;lx~0%gTj-b!PU5#U>tzj@W8xD+mn#=_zTZQAz)HIZL zOQOk;pMA;GPtW6^fp20g8S6|sGduv4!N+xGjd?*5k=Bf-wAw5~)df#5$tJ|tu#DjF zMb4oD)D5Kh?_icXdmV1ome7(%!*D*1cd1cmcs?co;U+~$Wmc5P?hI=4QnT8kxZ5dlLy)w$%tY18^E3 z3naIM1y0!*%St8$p{8W6P}tI>a)Mq?u$z=;8N%?tURJCxzs%nJh1m&p&GMppLTtIfwjHA6i*Rxg2fBei3}R&(SX4ia zysQuos7^~|G**)nT*S%yHKvZr^VVIK^nUus+whOpllwd1vjei3I+~HIE(K!bMjSO; zkhoRH8_%Lw7;jo)g1#o>A~Y?*?qU2!+@)c}UB1H->;byL()}hjuFRe6UmJ;4oktQBzAO$jd2lmf$)^z%TPoSE@Rvc0vgG zJ?_|wc>m7J=H}*SH+7CdhF{s*(b3x4(e<#HH+#t?v;Q(+?MFWLuZoK z06t3);Sl#(0>KIREt0te;JdYiYf&ozhr!{)JmkWI9q93i3z2BYohdTHE%2GBKFlVM z+~jv48qQr8Yy)>;lJi3OCat+j?^n6c&P{a-Kp*HC5*C}*Sa$=UqXfS=L-;Nyhx~Ur z?i`pAncbJk4**vksk#lSBbjpS$lPbk9hh%HylkE&fcEs2R#%t$s?dZPuJ^C2zNOXJ z01Eb~rSHx>^HiXTX6|uf=C~o;N7C@^_dW%5z@snUKF>a!0tXO@h5Jr_ggx-ieFkKK z5Yp*BrhE48yHnd26uTZ>a8v$ApMZE-5n+`^PvE=Tf#Y7|@xBUlbEuXZw;(qZK@xEH^eiYGIn8-61i(}`?+pDEy!rNE9H+h2KkcfN|%9!|&i&3AejD8TV8;qOO2DH-Acd z9dzX)oiD3%&|+j?SE_8zJhvCWnT^U#><*g?zd@Hx3MY|M`5<&zgdm_Y!H<3VUaAKs z{>$lmwVpg{<2{s^AqmiXdF@dLe4UlV8_9ar@g_HLa8mCTaxTem(QwcRo&Xww4-}x0 z5-;%%B(7AJROWkfik-#8K`>zpVi+W4=#*f3TK|+-hLxD0g zncSkvm)P{!jvZsl=U-dnv&9rJ2mFJd%*1`kCFHS%vsR}%YnQPv@H~;9K!?iSC=siL zqw`AgcsHDt0ckbjw`|iwJ5SrUPV(Qwfx8CP5;zU2i|j-O(}_!Gf#VyPgP8rag)sMTPX8s2&@4|08kC`}PAigbPC z>pZ%Z5#u6yi%Xc1h5IE5+=Mlt;4x(9Dlx{r;N0n~l|hH=ncLeiSP^~_mxgCR9(2A^ zWyCh|RW+`~Ls~ekqI3*&fno#o2G3N#84VEmx2kb1=E0P(BKQx5nf)yCJuR*!zKXz> z@=b__dufYE!Z!G_1$@5^c#~RTTVkph=h;sw1Qg(e$*!V=Ak?6vgH7lq1NJ@qEc$7V zCneYr+M<@0P&e}GO5mDnOKS`ATsd}cmKWhKM0sfI=&=T=-46sq)XRZ_>M3xOm$In| zCtrmUqJ3X@_rk(%{M=FCM>2LrS9@E+eyF|6-l+bbFfSZr%?B424k+J0xWIl^94L;w ztFyPa^Oj!ahThIQdg+b#clM_6Um18P;J+|0j)#JZV=({{NdBPz2S_Mi#)(-M&GJ%S zX=xsQl&7^9>tkDf%0EaS)_!O`frTQxI_(!1e9X`*Xtp>>*o0uv@GAq}2)#!|IZ{P` zoiI)a!Gb(@QEriHiwkx&G}llDJh~Gs%j3k6@dp~5O@6mC%9}m8+BZ1=lI`o@So@er0DB{^1x1F36!iX#jhrD3K502v0F2qzbyh zpV@ehgL;}W300AO?D>aT+0viiQvM_jD3`%w|5-Xp@R(nYe(>}74#PtLLqw~cF^|*3 z0$m?|dxP?QHqi4Swos=u@z3_4esr$bg41Yt$gIO>(Iy@Z;?WTvz2M)~a^lNvUp~Pf zFU)w2dz1*<#a8i(Gha8|`m^WSo_m%*?r41$uYblLaa~A=Bj57l{5qh8QG+HV$3nPY zjDy1>FMjd97b4*o?)%~k@m~+#cH7{ue>H$d3>Ake3v1Q}Jx~VXq*$0s?1Vo>%tK_K zoV>`HwqOAAH4nRtl6G>fc26F31tImHebI|WEGpgLmN zS@?Dt9Kr@U#6o`A53g@3<)=YH_!~G@Y9Qlxvv|>o!sN$?K7N-mM-+36W!F1;#&-=J ze%IJV0yV495sCEGu8`jl9Uc~B3G5@A?4l)!G& zlTm0!wF0xeD9A>OY~(hD=3v)+Z&t(*bzqc`S7wt1A#;PlIE|3G0cHFQM#Dy5sUUR4 zk(Ag74&yH!_6@1&6WLS{#_A&j-bp5|+XYVyarY4E6JReX&hGa^#|ojrChsb~+>jL4 zXuYKQXa39iPN!$-mA3la!aP@YE?T!l1CgeN zFe(u`vRp{QYYf&SOo#Is(Q8kc@l*O3}Y@W3}TV|W37uI%IN_SjKw^urIYbC9j zTDr3m?SpR0slz!kgNv&%PF{kPwTz;qKXUt!H)pXRreU{GAq%r$4j`Y5dND)7a7&Ev zERNGt2M!>0dE#OA`p%Jw(F;y3NFO={|C=a`p1w%5U>(|pLGp#?LX9QQJa8;JcyD2Q z85m4KfZbsJi%44+g$to2N^^*ScIwA~4UUG&(W0uzggzcZJ8Pr#brSICjFCQG9Fm$& z9y!w~lU1;)-yA|&+@TM*g#AO|mg=%QHuaZPpafroG0WIEgsLmDXdP)8+B(+j^@oGL8iU8S zyQ6Ibi0A8W7~9UK)b05uOdYCMU;ZW!^RFc~O!<}jfn-qzAU*-KUBU4gEmf*e=^ z$e@M0%ckW|aD*`yjSPca>T0QC#X*rE2a6M0oU8;**+wNq(pfoBpcw==u<*#`iNxh2 zqYH_|!f1RZ8l8#r$L^L58(LcWx_t$OK3`#hue)zPerVvffkScaDO$Uw^|scjIyCmX zt#wQ7R9EATja^-h^ywSvp|Oke^bESxA#rFAr_8o$uw^!4k*OCYf~0h2ae5^}KOsy4 zFRl@)q7ku3P8b|w0jPTXva!6J39Ye)`trJR0H7jB>ninn+$v2d>xgnn*3u*ZDuAQ& z%zc$Euo^kL=OQcZvt2bIONkA!*b^HQ-4hetiH$Aw6&2BFMFpAIu+N`m_pmF(pQ9A?2XWJ0tv568mdpQs zfPEJIqn{V3-|p>VA1WGsP`Fm?)A~pE!Gh=gqv-{{kl`!U+UXK?UZyty%~WCXa-Xy;KO{^PwJd>}q6iy`ayBbAG`qLH=AH(|b!=^4D)Y3p+ zuGLI6M)a`4$OLJULov+~1GkZ7m6gOic2RQ2=zxL0u#KZLN$=im1A|-9Pq(tXENba9 zMS8Ao+M4tQwhj&zZVWJ8a!PHjTXHB+S>>#9*ZRc4e|P$3rUru{etba?2h*dCUnQZaO8D-N5~tt6K|sI6jlC*fdDY4eD_EfI=rHkO=g~8xzW}CMbw0 zBG2fryx$|`MFQL5Ed+-`pn&iasn9b4+8G>MaOD7<1#}N|@M%gznuDy92&+|AiiS66 zzlh?&MoNby9~}f7=fWDJ_NhLrSOd|`hsX+%E!=-eJbp?4;Kgy}X6sQ6xbBDtiyM(nR9|GCpqyb)&Fqa_~ zxmtv5pf?r&~MT*dol);Pi?f2y|?Dzf>cJkYwUuD|N;Hu874d zDw~^^PR(9n$aa7u>5{VxdUzv5SZbfJyWMsh(w@R?wXfB-@dxEO@g=3*-8WQRJTjcm z{$(l1zU%25EGilvE>Z^Ao}Ap1Ik~wxCv)LXBijPBAsb1xEds*U#AeO5;0+64TY!Qq zjS6_wq1fz$(P5Ogf)GZe#2IDdcxkC1lmFfl`?WJV?9-ytE5~lt~kX2@jVYXokJ&FDtrcLV|d3 zHmIXj1vRaE_~E^KAI1%E7F@y>aSV7}gHTF(9xO2gohb5z!+_n7@CweQ2rz~oXM;d7 zu*>;LU~sNc9KXlvpec^A% z$F3h3YnW+ioo;BDZXLgFVBk8I_agrC;$Qo(8*QCwYMNO>>3zGPZH-n^HcwviZIjf{-Aa%2- zhzUiJq6i8DBLbYy>n#DYB@2bDkYK*Rm5~giPUx5n5jzz}mqhpcIXdaSY>$|;q^$JU#FM{PyaaMOF<^H8BX7v-8sMpA{{Ch$4Eke6`7cPR`(P6`5L zP*7Bl5;vTTp~tl!$YRs{9e1(knO*tIXFh{#pM;$MPvJ3q(3>cvFH?KPqq!{npom?O z8#J4G#m7iq_@113R(bBRU&5o0j~>9apWxS)Cw$2h&7AiU> zBl7?S0FZQ`)0wv7$wY}ECSAJa5kFkVbW?xgDXH~!d!4HV{RS!q&hpSsV>gzj}?r3A9N_9kYjmyC#_pvYA&^PUatC#hsIo|(Qsp9Gw={Uf zdH}@WB^6{;gFsYB-4I?E-wzpsCXCw|X^dk5pad6$E64oto35SPzJJY#<1cO!4~Sqws6Yp-C`BL*Mz}eb$Bw;?sxRidf}*Uns36bds&Utx`$RxVB8P|OXKac=M|G>-$JGnz z=$Y(^1OhdYfOpF`PL9;JZdy&D?L%$8%F_INZ&PbL*=USaM7y=MEz@WfCXL6QwgiHiFja8k5axQ7#>2N|*o8Xzm?5ftq$juDwsbZDB1HaENJT%Fk=#~Tvqz8L z;VBCG1K}QNBGEk*F5i6r>Y458+-@&*c>QpI4E415+gy=2R#l&ovrWF@Aov6FJ;T-h zG6c^FpaBsM(MO3K3<1~Wd`mz!bmNq+T~z3DX6HI`)wFc<&t8+PEtR3~P%tQwMeT!W z9cT=pKmfTCJg`Al#-pZ!(>g2UVr^~w++2$47Q$(>3redE!lZ?dtUaIfByWG zqrhg%cR(1aa~Z_BvOpHWmjlUYFp-|McI3dXg1y5JL-qsDh>;EIdN{346%KC5Z#t zxBieJy-!(zsx9Hs7eU?B2q{fwQ}ghLhU4b(c~Sb z8?uGYXS!=FrR8b**0GV+Ofl0y!qhOgPAzU|g4aWhexm%7&ImqI}?Q+Wdetn<4J>dU+ZLR6mEkBI`F=fU~DKq0j3?eP?r%r4fABMDPfkp#T~U zM}a`K#yXyNNU&2?adqbz0&LsTc2qfh9x=a85P(!<1cxtFwBEhQ%>vl zX$cR7$T*IYbAs7v)>smH6>9Ufu9)xg%+=W(ORMdCxL1(6-gZ=7lTyAWzvgk=3lqC3 z9E7q-*l$*&)qEeU)S91%svISDZ!D&Si)6LKAegiW%{kE)D7E@CMNEQ3=y2Tcd}ZgWUWubT5sgmdTFz5&*#I#b=ab8?|H1C7=iK3)?1Y;G|2*wNQR!}gI6&^Ib>mfJ{3QLJ_79m z?3P;0K*@sF2|+$&%1e@d)@(3gAL_>|D$2@8+p36EM9M;CA&OsQ1Kz7@U4-o6!J1V3 z3QGm83)w_z(On}t#v%&=Ubt4O<$?+KfLRf#6_L@rIY%OyS{V8{hcGp*w4NRIc_;_Pghw zXZ6ZkKm6hI&npk#M1i`Sf`<>&K|H-(=oW7k@4?~A6)F=!$fguS5UubtkUGIDRtQ&O zZHD z%$l3S%I@meZFk<;d;EC+ouBIa)N$PZ`?&wo8SigebANc7wfj?alL8VLMu6}#+8C;; zo13fI-K;_RsnS1i{F56#b?3kx7)5uin%xyf84u;T+xm~=&Uf}7Ki+$%Aec^n0r!6p z_ouGt9w2Fa(7!9O=j(;N2{^$~MI1OhK%o$!ltgMgU=XeQsN%5!SyhIdeF**vj;#XF zGrLgl&LK>o_!kdSR#$P48aa$hHEbk#O*nZ`g*%JcT_9w7A}TTc49GhVq3>{L-3@-c zLQx$44u2O(W3JkGbG6t90*9AAedy394;^CH9(rWxw}%eB#(%>b$}#quQqUc*WuFKW z)#s|4*OpkcUpRoQ|cLDxh!Vy`h#t|^|GP2LHQz0_b2t@M#N@_e3RZ`NC zNF!WkJ_+m=mDVIMU3TDG_x$eB&ph*t_<@O~D<|-%`q1{C{w?BAFJp&wCE_{EI0)~8 zbQGmXWFw{51CNLs!p8t{Ol?%*=%zw+f((!u@GjcFY>T0Us*m7yxD_wG0dd?Q6C!i| zV;}q27W{ndAD{TqKYomTe0t)pyC$Y5Zg}-oNE;^XMLzzUsC%3%^d-7+H@FUISA&xh zxE1OUVmg9uAJtw$9y^}WiR?HcxhP_&mLQ;J#8w&vM&OUBXh}X0^4c{t&4274fAqvZ z(kL(#mgINGAZR4u27)kvIL)1)w;jH$a^NOsh55wAS>}S zsB{W6V?JWjx&jC+y*QGCNarB71hDrIr7)Et)u4fhBG1()Z?Ht(;2huk znkvDdd`MH^2w9mGqZfAlh_CP%@l)k5Vb*{5=f4wW#jN;qFYvdnEc7KVX7?$7KJ+hq zTTP;(8_J}>IB*JR7p4Si+M>^JXhQxN)FhTHLn^71r?Q}VJ3P*i@sRRTT0x!c z`PHGcjt2i3B)h7j0V{_4Q#{Ops3E+-R!kdZIgx}lLMKo-YQ`IY4su}kQG`l(C_#_r z4Fve6h=JZXo`cX@jyH$9{ZxL2uX8wZaWV*&urdb1^^X)rFp?4Ycy9(ekcT@0^%>y~ z8uBp>A&4PojN2phCK7zyOdB)AHjNp65Z%!XqN#N#_+ULbob77EL{{3FhDq_cjPZR* zjmjYovu0rAlB6*qW=u7|2>X*v^sX2ap#RW(kHQaZ&>zikJF0Mh0{fSi6cu=K3E86! z%Y24mL3vHp_t5ejGKa{@Hf-quKBB&73;EC14E5qo{f|E$jz+_eKi*FzE9hKc%!kCE zLoCgR$mZsB>?X zxa4d{)LmU)zwO8?{(N>_zTcm(+??3h{xw0d>&QmlT?wDV{g0qnDpmAO!LRX@_K3WLK7*>Vjav4%^`F2v|Q&J{aMF* z_Hkk)Z=HUd?STiB8{glS8>+kF zx+|t?@lY4aZT-NFHMKQ*Z*>TOqFR;z`XTuVkA?mfyvwuBhk9`QtE0 zq|YvrpM+quP(|Pn!DJD%H&Q)RWH3?Ca326V77L1qBWKJ4UoG5~@Ed$-(5s!cbz1{Ktj(z~++av8O z319;Q0$k(#39!OHYgmC$JRgF5Gwrd38;RVt_&Fzaj-(y9^<$AXIB$D;;J{UBg}u_o zY1IsD@!H)&Fuu-BIOk|V63>E?>+E)0xfUU`dY5PfxsW{J|}gqVdGu3HJw*HSnSUAf*kz(RBPwwqQst6 zy9X*C_KQJYq;8{T1ZUE;ArK7TO)#S?XAz$g4!))gy-Wl_b()&z3YaIe`buG0ju5Kmq-q9}W2zQXUs#(Q*-tjJ5 zWn-Qfc`{79^IP;&`G%VHN^Ig^aXlgaq_LnuK! ze`03;RpP6M=jfeiJURB*Fkqa04^C0XxEA-kt(rN*X<|6}yoe3BjD*Hl} z(gD_s8O&jbT_Uwx$`kT(%W^^n6ZwTCZqJYogzB*5&w*VR^EHX$e>Fxnw0HE+NLTN< z@T%4g-%%bGU)rRcicMF=`v*Js9Jze1>GFXeO@c3_bY2Hq08b~j02M02!)7$;O-B79 z4097fbHr70J;OFuAvu))QeP8BPH-WRP&pmWIAU=|D96Zt(KgB|llYy;_@~nr^Ht}e z;;{E*TF}b7&aUYYZ_dI4#c`~N0-(w#Xn+b2FpD_gP{-g9Fh~X@U}8uXO$hIMH!H zp;c%A#u39fHenouXYWaLQMqyKRqFDCFd_W9C~gDWs%4a_vkRzm!<+k1YoE1EeN5P_ zK6OK^ZR+D+ZtBB4D;xT-_~mLppY?nB(B74V3r-&qbEJK|wq*@PGZF1VVIPap!vwR0 z5MUUdsqL&e)~u8W^HFvQ|Hpty=`%n9K{-J!@MlwR}cY0Zdf8-nqx7- z)lyR6#9|PZnjCIST!g<7|C5(m;>M?hynLKoM4fPDp~sc4SKFa+#-O%$qj-7<<-w_v zv+S0rS~!j`b4=~sI~AY0NScuvBauctT{LyPAslYFXwjHP=ExUm1+TGP>4JJq?#SGJ{VTU~os-R6!>C625q z<#fx?P)i{0_s0)!EiRtw++5$4sK;lDO7MwjM@ehmRAQvHb;KVJ2IJ&+kc0uOQMPmu zR;UV?YO2dy;LHY`G#V;2VXD<-)R0g>VJ*(f5EVH}SHWc_NrXE_WfE7BE44r&0Q)=+ zxW0&a@{Q&JhYz{A8%$d$)XzeJPq>gmA?Dn9u{hJ-TsXXO&*u21y=9Mt+(7{Jg}v{6KuoiIW#!2-+1#)#{HAE?egDkp8284L#CU=M0Eif%Y0pB7o4`iL(rBzEha%5)b&KV|;m9}(twxD#< z?d-GB`JI)#YEpc7YHC;!4>Wg-B|E*H$-aa)(f1SHds1*hR+=Hlt4XVYwSZczx<$la ztjuObT^jU6pjwkKxkXfxvk^d&Fd~rTkm&%`&A~4UdkjLovLaZXitKr)7CSmK!&s%d zR@JPUW=H-nJ}HV0sbQ0p%uj`{R4gc^WH#!*f)xB8Fcg^aA2vL?4d70^9v6VoLQ?Uy&krIFF04{|NND2%ZCc3-bQ~ z|AJj4n+&L}2n}Hlrxi0oHozQMXqS16DxwtB7t}|pX=3Ew$5-Y0k7#@RCiN(HaoYly}qSynk~#tA8KG9 zN|xqS=b?Oc{nYH(Kv`3vG5OJC(wZH~3q-r>rw(izXs2{N%$1Bl!m;NGK8ovLso*!k zdLeX3eaDgI1W=n<-G`%SWoK+PLMZr33iC*N(RK_O>29J~1`Hh8lKsc3jXfHF2DMxUaRfkH!Xz0AurD z9jZv;VmMTkOv_D2fDh~v1B8Pwot;kjZ$v=VA>yhkz*33W9y1?aCmhcqjF0=+>3nEA zhk&RUCm%9&#^JoEU%!9Y?sXm6z2|Z^+Y@U`RW?T zdoP@h##@{G4Rs|GuH5RuCOhR9P)vy87#^WH(SWbz(SN>;!M?MpX9ac?n%r=OfjD2Q zMCv2DRyZ77IQa> z`lMF);*08Uia+_}-Cy7JA*)>!v#cMw;Olok`Q(!`R}T*lv-c>^<@=3B_@u+iyYa@= zSL543mu<+ovVbl=T2B3D@3#G9CI5o#P51=sljQWY>Z!oVzdZS@JhoIsfi z#Sf{sjjGwF7-u4tW||q>3Ax?V(%#?S-qM4YJI5Y=z;z_Cc0^ZxEI5vF33<-rsOTcrBC@lubL)~j2jo=VQ*I3eFCG@MImWsSz9gm)( z+Gmt5L24Q}IIck@)WvgNgudlPMc7CsML=hevQ_TSDfgQJbairO?S*2I>CQ^|z;9p#j6;WP3mFxt&b54X7`^2Z19A#o*8Fz!_2Qv|m8gURVXZ zw42&V4rbysmPCh=NQhu=R69+q?J+^oC|Ek06mc0;m0a~+OBwI)1$NzsA9kts;t#1e zPNWKHrJj{!>J@Db;jYu)XWtbsz<#e4j9(20Y=Rht;@qs+q%rVkSS9E@K)N$U7;-&s z^cf)a6LNu68YrRv+*sLCTia6EIMLQNk@30$z2K|qCl;~fL~a;%4c`#Su|(`8CHB>C zuxRh$ii*R%F7*jxlkgLTK64z=06>=o^Vg^hEA~RPy2-&{D|j3-8ukET6jpxr_Vdbb zfBy5RctH72=2*I&Iqzo2p8U>JPw@QkmxMRi4eX=HAHN0Pd6McBvlAyN8=Ul}y|@Rh zQx@Ngs49l8B1g6rI+41aJnrm5N1@tk z6o^_272RFAfHCYPma%a3`$Ty-T#lb(Z0FM7mtV6FR|n{~K(!fxrXSD?dLyaoGsjQ= zm3$#Iwyq@@PfNI{r$q-DmS1uF~kj!4zLb4z-kz2HI{qk5UkEbMSxRmfnR)P zwy@sSfOnUc5{D@Dm-@?mtM~-L@Xp33NGn>0S166(7tgQd7hf3}d2wWfWDn2xh3v5i z;Y20(A{^C$vBOy3O3%VN?M*$T&Zz&nE4pW%o#}pfQi`8?ihe+0)H?tGY+5=6N}sXb zfttC63I)n@`ihe;(wZ==g&@YoDtI)UiEPxOwcr#YJS$x3X81g_BU6MHAM62WO@KDV zc;7{_z359gdgTU#+<)~b`-ySSXAaEF9Qe#0ks<8ik98!mS0d)es=>v$FS8^;7?gV8xBaste6I6yKK#{<)O}t)$ zB#e7pBALj0K@vYFN=ax33GM|o<$ov{JsC_j^L0mQC{$V+4E{uW{;~GV z{uV01+odI;|6KEeb_LP74RkIP)GlODP(fua?}~O82S;%q6w$Q4s-&Y`{Pg|z#dh41Bno;Pqyyq{BY-<)(O1no$%s4($zmF zJi;DjzXjFKZ!Ed!Qj}#!&WAzpFpoBt%%voYM_tZ>=B}>hd+sSIf9J)d>am;e=?{e~%L34PH<5FZ%e-)j+&s|YN4>YJ&2GN~5JEzh93qEESzHxOCV0Gy{T z<2H0F`k7)6D3j_BJH-VDmmP$ayBzkM4_Zu#0JJNZ8~pGls<{OJsR%s=To!j-*;6zd zX#MPx?2=;S*SV=l4TUOzP)>CeO%>^aqr%UK6KLTJ;s?8dLwl zw^5JyC%8+6aDBo|tw>;T$ik3X!#RYHkX8kIb(L0y(ks*CA=(XwEFn#Rm!DgdexdT~uZoTA z*NFe3rz6fWLDyF!|3C=%pp~1e`M2^SRP~!KOeswI_Ki=Bd(NSFytm+%>?;me#>}f$jqKfv_ z=@am+|Fc+*6`<;TbSR-|2olaDR*RaD3A^~R;ln__!>2)7M?|hay0Qwg3urUI zEU_~?6)B3PxUUCtX(jOG09EMmH^}S;{Pq~#5;%%k27^aLmGz;x8^1Unw*kC@h{uvW@aPvTgRoRPQ|y- zF%%-2zM3mQn?htT)z(*|9QmcFh6klQtA(I|)pz;IiE>U<%MS8>9I#HI5R$2sl+tR1^50P>TZ~Ym323$%NCo%2N*Jl z@K>|X&(9}iqmY7xd(3T%L}r!eq^HKWPHZ{Fu}@fJ&4wil*7#Kms=kq~D+ge|20VJ$ zui~~cw~Yb+uGuN;zHqOl#A9`j4P%5Hg>%7qyBwbuWPm zL&Xr#4g*hOsvib9O!1HNJ!dMdRZU^>C)qRKc>M8gk3aqmF_?U1;>jl`UZEo_bVFWm zf-K3$-u2kb+)kr%5AY6!LSXCq%wL%OS%=ST_BozqLred>{JMM7ZB~dZm*v(izZrWq z^OrUE$hN`pg7ZNOjo1P$v`3%$3;LHl!iJvBF5~x6{*w7y_p4*S*>bDJrPDdAw{6P& z1^01*Z;okv6CP7g4+{sr3C)c6ECJ(0LIE#zEzUvgiQt<^0^)oV%+_9B=Ag7E#6zIa zr$vM(6{Una?P528SE8<2B{=5=8qZ`+V3*C66|qWRSq9T2|0m%}UIKQ#MCeTme{d=1C?0@#^t1GU)8Vbaphc<2;5_6XRJhyM(+~wB!3T%W8K;0p)EIF??ZK~V)TSZtUBCvS4#PxDAQT(tIqdimpDq)+)q;0 zxQ&^S`*|i%Jr~6|8Y>HK3|-mRcjb`!c+N`a&!j7yBM^!4ML=U&C`}Y|jW|=;!hm@l zeCSz7^rq|{{8WqLjPe!kMG)V&1cQRk+4A>5zG0AOfzpMakJ2J}GQG2TjQ3aWYGJ49&_t`HOj#Vu2abjlG^n{?^2b=m2b zTvS|*FzF11x&0E(B94j_fmP#p=-P*NTtTNT$Fd-z3G&hxsjC=Ely+rjcavAamB@@5pY>p>)_NZjLBeZ{41mtHY5(AV5Iz?6#* zAHEpKc%^S(pbxgyGR>Ss`wW_K9aG)Kv~@Y{sr5N82E4p=tJN=_;Wt6?Kaswe%r?3a ztx59qoxqP+;kEv5!t6oGPaI&GYD7>ZA;}LC@|I98XaSa+NzyF{cANbe4!wlTP7W|e zQ1qZ0?gjbx!82pC30v_b*k-e{L}2UTNE}Fi5xPhIcEgO_%z@6i`(nL+tYdsq9&$Sot+I_m?mB>axPTIWySV9gY%5cl8@Cr)6c z{aUZ)Vj(CzlQ8?30lh%cMGTZQfs#=eLZWn;m6@UR9ERn9S~CVwzaQKR@C0ftM*?vx zo}i8anGb!eQ1wTn);1h!@908AfKu0(DM9qy_nJmDM?k_75_l20CQ3RS6;!&Rq}s!& zneuB<#d;7A&3}BHfE2%Q(muYZV#l?SO?%q*ZFqMq89VfWBZ~5Bm<=d*RMd909hvJL zoS&b()^_=paL?}M+isgwo|?RJa$d~o=-0FMfgY?WXh{98o1i24;FJ4E!t8)VDaUap z8d9OEdKk6(qkvo(O@~vKCCWdmu}-j2MB@dNHU$)8mTL72Mr^#g)HT2}YsTo=FX4m; z1uzDH6F`IQG7 z$wXrubg2)sM&*HuWCa^-yx7@02m-J+aZ-Q$M8TfkaQB{O5M-V`GwxS@$qBNC$01Sz z9!L5n)|Bi?oo;;|mu1(SENLE>8QCQs_tY|n+x}3R$9=oB)8XhWWeLTKH6b4iyg1M~ z$==QuDAOO&4)7t;I+yt?PY5O;UbZdUOSJ?^^Po<1T=kH^VpU~wAuW_ewQq>^q5wA2X zzhC$w^d=T@pqFFBTLx7E>jtA!5z~Y!; zt8YqwBLbz1^p$#11{MT!H(3|?m*N^K0CWD|It?rrL0-B#+FM&1qqQ|bKjkeVR+`1R z74atz^gxP`YTU|$*gGWyroyMekgWD@p}HKfsPVfW+YlEZL{g9&aQh2urw|%$&bhGb zYNOLaoXq93`JKgHH@z;e^Vzp_?K4|y=fS>=2McOA9}9TkeZF5d;g_}nlRW3FEaors z0y%H8GJOoHl?1-_6nfB8@9Z+5EIyhrQ(<3(^7*MG8KIp@TI%$vL}wQik0WtFX%z+m zm9?NC&RUJ4K`<@V9V@X&P8@V1det(8j>5nnQRXMYV zzofaz|LOS4e~(LaEVHs|yBa$uy7qTnTGLg-+QReHc10AS-9%n$7}P4a<`jtXm_$gHz{Lm+ z2u&NT{p!Z}DE#LKL4@~xHvXIckGwa5kE*)j$M5oHCVMj3CVOTw*&u8akT#+#dFV8la;>d$PtDtqN5jSGbw zxs|^_*w|J!A@zD3RoVyUI#6w7DFf9Or91|Crl2Y{wyJR0tqD|4iV_DQbT954hTTQ> z3ZmJlDh1Y;vDi**vshx6hOi-+=s;>wYPz=wzlruXkahrYTlEg;FjtxQL3)XuBIh2I zS3!JZ#;{dK8-Ws~Lmhua>-4fps8wa4Sl?)CZ7Z)_KjY|yq)mP0W%ux6>-u-k;~ISn zHn;B=Sle%aU(aH}kUx{EmQs~qn7QEPMqWu6Fyw!PRw7{~LM!VU8c@Kwp;r1bJOBZf zH!auAJ+Gzb-FeN6w)Y*;9QwnkiY1Ejwag#?*ph`O{>S{Zt^16>Jp5VS=4z#rv--XQ zd}{)vWIy69QVVjj;q6CGap*k}U0Ua0At2z0*oGVQiCA0`p0D^|auJFA@O{k%k7PYz z(CQoTKu0JE{V5lujBF5TR5QdTf*TS>;sS1IB$_)yiWF-!OG39jJ?u-K5Ne*f>Q~cw zg86z$bIE5(S*2=Nv!9qXfAIyI7M}a-J@ws-|4IkhTb^lY_Rs*%T+k6IUWXm%In5D` zR4VGV$XOJha3an!D6XL@rch5L{3Ox=Uw2g8K-3=Wh>MaE1&oQr6Vd*v)u;O5y$T*o z@ec{{3FjVaf8dLVdc(U#=WLCn)9`BQt$9s&ZaH=u@kYUhxjz`b^U2{~@Yt&6s%2*k z|Jl>SzcT+>*<4AipRKfnK)qZ~4+Uep+Y*@;r>MAjV6SWZUOWR{G810U)z zmSM(fJ#ZGojFrhWls!`C?WV4+AI@r?c-k$EuYc8%SytSprl^(YPUv3$$`98cwDSv$q%VNz4~bB z(Pky*LPg~17p*?UkfM-uG)Eq60HYTSo?p3(cY_l@;i3(F=6jWVs(Bq)&y-CY{yzI; z{&tuHU(ItkG^~Upea`!|9wkTF^-)fPJni{bl@5YviLP3Tk|S{4p%@I;BHC0?f9B^B zjLJc@JQhJ10}dexiUKcnnXcu9O%qyYgDGARY7Slh&>CK9z7!i9YFv22mymmI$exK zNFs$KRAE4ggYbF@Q1B0F0~vZ^9V}-4p*7|kyc*CoM?mI51?i^Qyb16rc)`H$+Qp6I zv(4wUJOjNVDv|g^9rT_@XkY(KC{N(i0Q(6Pi!PTyx)d?S1t0NT5ImQldb8Ho_(Ivp zglndz)}VfB^2r^tggkF8eH3R(-?->0QZ_yX@&KQWZM+D66zkx#aVKt&Tfq7JGm!*1 z-1obwPMWhW+l{psUtD|1<+YbzGVTho>9YL{ zunJ%%Icz&4MQu5>w&bD_WvjN`H9q5)^;n)8`~};z2o1~^_BDot8J-KjM+sk%!?roc zMr_WO5tv`!-u{L53$e~Atgr7>Lj#*ri_Ii2s!F+v1=}=h!{wu%egC-T<1V~&)LY-o zYWkpkM>{V<9gQ9BAJ7>IJ+V}JV${!5^W-%BMH%>7oVGlgW9Pe&cP!nnt>o{o%5QOM z?Jaz2InR>vBKA%+Zfns7NO}EClVnzR8X;j|?`Lx)EYsXY_Z6{Y9H6g$Mhjb_G$ZB- z>drYX^CLB>v=7N{(uh?&PsecNL4YQeAep449CHM;uFSI-Rdqe1J=l$Tik3v4iTYYs z)x{xf(c)MHNMSQNwq$Kat7QbROaQIeldZ7Xr$S>%wQ;+VBMdxq-x}s656W((lv{ez!G4J!@#NAG^cX6 z{r)5GowYV5Q5s3H>-mxVT$GCt=_PS-CF!QAWIMJ))balU_@Ri2Lq$MeEZo1zi=Xn+ zBfI+wO4CSwEFJ2?V4V`lCSV_~tjI?-h}!Ck(Uqf1f_X#phr;SX!8-6`qFfqeCsr8n z)ly0vW%&#vphWnd*{yg`QBZm~V6%A3p^xw)h z!C;%9c#tAi$!3P8CnA#@s~DP}2bWwQ>=7=C-NNxtP5>~qW6;R+2B{5+UtBm>ae{S1 zNg`$y2NBIxSzPHapg@tb)G{*h)RGvg3>B8HOy?9O763X!j1R$FIw4S*%uH{u9W(3V z1v4i!Pd>74?5u4aL#GECMjYBt2>)1A{=-AVTo<4oS#i$uS zpD(YSU&uH4nwFb?Lf-JYjzaUi900BAOv9#^zzRbLSR*9CIoC&%Q}GW!kpVjYsCHo% z#M&c5!nR@~DxXKJy)0GAg7`2fo?+y}5%9TF*+%U~>)D)o{{Q#d5*T7Bd{|GZq$k82 z06FmWQ}QR)jw3WPm3adt0kVi>sQecZ<3IFUE9%D0n)<^ZhSnalYxdZYA~b$#dsFYP z7rU?(*se_!p}5N4nt?o!z=SZ*m1op*Jsh-qEevr9J3 zo~?a-Rl5xsOCH7&PxfkruVDW}^@@K&w9x}-*6H5{;}!f;@ddhpIf91ftK%HK1UL_5 zH1IU@Q~thr0Z%pm&QIAjo1er_nmyaR$h=7TZgWPlSrq%6%!Y?5$T$fd18%82l8AaD z4;K}PE{KOE8896Y&u&=U0+N`F%uG*hsak0~;fysuUxGC8i0z2oapZ3PYP)$_QEhzu z$bcBFeW(_~B5$IvTQKi-1_eJuC)y{+4Wts$ED@g&p58Rr+a;SYfJn1N%+;$O7ngU>T`Yr&o8&w zhVct_1LojyjDh&SqEI0a1+@&qvoS{}_jfaduuN7&`r%6!nr~^dBsn;LJD={pzVk(e z^3zEPkue`7J#B|>7Xr@6`j+Vv6~~-Ab>WGfn|!Nxbc)YeoI~K0(I_A771PX?+z_oKsA*J;rhmWZmgS4H)0>g5$QVY`xle|fA-o-jaF?0$hMf49UqmnJqhs7z8V;6^y zbAi(7tiJERc-@)Th1xi83*mR$3;fGzX43eAV6Xr`d_3+=#}{oO{8gAA|04ZoDnr8ov!w zr;KT|G2A|5%G{}Q$A=mxk3qgDl_GU?VDvy-r)yl74T5X3aVTP-=0e$JaXN-MA+o-M zXn;%9&$(d%asIWrA;fu0LqiJ>{enL@!JO8dQ^*T)ataIoRWpWQIqpkVRj1@CvC|tG-E;yqeg{F;pQG1HL5{f z)CCJi(cF!Ki(>eS_6=Mb_}hFb=so!i5$S|Pj~ol2Klp|uc^GoRN)J4I0uE7udoI0n z+NGCX8M@+%(3NU0@8H$^VKEi$B6bDs?oTU#hr=gYJW7%{8#TJS+q=7Y^@`RN_#aOK ziU!sIzIg%8W@IVWq#hb=Q28yCmcYQ738|5s3RovSPKx+cmYSC4MIP6jiV|4U>%gp0 z=|$M0eEja1CAY0uahoF~rD4)i6}K{d{Un!X%BhI{Qh79DkS{Y8Nq|ucFRE8^umm&d z+JeqVC=2sCCDe_>ZYh<0fi-(U@hDfGj?UHhX| z5p0+*l(s{bCqP1cKEl+9zeTAdUTV1ywBs5$i;0f(-`8DZ$W z&tEJaKD-z|<~7y@{XsuGU|$?PthjiX^T)j2`4?SdstiyI_{vM4@`DPpGPbkro?_69 zF~|#pjD2w1AU=rXT^#E$xgpUR)opOYa|@J2)ht8hhd7n;EN)Q}hnf<0geL24_m*$* zdPk_FxY+NfGEc>2#i;b;5Bh^ef!yr$!i++4iYX2h2a?HWD4YWg7s_ck7^NB_RS`{d z-_)%9{H$}&s~tPGHZ3nTBO?_*uW2WK5ty5wm6hMnSW`7BxIR6_iVsQe0u(^siC>N4F2w(2v!>8!LJl~1r~@GE!->)vSU~H+H4~LIS!Q-7$^k`U ze#rfI@R%R9EMq4f*9aXa8Pq0BojQS6_x?x3^wckDA?4=mDdWq>Pnq2_n;1YHn_p+a zGdffs!MEjC=g_#?@W8E3!GkfRgPkUs3t^DfsYWVI9ck(g5-!*?A- z^xR!|j=O14)vUVATbAkz_yVbvd>l#UYmq22(}!s8vgk|LOF&$Z-b0!3!k6v0+_G}> z`LJqwvJ>a8+?-`!-FV~5`Efb1iRIp`&Gyy#o7;MNc1VE)#VIG)pFKUR6A?kqh^i$W<8T)859qx z_`yu_O{Yi~Fw^kT*ov)&S`W&B6zn^NjYCEV6lW=KEe`HVMF%T(F;h7|gXsgSHL|*< zcA;{#uBb>NA8YMcO)l2KVeq!DET%CizK#5=t(uJJ2*L^ZiWAQy_^d#YRTI%WGDz@e zQrnA$4eMA;zSZ;E=R=TyRw>G_I!^mldmCKvt9F%@xV#>!2Z_L7^WQ~KfE2ze7LiG1=_91zSP2#oD#&qq8g4ElL_Sy_qUcxGLR!dcY2 z|8YUmlIDAKsuo2*D(lCOucsdh^Tp3&R>0Dy&9$}7^ivne%PT6%%L|wmp%ZKudj(az zpHy&HhYpnUn80&_WT&WBjF~0C*&y*sN)=x>VM1MS*svgeUeSMzoLH{@#4W4?OQ#6A zgw7<=!7#|sKFmiyod#_inhQ9N;wmy1B%_i0XcK=MLA1ix`!Rpo9Mi9@Xg^4rR@zx` z9fd;H_JdU7UeiKexzO|rABwC!-TN_`!G2_{0BhuAB{!x~J^{K5Z$?B+k?S&5%fA+V_1;D5 zo)^RJwEbX+w_(7cj6(;s9qLAC7W%=UYT2vW5h_E@02n~w?Y6bnQy%KqK53qS6+F=Ou#R|#Bu&F#QEQd#Yeg-+}WQ(C$c&lP$K~Ioe{)}n!q1l z$loUOBv!G_OV9}E><+g^XLodvqCUGLvuQ1Ub#`0L)7H1T=>1sudUQ9|*$h3=Lc}c- z+|YVpErB3m>OlLPp*;3ZC;=Bh0d=AdI|;F|!ncqtmY8s*Zz6ufhB?e0?vLLX9A@3V zS92Ju2L&_rV)|{Ecd<7>lU~N!Q?-ZLG`Kc^{hz8dAE~LzjRw3ygAcAJW$zUnaa4Z( zQAZR^ePc=eF~=Yh9{42w_DI;qLTcaAIzrV|IC^52C_Nlbc#Y`&(c^>!9T=*Y(SYz! zRe~3Ir*YUnju26bgAqnk(`EMm07x}?!$IP% z{W6Sv0JFF$no&gi?U3QH?JOBia~MDhlO)4Q$Z)t+9DwX3R>P9vzzRe2o>S2PBLnH@ z;7XVQ-MgpVPA0H0wu@xBuGof|r%v&>Cas@7VM%>=!_rB5jCTC?A;xp&vYY?{0|Vq% zq}WktS+E2t$ANOuvzgK$uZFiBZKRUsa$-y2i--SP9UPA%!6<(cXw|oJcsVwy?Ztcf&KFiDG3phA%ue!#NZ3rfe$GtbPYHa2Mk+OSPkuW85g> zxef(RJ+X1|2;oRbNlaRo&b^7rDc*IMU(4o8(yv5*D5UpxQ;kP>JtMdS979TCLdptM zNk~rYKzK)T7xPW>lqbE1qJXKeo_*levB%~xHrEF21p@A*mwzrPZ z@KK{EpUkK^qvo{FXq(zPX?$qxn7oF(26$Bu8;atSfkLu52jN{E%mDSV^2-boEA3GX z8~Id&IBBP93KxNLa{u47v^JHF9$t}!h-$j3s_bnmS&5%}uDA4)kqapV{tfJR#I1e5IYykDfWnq-DZ_a! z@~k`+=0EIlG;aJ#Y^QoHgNH_aUrEOT|Gj>}6QZeCCp9FvH8p5s!x zz@9YMk4Y#>!2e_F(_!d`6(2@^nDF^_cTQ4HL)!7N<7-mW8gT6{t{ER|-Y_?~s3AAE z#FJvA*Uf2I6r7tnGTlh=l;oQ4ZV>okza8YOUIMqs*4$Lx{0NEC|$j$cKYN zM!Mk5sXR9D0e=nB@UdAr*%V`0ke?mMK{lC`EN|BSV=N`{O99vyNCl3vno)S@n6P?s ztPru2S5Kg;UK3YQLlljpmxMw~5E-vO0W;MIwPDU=epaTWB4}Eopaz)GLaF*C2%kXN z7KBnD(H-dXtfBxxUs4=Eeb7QUiaYDF@25g2)n(`Dz5va00Mm+*K&Kg1lD%z{pa0gs z$-Eb;FT*g2mf)7z=-&xA zVe0H;oj7!0R?da{W==3S=*!Ufx6}}p>j=0-Fs~d7k}Ho9fUWZX@@UA-+p5ZWpne>J zTe*Q>wH4-DqoZKv^-ary{O36nPd4vE6m0W~pgFT33g$w8IkY^?KtdUk#j%uV zm*GR$@wt8#pX(3$gGGVEjn6FuMTo|_BQAFy4@E&;6AswLLwhuxa5>fvdBK(;9I`bu z0r8Lsn<&hNmvFoi>v8t0)r2FRaE=d{@-@zfl+)@duNqR7;`916-ovSuoV20{m5@LN zG<7H~a}i+xDWJtuhVZxOaSWdoxcL@;yKzf)Zf=%Yk&|`fjal#`9yZf|^G$*FsBwwL z-0Yif$;vUmx!FH!`0)0?EyLRVH{Rr*X}*->yXB@VWKC0Z!$1#LfgTnj&c>P>EFsv6 zzFq8efbw%W#`m3Kr=3(RYMvsY)66+n8DLHVm@V=t%Yb&NEFYUpXic#qN$09seDL+4 zPxAJNLHN7@RU(-lf<<{Kos^lInd?nC0HH#{+cHlbjC8IHssJ1$Tb*-@5Bp_Vw3en~oaYu71-2%#)fLOjgT zDi+YrXh(+ylz4Pr)Rnlzh+Qd}qTNVOdJrll7r$;tQl6)GyI2{UmvHOx@4lnjtYV)k zUz;S-6Ox7p0rGHRfbgL7>x*#UA$l8+;*|AL=tEjV!0RFPIWGfE3{IbkS_3YZdG%<% zI<7cfTr|@>Z4S>#DT+6*!Yx!nf(#Bl_YBbmUj^hsfh2O!Tqp=!PWPsf3|E56X^<%B zG=#*+<8IE$F-P&I60+lNUh(FhnrDk#?XL91=gg1I4>gXefTKMADZquU5*R`#a8$G; zz~M79ATK_)JLUiwV<%@T$Bhi+dO+TEN<@MKON=nTF?%0Gfz)=9nZjFEzVq(! zwIA3xf>B0iWtb0*(9$NT`3z*pf8CPDQRhk2N&<^ei-QeGX%R^m-hRT|wAK3KcR06J+BEkH}h9Fn~1_<>cIK4^6~b>uWid z8!vJdJ!_BBUTau)7*l!QJTXF^11qNcejo(C{d(rD{gOjbnEQG5Z&UYHwG*7Gko6syt&(c*V=wW?c}1$yv(fB^ujx;$F}(butP4M(fZmC z7q`&0Kc`xcZ4t)y4EXIcLJ8?0%gDTqka8MbJnZK~aA3l@GcFcpPq&K_#)A*=tiWvJ z6pS)>=2E(>@UtokWq*%$gi?W|5>^rn2GUUTE|5m%Q3T&nC=Yyrsa|GnEi9xN&|l~T zUr7Q*_AWmA>|?JT=`AR#C@icfD)3IecEu^DEMHwvT#=WXk&&A_B$&8*(dDg|-{G(D zD~(V%6kWATb;^$NZg^9E8y=?Q27<3qR>qi`TrczbTh zua`JI!&KSn{*UM>qiVMFFAdQ(m@*VnRKvp1uzB&}2`uxRb zezFaKZrir4eH*#V+4*2{5*C#WC)iesdR$8}T8y8lxykwXkZduDdkkO}D#>gs zJ;Ki&Lo_*{m3OE8Y3ce)P z`A|FniXgJkLL^oKAu>V;7Q;J8VNvH#f#?=ZD6H>wtPj%Upq5RVijK1pND8e8_K=5l z`HeRox7kA7{SN?hv`;nOlGwtB5&&2PaRr!5sH@ge&;t+$Oj$bW#ke4>QAC6kP$v-9 z`hVRKiUrDGgi$4(NvQ@*5yr4S`R8We=HqU>artJ>EL=Ts|27L%fQg(8{{}zoL9n|DNiSFy2{^%xC$@AQqfTvthB4IxZ=jv>#w+?ov%D+=|vYV zJKMa5niA@6^<<y1}jAxhiLYtCMF;YCZ&;gr9C4%4%6j;3O&QpsZ& zapn6xgcc*UovS?^;nda)qEwGdvok)-C5g0R;@r8tkMa4gjQFLUJ9cy~i%E|$e|#3d zY==ls+}zW%B`y^Lw8r&nRqwulp@Ia3tC0d8V`6*@mN-S<>05T3@CYfdU2q%STAmc z4ob10!~|qIfl`o?>Zns1Zf^=R&rd(8rRAjQ1qB4$^BQNX2}Prpw;~^H`>aW=?Fw(` zv=4sE|CE!l+sXC?cE^Z#{K8+5Jbod9VSV>~{nRe`&1Wr-@7t*G-1j>0JR6?18$;ei zq{xs-m|=m8I`Gu94yO_o0}iU~f(?W=HStr+(PH}wApsdAVM{1e2LyP3&r0)Beou3=+0IW~x#FV_gXdA1 zlk?2i&DYh~$7AecF|v4?Z8BDa+#wT>YrJs&p`#izt_~O|D}-F`@D5Y)xVfPsGV6d_RqW{C4}q-H zFjK9SPb@Z$Xb+{a=M=YuGz_{x1G;qIp{4|J93^RI~LZWXAwAAWyPu>d?N{s$8D!F+5gj;K}umM z7zF(z1_{PdwZ*Iz27A?cQ_T_l<*DW=eA)yW$%IWaj=_I(X^|mv60pDoCML>EsGQRn ze0-7;gq%hv7|^L*7j5#kh{zkIC8 z+`tVy>sP=0<=Puz5uHy?NBqZdUi{}vFPQeRp)(38wWcAN4-S2JXsgo~A1KR%coI5)m2*R44DuaBxz|V-hXOd7cv>2Y zmdEYl5AWL5`^T@pUbKBXRV{CauUe(K`;kWoKG=q5h&)(F;SZjv78IB%-nM^}Yi4zs zCEVJ+k5~f{??`_0)$qg1gIVthD2Lv!r*9ojSo4{ z`ANQ_q!@scR_-;wrZV*0lQT3SE|~63Eb`@h@BUdzcabkWCAOdp=d1Mle11@`*kDFl zO1jUV{kxZZw2la0D*$r^blK4PK&HSP0bj^us+LgH0p>+Sf)&FGl6k3l8sGZM6;HH2 z@y_?r&d$|4shz$*!RK|0n2Py>-kO95Zwx6XQt?ZI!#R*IA+qKM>UEU^i>b+gJ_PF; zss+?b^!a}D(u>EPOa7`uQuy1ZKV=B{vtH2F*LO!kjt9iO-QaL-PriKs9Lbooj3gI4 z^A&TfL$uVT(;0xEBfc4MfGf8&R@$33YDIZ?n}Pxp=jU&0U)l>+y!2w50)xwTa-Ibk z@Z__6fPs%8Y(glM;=wK{&d*GSZ?C~i^1w|?%K$lmbG9vzcymZYs;@PQ+f#^%tZ~hK zlG|?>S7$hIwLAi&#vdTRb^!LmP+P;Wk%|2cAHoHFr+^k{A6c8{4(k)ijgIt*Ug+fE zGq(GVJJi}#?}1z0e~k<;eE;YL34qtu%It$n0I!-7)dAzSA`-!abq2Ap5yodeZJvHOYCM`3)qSO8z_ zgY|>QA~=xqt#=M-UgiAAyt1}~^i}I^v&y$99I)*NeSUk?^F0TBzQ_4IuRO@}0WTdf z4t!zJZ9Mop8~eZW=t1U^W8bJkQ`U$$HE_^(z@H@Avtq3? zG;*+gqrkD(jE3!i??hnJD-H9H3KxemKT&Jb8jp+Rhm=D*LTbT?L->KyF|p%Tl5pAy z5nS}H()$~7eHb53FLuStYXuEzrw$U|isrI~ZcjtgyUMX&4htrtYc#7iOuX-3QSald zhFk%q=zuX12Gx3fPITdwtHf1Q1&PyK9dwSJuv6>b5)Wz;EyVWTrFzB^2qVL$Yn02K z=4bz7an6rD5(XcuroI$wEVe&0WtAe^aT#osRjd}zjDhFI1VnJQ;aHvndEqEz*k6K> zqT|^bww`Tdr?E5G+3Y-a5xbOK$*w^x^DXQ)_9ONa_H%YWdx$;Ce#xF<&#>p&OY9Hq zHTEWuNSr98{BiuCUe*66?uopn-oG~4|NfVk@>h1RO--Brr=X7m`-=UWeZ%@-nDy{@p3Ku=$;;)1oaE&SUWFLx20n&2^9g)1Z{suh96p~P z#TW4<{5XC*U&Gh)jr=rz20xph$1mcS@+}LnNXTR$l8{M;ErFZFiF#HYnuCouj4Z@4vjy-Pd%3aof ze0k*ZZCsj1tG~a{7wWG~O}x_nW>b^7TRnhVxYiHj7M|aYO}2}6`xo}Zc*gWM@!RQd z`c^$e9pj5H@Hbv>(%0&1)r;zYUffOp13LAUY8QW?^J{ao(Q)!uqFK!_D{GMEa(IN z+xsVDgY^JyBKr4TKWQz2hlJ0Bu7d&1xZ=B@QeY<|%ccjF(vqbe7~}BHigH}4ak^WP zu07mdA?ZNTBD6IU4u)VI1sn{?Sr3{oxWszl>gIIP8_dD5zav;w*BuTAy(loEyuSV) z4^zI1G=#YTCW$S;7fcG2=SSfSAHWxqA6WSE!R<07-xiTPhRpjck|K^ zg5APvJ|0#L6=OW>Z3w(vmQpY{J;lqiUu?8O4O5{21;2^m%*YpODa(TJEbTUNmid9GA(;-kT1)gHl#9W#>I_5Sw3S# zoT$;CalKDh2FGSk<15AnD~C+`G|?MKO)W}EL<{07>*>Yx>e$8jI(zJ|!VM>uWJR7W zX+tNKLnJjdB;V=-hKDfD$cMCwO%1i$7Gd%r#b!92rkZ?82XXV&Wq5{^`Ktm|P6~2} zWq6`oO`{folcG#(=%}mfXc*m5Th|e_2%@M4JTlzBsW%;tp1s%2Fc zg2Bz@SrO0OR5HSCP{cY>MSNhF!+{*Tyn2Kl-ROo9&DG5V$4CL{Dz$}!Aor(|$Y>Zf z#{SgqelS@4}=-oG9Y^Bz9eZh`*N zeLprA@~$FZMkQ)6*7N!r{Aw_tbgIwp!b+X{oY zZBbO7ftUho0*uH6Xz;?=caGal8LnXYgZY@+gIyNnNoURycAgGLgQVGW(aONcOU(pm zHx&jFFy#>#Nhe3#AWm{Mbnt-$B(Z~bHV5R*BBvC4u~2y&mC2#j8$|Fc0$+RIc;_Ad z&v)Ljfu=$@#J@HP;$H=W-#g5M;k127VnI$kWt_*+p2t(YH>-87>9YV`z!s}PCtP&Z zh*joufY;`SuR?oqP?E5dz2vxE!UMFI#VWk3o~g!Op+0Teg2624y;*csLo-8p28f5U z;c}6`4Gox#sI~zWOtvx(!Y=@T5|<4?O9E2C+WR5@?RyH~8vaXsPquwMMi=T7y1-DN zh9KZ}m6TvqFY{lSAvJ*a-h0pLo1UjJDDNh8YFpUQDW93D?Y0q-ma3YC$UWh8>0#EOLqe(o6kRI3z9PD?cO7_ji7src?dp59`HEOzeXr>8 zFeG&$l)bVYPtX|c_oK$>vd4IdxsDo}H*X%bM=;v$Iba+nJau6Z*P-RV|8yQ;Q#1nZ zKKolE4QXlHw{L$1l{D2FP|v^M97}`OwVl^ngQ50%6rB<*Ih25zR382+H6zLygQ9Fl z%9`iU?>38`g>k~YCHzU8DWt^&>_ zi$$E!q|o>{s!~VE5bW#_AX(sdqe^(j#gPdmE;$Y*F5oB|Ybb)C4v%Ozwtq5gf7POxpvF$dEP&#gJbZ1>Rd7@?*_Q_^YZrbFCcNy>Gehisrx? zud^?MJW5ppKe*k5BUI6?v7f%uRH_kV+rbdGqoYf@X>@Dlwf4yL+W$M-W8wBN>$+Pw zq~KTJu6K*rLRjnR?xuDzXVk8-Sm7&~O$dd6B{*2%0Ek6-K=QtK7D$Ry36&BO2yHrj z2e}VMkQc9zM)%um@hC8 z5+*HFK13cMN1_L>o$k(?B3HY{ri5_j4)FOU>By)GSxz_c?-W2-5+!%U4S-2r7vm-Q5xhQWdZ9BWW zyLU!yIGQ>&*r<)C*AneLz^nH^Bw;$W5U}Bip>Q*}{G#XY-z#bCbxRPf)vF_HRX1@=EH&0 zy@={!9t@T=z(PMZnzl#qqvlSIbm9hlgQAK2c2Fa5f#jgWqLPOUo)a5^DA+hUC-(N8 z0-Fa*17r>)PP_~~qlRy@L?q|{N@A<+ zvG?o|TlUzaR`4m>ei_YV_oBh1?i$r3R^46HEPC$24H~BnTQutMfC`#)z(#oM z;kRRm3S;3mux2%Ec!glY@kvV;d2R54#5Qy?(lDc7Be+z9H8?#~lURlm=|r@~tZ4k< z=f~A3wA!X|tJbj+Bh81`_^XFA$2eaz0^YrJCZOQ;T5)qvPj8n6D?gUM8ph*6=a^1v zv;$&Ht?awr+8dWCJnFo?_8emtY+iiu{h<%7Ahoxrho<=E9&zEmy9qLypS^01Bd{I9 z90N8wCNRm%Xn@u_3B!i7##(%G)^soc4kJU>5>Dk1&Lo<3U+6BCVwecj8h&4-RKBznqH3(m{ zUHKxF1|OSWAtaJ5&keQ=XPUA=^Nrihaoc%P5B_01g7x&R;g?BDm|^KRz>q1;3gmnh z7&f@yQ5sf=Bm#whkPU7!m-OC-Iy0SW?SejWqMiHq?W5Ow@R%H_NZPmC*ZSPDX}7hj zt@VliBYWqd`ZkFRsQr~0#U8)CPB&D8oO%!?yWS>yzy7U(~4vb56BQcZUawiC++?9B=W?eByv z3{XH~|6#SM3#_+zPn;_j%pn~(hmdH`UW4RuKtKG=?%p@L(H|&1tt0veI1Q}Hu~?Ho zQfu=6R?ffL4Dg}hAKiIRk9hgD*UXn9KLCgJ@}bTn)szPf*7G4*3Ery>|BMh2Tt$(^7Lt^+xP;8diqL$tDP3E5SvE;Bj$^! zz1qQ4xev+Tkjos^{9LY+573ppsqKvoe01|x^A|{ixYWD>MVoO=&G+ySr!((h`ICn9iwF994lsB6?Q{W6V7@x{Oc_BYzlaG(+L`Lty_ zz@bwPJNPgfP7jSn9j8%n91NFQ`mn}NyaD?Cc088GDu~q{AeOjZVmz$VSIH0xxB4_7 zo|RP7QH(S#0YY#Tij>SmOwTGj)H1X634}!J@vjHJHnzvSweI%O1pzx#3XMzY(7mCAn;Gu>%SzDHR zHHt5Q6o4LJhQP)Gcd$R^9XtWiRLv>s^BXv}+qKq=;y2W~!`d}KsL?Fw>v6!Vs`sO6 zRyrAPP--kgO{T?;H<9Rd1TXPyWT;xLGIHhm8ahs1xXL9SH=7r710x zYG3m+st0>gI){haS9{kL!^j=TFgADhJ=Nadj{eIZQ5bV16o!JGc+8H54&Z@ zMp2@OVuK_2PGq3#j&(Q)zKO!C4byKK+N^L8%TP%Hd-m9J0L`jXM!6m99a?^e97+i` z7}zacvI8v#ij^fFrF+6r)!x00E2ad4%eKz}L-zL6Rb#r3yzV&DkD=P~MWkJuzv-ZF zJMjRYg)3*fh;71#A;%Bz-Z|GS4F)H#Y@~LNyY9&DF{`HPc0uPu{8+U=0WUQ}KH~z; zWBBM)@auLH(v|0dMErNSQc*BHIAuj+_nhs^c!-+hFPJ0Ix`vmB6dg&VxG@k9I^EDD zxig>=AL)LOyFVe$?V`dz6kp8ANR71tmkhuyrY^l^PIu$V$w3|h5~yXu2b!j}(zjTw zl53$yhJGUnas>3a9L4?Y5^u0jXfJ*$zm=+re0iwIS1n2mW^kSnl*o*dXnxiH#TV`7 zS3D7)JIeXn{OxjcKL2sK={No5{KqB&NF#lzJok`afFYQ{aCF#aSR5CrWQKL2eV!rm zdS7lABX-D-%J*fJAI|%L5WuC25$(M%?m&_dz*228&xr!^{wyu@~*|hbh}T(R!}2td2dX{IC~#s{ZoB zVd;Jpou-{<=>?R|OU;qhzdF|)dybU0WSt$L`?00Kbs(58Lzj*xwETWpIvPvjqfuFB z5UqKIheMPfFf9sx+JlGu9{APpds<%6^20#NPR3Owt~ySBEdyteRwRiyU$FCF;z@o8 zL4F_^@P8pcsP$;ANds$gEY{@zQhuOiO#k*LKOEvb+VX>%50W23kRNOuexLG#g)hv` zUdRu}{$G$EG)7>Ooo(K2uHZAw`^;`W%ltX*p1sx?abTWzZ?{8!AU@LKOApSkE0(}Z zk}T~dQXg9SSlUmD7cDzv8ALN(=}H}meWG-c;XQ`7H18ii=Ro68;X=_2;#I=DzvkXl zGt>x4MG6lJ9^D3R99({YdyNpxr``!R1|4;AZaF}0R19Mr$Y7N)FzAphIC#lzs2feVr%G%v2Ty41GP z>xN}%Ekhg@zidyR#xhn?m}UMxcxJnG3vOG67{E6>Z~}9P(uy8}TonXHERpSYOJGw% z6($F`j*=m){dj0{m6jlaO3`3x8c3*$tdnmoQw<_RC}GsRw%O&fG*K#DAycc| zDFcBw$5z+aNms^MivVgONH`BE8*D6tC!1~;{4Bs#`sn?Mv38@X$#9F7o``L?U{!4r zcdYyaD5VsM^r}sj=sZdc41=jmq?$Tflv=F~O9%07^d+{Crk1pgUI(?)Ywz&S6$`DT z+X|m57Od)=izP2PC0Dwj*q}{?T!wun8u&h^&O7> zGKzNyYcLgEvF7+Y+l4ihutZugs&n`7c@hGS6r;{LCfJBnV2;0?U3i&&5>mTLhaTpa zaO0499a~wd{C6z=O;zVs(1AoC!!?v!pp&TEN$yszMX(s6a^s+ti)=NT@=;QgZMi9t zeOHdiOqlV!LM7~WVgGbJ(H1a|tCPq-=QV411x}eCw6Z7d4 zqHBzs&J=3gSwV%8kkTfw2{WCs^xyWKis;)8Aw*CWQMFtyl$}=5f0FQ!5m)z2UT}jc zj0c?!XScdLB|#J8r#zD^zc>?63ps?fo2TCxlAuis9mPSpJS)v0Ufoa^V?jD0^WpM!d8dPn=|yU7F8L5= zXXGWI=uI~n_3b^ZXpZJJ2?k4lFR@?-cSdmXYD#}^(H_Oyt1gM}QC&sjP4__?$>oJEqVmSXy# zs;#R;azY2<0He!$6|&jFen&ZhWkf>`GP;#ad(GlmS@hWz$uX9oJmWXkRcpKN!r% z&o|bmh&72SsEY6uZ--5X5mG1wk?_6PW{iI-7rX7q|dUA zA-GlrwhV7S}g9HE+?vW zJzDj0pt$h;t#0R9Lqub^EU&q{etc9pUa@#U9bOC-cpE7$1no;ePW}@Jine=(Be7^M zhy(KYNyOh`mm3|#L3SbiB>n=47=o}=j_A!TWCVuTzXe<0dKgdWhY>4FQ=56qM#wpev?Ml2_}p-lQF%7YmFv6)EF4lE)EdZ0M6L@^Ck5$kl1$J|+y+hhOZ0vL7*4SEV~H9?j;3vvo2{ZRy4+!Gj$Z%4vRW5@%wNo3e>4x^ zm)e5XcqYRh7(Qnd5@5(=5^N1Jj3R`cjSci;5B7r*1aa`rRmzG}z|`15lDfxQjGhBeSnZEj{x zR<18I7voyim(n-9?}w4OptTZx=36AyMFeQsq7^H?xcqXBlau=D`o^;yWJ<4BD+1Zl zL3@x*6HN&0M>@pGFzxV+OGOof^nfciTP+df%+Z7ZO&17MqzalRwPxlv)C5Oox&MB&$OqN?Jb&mYb4r0&W$~+KX5STkS74oo_sIU|1@D`gd~CD%)M_dA znKU=T911@1ba^V?_v%X6;FO4slEg0=*b>lCD*7k75TS2~*~B9yCndxouejGmJRrP$ zG4sS7dB7z&&WWqnFW!_l{n%DoO0CC;|D1K!rYVhuWo3npQ@-cEaq9K!`{7NC*Eb)$ z?_0r9=Ce(0_YkE-Ff58`V!&H4QWq0ziCTbDM!750nCO6>Rw8&`x+GOn>G!OQ@)!*M za7W1Jh4~~mCo2eI^HrV{!cR1_xbrCyQwO-7zdSx1pt?HZ~^ccMN50LSj}^ znLjZ*zNQ~OM}qclkUMbhm9eYa?kS*j+K38bG0YP~ym9zMi$fU|gRNvSZg)%vi-~b} zB3D6-8{-<&{~QdzkD0a(#5iz=gT37mDx_j^CB;Slg8ZDa+_LCXG65S*77czvM$g{g z3QMvwabAub>M!xBi&aw=;IE3hmIbmhEjz6sJH32_{rMv-s`7HZs07N*Z)Mp^R)}NB zkB!3AxdLSJ+B`8}uE!h&l4Tm*>+7IAc#9B(RZ6J3Gh=9W}DK}Ia{hV?7;170$ zGLn0%EXGrE{TrYy{mqEY@GsFudLyG|#oc9-9=G-BcdT$f~i>1sa1n z0Z&Zp($B7Vpb&9t1V1g$A>fleNZ!6BE=G7f=Q!4Aghh+BShR-zOEdc2$odR3Y{;#k z7`A*=aAGV%+y4bdQ^;8PPC%S_9coDEi z(?zNxaR9ogYU`5M6XUc$jo?*NB+p@pJv9DaAHjlxwSE;!_%n^72xOx~=sD z`QEuENfOzwY)|`&wuvRzV@-zTdRc-q|G`7o^I=YCr#twxh@MZDIP`qNtO9;dQR^@5 z(0Rj@I$i{F$X(44MyjFJ81DG`+RdUmYlIo!+8WWowhawAA0!s^|B6 z>)*kfhWo#I#p2afk^Hl2juIvmNNfB<7= z>w!A4jfLfCUzA!zG3i|EyMri>h8kq%$OThi4yhheK0=nXv}-a+_&gP`jZ!ocdZA_F13Gm?VID9_7gYlN z*4awG{sVB;{mO1YP%-`L@zx|uq}O) z94mq3?8xnY%BYS|0%jtjkNQr!sPP&IOsUen3r1=vHkJVbXdt8X@s@(BBa|4eqdGkO zlsOW5yA9CW9gZE4x)_u{_`<;+(4OiWi?+`{2rZ!)3X7IJ1yX|QHrk#S%RKH4VgYRh z+Uic0uu!lDLC4zl_fwi?H6HN??S06%;hS~G8=CaT>@L*?D}GIJWnj-eG)mWk>F+)M05m zckbL((w4P8ZDhWA2?*ATzVZBX=x!0i#3qL(;z%KHE@T-67{`$=1*IQ7@W%o#O8!{x zMG)-VhFd2(DsUI%WFtXPYGQ(&28%K|r715RgaoA_g>G$)j@=4MNvOS`5(ue znULX%<1sO9M{h~+x#L{%i1PoN+1b{6A3om9o9r9WlFQSeYc9cc?kvCgDQ$AeYWnUG zrSf=SEXZou#8693si3-r7z*;{xmM7zj77seV4{&`Vm_E?<>ldKGdUYpQBoWz%*#zr zO-hJyvmBmHX<*494|JlKot-B&1BFa8sKOq{J#}`c)z+z;xpa;rh3;x9nie-=R&Cv^ z;c;|rzSW~F9v!Kr^jVVHj-qP!Wd}d3Jz{9(^qLRx#Ek5qsQ7@aBE2W>@RS#oS6Abg z2WR-7tQryq=|rF`CT*iOB_XGvDma;juw@=sQzK^RsxE{J5dCmX7w}=^u?68zO&~E3 zWZtsl&%7LZd(PiAi`U{;0baX8Y!BClQSEF2|6wq2M#OjYYX;U#6~-1TP{n6P?|ouM z4Vth@!yFDAXh4Bcz~5bK76TO3O7F_v)rx-4?W+(KqEwXfH~{Z)pM$&|G@iMAb#x#6 z@gjZp6#+$*Us8-Yg3dl;5mgK}Mjrw7I;CTBk9FfPEX42;39w}FXN?)Ul4CZ@4wO(h zu{Sn0zk=cb;F?Z8X13`@Ga3zZpZYnEedMJ88E|GaOz@Y@=4Z_|D`)2_D^6W`PJIDj zr9=KLtWs{o;`%ZC=xb;;@Rz<8;D7&QT}$M2{lL05SS2sAu3hX%c(dr|-E1ho)w=eu zF8+db9m7(^aO*mjO%oyOIvyDyFSM=`9M_4wNZ2qWv3hC23H+5C>H2w0eW}r8T?;nc z=(4UQ%Q7xw6HxnREoud6AK{4}s(fV3MwU2L#X zemh84sJZAwe;WW}C)!^MtP^&Q$GtUxZ18zr4~W*`XAOP^;-X!RV-?1-7XSI#42*7_ z!aLnt%_?!PO<`j_`t4LBru%-(0?p1+TvPp}lLxGkjg0;quGZBCU?Q^i>=i?+Q;(`u z^+33sgR5oeX`>n$tzG>DL9;~lwjRH`asMRrOgLDFYg&u+>>T9soQ}_#7z;t>XOWht zN3}rX>Ob2whxDWO&0zi7&~#$Fw3BFcb}20Q0kPf_e%zyeTTs=5t~X%M_y=Hzb}d1; z%o=(0_-MA)S^L?!?+)#)2@0> zd@f~Fo1_zO5@jH$k5gl$RkIZLX5%lyoNluVf6@Fb1J#AM3jOO_ie05nO7&;>m-`J| z7PDt@WFk2MatwlB;4$nO@I7&;qLjcBL1&ZL&rnY(g?llIyV+$tm6fnk*30@>H>xb9 zqv&b|&txBCM=nLpr7T1~Wb+)^WY*3)%DB4;6S35AtGOg55ckm$Lt$E>k%l z!iTa7)Mu&$%^${xvlmdQX#{Gkya?a=YW5PZ!FfHB*Rf&95ImeWuqr-^kLHbh40{)p zshT(Ov3wj`#+xy|mwAY{u;26XsPQzBPvWiYI6j&EfluL6`83pgn$C~lGf?@dozG&I z^Vz6Rb`R*=r=T8nd=5X7&t>&|9{Vf%29==}z~tA!7otMc(fkSEG{DS~i+B@)Hp_yq<3W)ofy)@RQi1{A5&^ z+Qd)cr?Rp9H1+}i0pH9|N5!cv{7ilpKbxP!CbAWLD?b+%sLtmX@C*4x?0CM7U(7E- zJ*vytDtZC<9}xFvwi$6{ug!uf1Azc@9=l|d;G6#0Xv<)&p&{B z*_r%9{t^F}|BZjb&ftINpYqT6=ll!)5B??pivN>;&Hu&!4X>Dm{6GA^d@uh7RqT#t zf9LzyFL*CEc^?#3oE-y2(TA|u7y^~-g-67QSP>`UMS@5aNg`RK2(L&LX(C-@h)m%V zSt48Hh+Ifh`JzA+3cm=5A`ukDq6EBKnJ5=S#8B{Sm139}E~>-`Q7vjjtr#ilM7?Mb zqr_;@D8`5;F;bV8lsH-(BaRh|#A4AYy2KLEEtZO9;yAHftPm^3@yI-Pf>ez)gSb)LByJYBh#lgG;#P5+xLw>K?i4>lW%VD6 zo#H3rr{ZVgZt-(*kGNOdC+-)!!~^0%@sM~}JR*J}9u<#?$Hf!km*Q9A*WyX>l=zK! zTKrZ#BYMQM;yLlWctQM5yeM80FN@!cSHvI0Zt+L)s(4NONxUxJ5O0b-;?LqO@fY#7 zct^Y|-V=Wn?~4z_hvFmgvG|+#MEqTRDn1jRi!a1K#FyeL@lWx!_?P&%_>cIn*ekvf z`$VrWMIY3Q61owYD$``T%#fMVC$nU>%#pb= zPv*-4St$K7Ad6&B7RwS@D$8WK93qFx3Rx+K$>Fj}j*!)|M%K!avQE~^1_>WS*(k@z zCg{$_$z~anEpoh^AScR6vQnl|Tq%#2tKTweNuDe>%1!bVd8#~3{y=V)r^_?s7I~&TOP(#ykz3`t z@;rIIyg*(kFOu8j#qtt)sk}^HF0YVR%B$qna=W}nUMsJY*UKB^jq)aWGaS=ykvrrM z<*o8IdAqzr-YI`1?~*^3JLOO0Pvy_#-SX%19(k|4Pu?$g$p_?v@*(-Kd_?|2J}MuR zkIN_IFXgY~ujP~SDft`uwEV4nM)t^O<#Y0R`GWkNd{Mq6UzWd@ugE{h-SUs}Rr#9y zlYCviA>WjHtr z@}KbFB5$Sw)Uh5X!U*ggzjI3kEEY5rYifamehLU?dtz>^JObRQz~=J;)w1l8qG9 z8QaO8V81lHklEtcokpsWW~3V#MyBC2vW#pfKXQ#cBi|@63Jt#zFp7+zQEZeLrAC=i zZVWMo8Wl#RG0YflR2d_TYNN)eHAWhBM!nHsj50!)Xg6jVvyC~%k;YtOo-yB8V00J@jiZdCjbn^sjYY;{ zqtoazmKfc}Qe&BMoUzWwaqj8gQvvG^D!}y_bt8trgyK#qcr|~1>F5}0>PU9!WPmP}$cN;%9 z?lJB)?lbN;b{P*C4;l{{4;zmdzc3y(9y1;{o-lrC{L1(>bSJUwe3VGKh+T-x^Jg1R z8c!jH;v;sJ@f-G}@wD+<;~AsJc-DB%c;0xy_?_{h@sjbf@q6PH;}6Dec;0Vu`i1u@_b5L&%fnhF>E5;Sewz}fj zc2~SB!IkJrawWS`TwYhIE6tVe%5Y`6e6B24wkyY#>&kQGy9!)|F25__Dslx~#jX-p zsjJLY?i%76>Z)*6x`w%iyQ*9xT-B}`SFLNLtIk#LYH*EmjdnG<#<-eXV_oB1&90EE z#WmhF!8Oq}$<^wb?3(g_XnPmHsIF>n{5bG8-s7=ZO35&^$vrE z>qYAq)(PvE)=So}tdrKSt(UFeSf{MtTCZ5Yvrb#Tw_er0Vg12+&HAJDy7edPjP+;h z4eKw~S?jOXo7Uf~bJpLjx2%6y=dBBPdqKAi+r+Y@W!rWRP9geiKQ1c|*m-u)4%uNl zVn^+KyTC5Ai|i40v0Y-9+9T~z_Gr7z9%GNSFR{z*OYI7KoLy;G+2idAcC|gxuCXt( zC)w|@V|Lt5*h#zAPT6&Kz1?6p+LP_~+LvRYX^Q>t_EdYC-DEf0)9n_!)qbBn!@k0v zX}{l|WnXE}#!~4V`vdk|`)YfheT_ZezSh3ZzTRG7f6!iNf5={BFSeK1ZFal8)b6l5 z?Jj$nz1;4$SJ*4<9($GDYxmi!?HlZVd%zyFhwL@>T6>*+qrKk#u)V?lh`rIi$=+n& zY;U$dYTsgi%)ZtBxP6;_yS>H!gnftoNqeh(r+t@wx4q5&l)c^lw0)0#uYI5WANCIW zGxq)VXYB{<&)GZe&)d7~FW3*-U$l4I|7q{Bzhv*VzidBbf5qNsf7RY^f6YE%|Cjx+ z{eSF7?62F8+TXAb+TXMvv;W(E-2RsRg#B&%ko_I|N&CC@Vf%abQ}*}mBlZvMr|lnV zTeTOlZuCj*m)Z;VGup549pqo&>ygK?O7@cdBl}tV$M#YCC-yP>r}lI9^Y#n&&+Oy& z&+QlOU)U$?U)nF(zp_u-zqVhte`BArr?vNYuIbDf>g|rVOlwhQQ=OPGF%x3ek{OFP zPgnRf#^JA7aPc>-LGWo}N;uOJIn!3PE$#2?&6(D>tgp9oWzMvgwxvUZow?0RyZe_8 zt?KIOT<34@=o@TXy0o)*(4M}u4csvL``QNW7EY_(!bBBxlO(xWqHdO`nkC82wf>fL zaNR*hy{$iY zMux|@Vy5?snTt82_7!P)LyjDByro&>)ZCDBg{0^Tl_L9!!S0@p&fJ-q&{T}cI(wE1 za8_Cxv3Rn{GYjdkXDg@KE~na*+0whrzRDH-^tckpbjk2^5zKUv-E=A5>5gorn44Ry ztCsim_lj>xe2e2txTZ_Vw8ZkRT0YdftgU}&RZrW{V9r&NMHXtywA`x(dfEn-s}#=_ z!GfhZirFM)ip+RR zi{!nf&blU(*A|JeRV3Epyk4V<)4Ha=8-;KU7pi@&%GR}+Z27M3 z=?A0cO)QZV>9;o63piN+f^>znubs4XO{_jw z#mDxrD^ex|(=iJ#$H8{{{bV!*xV(~ejMyK55wEk`(xQbHklNhDqK&sSN%d-CkppMZ zqSi~rm?om0=4cb~H;sz~;c=0`lyIgcbJ|3lZPH%bI+X~QW~Ag_n#p$u*9W_k6KQv* zYX{Qev^kAxv^J;wo#}9tq?;sJO-(tSl8jE5+DN1%xlNp%SRy4gt2JdW=fsEb%or=5$MXx9S9rTuD7C}!C6e{ID>C7$G$iW-E0^_mcJ}tP^>%bGwR>2i%B``N<#DxnaiN;J1x zeVNubU2@#w_!6$^QkX6AyuPf~*C%pOnx-kY&!uVlITLojE8(endq8DvV3?E=@kXns zuXh6yiJe8qc2OwJjQFT#2@97J+ifN# zd?a-uByyGL*{=N8B{K*Lff0Kd8A-*^nqnk(8JCXO)2T>?U9HmPE5X4gLz(u;j`j*N zYB^*yCq}}qHpK*$@02Uwu%}~8x$~V$=Nl~Pe5dSH6i}VLmW=vbRe)>{yGoG>!E{jy zuS;>SB$A`JIk629cVlN~yNirQn*<*Ifeb|=yCm`Zet0*S}$8_9_0-pDn8?O|61 zVnXmnr;0I3{^KoN7Ua=MWIQeo955s3%SiTVmqteHAuMp_w(W;IZ8SFKbK)l?br zb9-fLu4=Sw=c0+%!x$Jy#Bx5<;DZx*@F^CE-FE-7c)aR3nPj##*R8speIipnBXWqDe|plTtKl z92u)sdOlX0a;lKHHwvB<5hg{XDaXHLAt|CviYSv#d?JdJ6j@S4oOEz0lB9?{DI%|x zbk<5dwIcFbNl!}nYb9N^lCD}wPpzbHxo?0iJ4xfWN`E}xv^rj@9 zl*E&gc+{vcR$J?&PvT2CRY>B);8s;3aaV(9R2>NqqfU08F6o&r@v5;8s*a?qMflYS z0#!(LH?^q-swh&QWDu|FQ_@{)Qd#whdsEea)YfK{GHQjpKGmxFWKHW?y}ZrS)H&E@ zy{~Q6sy0q=vavR&W%WQeMo*pva57uqJa0K{o+~iRx8C2jdUYG7va8xV+KelQjM+G4 z+Knk+Te)1PUB$!602u?DM+h0%_D zTFXcnR!3*gU|UY7h+;h)OmZA#QFN0iRpe$XM5%)2HNN_Al>~m=o3j z3uO>O84{tahCr6$0c!P8K716e$|TvG#7EqSa+V+%2?^q`7YxId4@nXuevVC9Ipc~& zGRue;nPrPgzf8ZAYTzGBPOtNG_E~s-mKdxUDJXHU-5!e9HDd5F6rY|-HYkHzLn0na zC=_FGa%@({bY*bM1CK&$RRFbdcC1w%Ym*A&MxBVqQwrra4OC)L-5+oXX`13vk=Ck6 zYsIZaxwW#9V&#>HCu$X{B2B4CQ$kd6Vzq;^fT;)@Dx0ZBg{cTrD#DbCFr^|)sR&al z!a9}7I+e*f6QsDnD!w`uU!77t(^{ta)_3;zO(NhOT-!&5 z(%>EFUdI++Z+CAe+x(r~%a+r?J>KZ+>Rv;{L`-?@=}zJt^0oE$4R-c)c1zb+uQX4+8rCK1Ro@I#<-K0@(ZG3Z z0kbK({6?&%^e$G$q@M0&Z6#SGx6Al!mv+qDI{T^VEzVduh|+9EtoF1GcJ{MVNStVX zO|j(*?&cYpOxos;5$P7H52nQ0p*wLFb3ej6LJ zM5WwaWjZ>GG`uZD9M`mjMz|@9Gbt}{GVLyn{4|jhtR$MIITj|5aj?Y~S=c=jR3^?b zZ3AeT-2*EZUu0oFg=w-|p!sR4T0vs9k*RZwSGV4s(&Ju3~n% zxw7t}x{9FKMRqdJp_IBDGFEmPm%$gmt(CcAZ|n3wcqnvv$jI59j-lQTw9Tb`{hb^` zh08<6X_yT2SNdr5SvW_Agv5VHh`z6iO=8b%NGS;XE zI#p^f1eMmdT4I_b&l)#$0dj%VqKq*^F~DLr_z8to)wYLt{z3rLB2 zwL6_?l+jdULqpyTL&y?sz4s4vc1S8zs4-bvNvbhaEGc8Cx;mAry1LeA+u-mrC~jMY zYz|=geR0~xMNytcGY%OWvlL`lGBq?rbwKso%rqi{XQ}*rqGviy<}Gw+P8Z1308^cj zh9K@J!VV24`zUr1vLcN-4w>{8I3#!D2@)~GQ85&qj zfRmP=Ml-Ys)7+lI9JjllbrD<%8==VV2E(z4e^(BKk-9IJ|6(`Uoq(uvaFa^2NLhI- zP)^h}y9RHHsPqWil>j76cL9#fre%d);<(9xiHAlUQq*zDbS!C7CcaXKOC?7z4xqqs zaud^OaU5NBIUSbcmWgJRLmFnP;$X?R0vh2WxCK$*rlZoa?_xJmf@g<9w*%Rk8e(cj zCMrzEGlf&eOj2)Tlr!f0g z?TFb_?%$?il{2TS+r2m&L1sy}y`nUl6{QkbrYQ57O;wldEGSxrbF_<7xw*5V3i)EX zDiB4d063dUYHsf;@o5GnM2^r^P%5lUGB_-i0Vbxsm!wgQFtTP-Du`jkQOuqpW(jP} zaK{WE$FBsM86hY^C`I7TVNu4PyAecPf`8^`bf%9?+cslcZej*@Dl1h@=!)G8rxCtfvFO4dnw)CLIR7k9N`f<1Iecb$ZT z3qxo@C@Bj>$vU-=lvG1Aq(}H=#!N*nvN_0ETzZRrs}%&!nVB zZBQZoPWa+aZD1kY;!kZ@0eAQ$z0QzOhKxzI(F8t`gRBALWoQ+@+OR@=YB5L#d*By& z$&fRtHo%Y%M@~+BlKwi!zm$s%U6XZAydn>^L5J{V=vt@pS0_W#x@k&2b?UTLqF(JG z!&Lt3)t+#oK3NyVRHCw#7pt)VB?Nm_cQ1~0+5<%B;2g)} zNgpj$tis}xL#fMc@9gOjKg?CD_JkA3hNirwZM{o79oIsfMdlw{H8(IWQS`k1%2FDtnX)1sZd9anW-+1b%Ov`U=9Oq$oZZcqrtiqPNI zJFu#IU;s-z;xPImhb9~hTPd!=&fXh_y8CYwG_TX0;exL2p3bW~u^@oW7$WeQs_E^MHKYz)(9{#~@mbFcl5l*t>K&wm~7( zD>`}{vT95XDns#xX$4T1gTs?sf_dd&Hx6N4R93-s55(OLhZbvU^>pBPo2O;S*)M34 z^?P-iF%g^A;-hIbca*u(b5d8z{u62!J^^i}a8WJ^9B3wGj^lI66y=L~z7LsP)<1+G zg+tWyG*xX9YIQx4XqFg6EhI!8CDc(7#3(VSGt7wuPRyZAQz@x%WoDjeQ3afcDKl2z zoQtltdrezUCzOohl`bKXY|QOk*Nv69-ofs+9xo0HVJp-LQ2M8Y^qY;+pVg}~9f_nm zy$Vz6@ie8k6F8>H{hB%}0(a5lqHh!RN`EI>l{c2YuBU4$2NJy85 zK0?V)h6oAix)RbACR!3c1VQz?Q%a>FMolAP3usk6k1glLguqx9B|(w-Zoy*ElvAiZ0%k)_t!B1w(OC@GOPMQYO9 zCS+8E;X|OmlU`0xxd=K4ExAah8snnj)OluiQsq*T>y+d=CAm(aZS@S|<&=(2i(yX3 zN@Y?DOf|RKs^+Pzit0|O_hu3)b+`pOL=w}i6hcb6f>d*hAI+{8gDH$cRS?o9Q_Yg@ z<^~+0fOAKC-#W!1tv)67B_-M=CFPrv@=eL+a!STwDI6ikZe-gsX&Nb1$&k2KztEPbb>|42zcm6H0Ll71*9nmeTq4j_D` zp;OX7;YKhOu4uuO3>i`~e@e+bF(vbyl*~6%GQUa5&?BYJp`NXc9xCG)scLei_w+=5@V-&9QGq|Ue|Qqs?+Wd52;srHkS z`EyDJ8!4HOrRtUPOR0mGNROmXopD8alon6Pyf`I?3sSPxkdm#2l=NgN84sk?nO1Zv zl1`bwrqtP8(4G89jgz@lO6K<|8Na7w0G5(@cPb(Il_6hB)(vpwsmN7Yn2e87GDJ+t z;2QZiUb$@nTIgA9cw`+RCF=&MgwWL%4&)@bjEhoo5G7Ub&{b!W zl7V4L)<;sJ4^uLJO39{6s@}<`(w8ZjpQL2yn^I?i6F79H@-6x_rOrg79!PyosPS4# z)@M>OeoU!@x9~6JBjc`=tT&`&exH){n3Sxyq+~q@C*dSsiAN5prDQIgl2J)Y)+16f zvPr2k(x_(=uR4 zc88zr!k?%kLGa06>|U~L1;Hy2*bfb+MXAuVJl(7~7YtVMhZ9*q!b;4Xg5d<556(s`{m6USR?Lt$Cxjq7SCyhqx{L6Kjcr|vVqk{ZBDYC146&oh1aGG_Ri19PV}m36nM~g7TW9pmeJz z?kNtzq+1~Q&!F!`T@6Y-o<`h-rQ>d^cEAqY3Z-i|;iqdim$#{!n)HZfhFguB0V-28P7;C1?KfLn0$luq|f0Upzj0X~OYr*zyibsX?T z{nvoM(SHm0JN*xUf7Jg7_$U2OfM;=kl#ZLDF3`o&xF?Ff-sJ<#HG+U)BMcZd=-TOG zqZn|cf$xCgR;V(-u|^djZuA7a%!mUfjU-^oNC8eZCIe0}ngLskR=_KaD*$I1a{=cW zNI&j*`XJzkaMP!5v>8hQJB=bwqz@K=20{DUl62?s!KhSjC;ec-t z;s%HR0>mu`C_%dHSHs;onpTYm63}wfH#lrh-@Jf(7f?Su>|cZb%?Rrb`%_vNcR73+ zHy+LBc(SFMDb4|n=hl%+k-fP0M?+Zmi@8(G2gTeY=0jrc2RAO-g%1NEc<9bmk$--9(Un_j0 z@LZ8rgsW4E0!UF5DWsGwQE4l>yl7ofQ_+l~c}25fTL)A97J#FxsHdnOw2jC?lr@^B zZNqI(rMUm;yIO*^RZ{;DzTbGIz8bd%F3=x?wpxsP?S8BEpk6xoyI>l)|L79jrm5lHqh@G;YjMY54$D4|Wq&F9t-IlV5O+FFVA)^B zvY&)oH*J)F7dLOl^fCHaEsj2HoR;J=spm4e5@oUwcP@SmJ=xV<8rSNlP&XFpzsH@2 zi*RG1r7h+%S;A$~#%0oOw4=9J$|ci@z5%zq;?BRd+A=Ps^e@9>E0IgGM? zp*_zUeE9vfa3wZV&3i1Gv%Zl=c&@6+hShj#}}19+Urp$K+>m7w~A@D-5lM z8-+14$DP9HX>d<)3+l^dG(Oj3&}}#19^V1ndV4KK=?~!^-F^Bq&~g~7>tCaqgZpQn z!|!3LKe&hXm-v01Z-IT3M(p}QjM#sVn`kj&*PozKy8dm9&vW$e;qKOe{sXFG_*O~O zDA0dMHBEn(YMTCI+|_!i{u8Qi`Z2nrRsSj7(W*aB<8%FI7?V%YkE0$o>p!P@sGq>? zt8?^UQf<^CF($`Nv>20Pj)5_`VbGWyH`1b4M}BtUR#h*J!;M_3 z&qjd8*hU_Wv5g>&v5gSba3e}J+{nMEL}@lfCHfKEOG&e-^<0)8=GoM(JezunOYYBH za&OUW3U@$LnH8YS-iI=z(rTpHl-`PRx)b*qe;VcV5iX~%b2)v3=2R#T%&AZwm{Z|) zU@D=*G^fHXz*Iud(3}c40Ao%CpHx~$X-=iTKyxZc9CIqj7jr7e6mu&5H(X|?X-);1 zVon8_Von8_Qpug=lKU%{+~2t5{!TM1{VkeV>Hnaa737PV6+*|H3OD;=PK6tNF{i?v zzL--;XYrK9B%YQ$ss0`9Paf+$ssRQ5~?E_NHtEkMdehW-FyC|j1^(GN^Ku9-$XNo)Nl+H} z%W}GM`uW!bxGra7&Mm%2bGCqfSI#{-_xsDh`6zKxeDp(zrHsRtvnx%lAgU9mzry!; z&K}~*c{pbu9{#Z}gaVUOq{6#6)xKwQj^cSC=S0rSUW4+$>4zUz2%MgDE}YzCQdyC6 zI_LGAvpH|6RB;HLK7WM@Q^`f;3$(H6un~{sLsIEQ&M2P_7dS8XzUn>Wt>mZ1J00d3 zZvsz)cM7nXaG&Fy?_CJ9-TP+FY=0Df=6RQUdjWgBgOCf+is|P)?=SXwe12cpU+1sO zneE%^+wR*5_XWO(kdpP@P2OAicZYYI_df3fe$Btc`=EC({b0w>yWjhW_c54ShJyr&Qz(v0T}9+jetVwbo_`HFm_eC59JzDd3vloCp-?;*Ip zi03xn2H$4iZ7ThM#h}$XKSkwy)%hm-rutfavs9?$qHMm0RH(k)6sGv7^KH%8g>NUh zvy|$5*ZJn+;a?^W-y%pX;Op?M@U8K!#>2l%IKHhU2k_!qZ?oHs9Q>&E$S#l`^&gcdYFg9 zyxc#-U+u&Od(1y0lP=gUXRZbQCZ?lybMEJH2uu7+u;=rzu^sTBIRjt3eATD~Jcb!3 zpn)BN^T+fShFGQ@rH&7{i}2 z=Lwe25r&VbytDlT+m8^$91XiZ_zDkXg|F$5y@u^IV#kh=elv4ENPhKM45!kL4rZx$ z08VAsrM!DIom}z#5rm9+NvEcp0k(&j^M@o=?4AK?=Qx}c2!Wqvn&JcSMpuCrTy_fAZ3UVG7Qio-z;hA)Eim=dR2 z4Og@s-KMsHl%rkfacU!=qt)n(nO-Z9I47{I<}qag!xXh3eFBA}-A;MbZa2RRN*BAn zmtmOUR1W{W9R7Q$J?dwezJbHs!1j?WhZi}V*I8lT&sXE85E15D? zLFUof9_P3|!1fx>V-3r&hI6`-!>{8qJ;dQ$!f-spa)#G3PZis1*gliPAL1~tV*3KN zzn4pG1H-Q~yjI1_uuejt`tu(4RmD6t4C720ujmZp%wNm)8n(X|c50oxzp9T?`>UQs zw7-fkBu;F@DHW@qZjrU_p`#C;txK1#T5WB{S~7sE)~qhNwaMzyzlbkB9K}~1PDYyW?S?bfIed@7k8dta z#&;IVbI;&w3JKVo?3w;@d?Gl(a5Zv(#ZUr@|^0NM9$2d$vI7cb1{Bz&sl-d`uf13oXt5~a<=8{ z$T^g=D`#)cft-V|9mzS0(eLq`lYnP(&SAXk_m+F3-crD7j8i9jn{v;1XL{#G?(iq__G6^E*}DZ}%pG}0y}P`70S|Z&0v_@n@gBuU@}&1PM!4s45BdzBA28}G#VE1b zm+(!_Tj6W+&BPdOfv?@S0$9IqJ>X{F7T-3E_ICO9`VROG`VRSy_>TIH2M%H6f7*A( zcP_BkZ}|NfOYXMZ z9l5)5_vRkRJ(zna_ek!U+@rb20Z-6nT9eKO*_U0YPgS_(&qDDETSD7HJ3_lc zdqW382SbNKM?yzK$3rJWrz0CfXCg~N=deKQPyeFfXt*?79!;l1Gl;e+8r;UnRr;p5?x;nU$W;d2oq;>R9EX{0<- z9Z5tc2ir9*xJdKSDT&}7IwcW&gHB0=rqU^i&>eJ2BJ{%SS<}Y`=hIn6{a1+B1Go;;l9p42cxH((^_{ne-aBDaWcxM=AjPU(G za=n@1M;YD$h_e^0j}5HJZ-L#!PRanjHb}Nx=v#uY?EsetCr^C%CRGTh4+^p8SBg^s zWxQKmg>F;^?!l>&9r$|P=kWEc2k}jy zz1mms)x`t&-qoYn1%C`*y81T0fb~7?i1swT`1q`LR6C|Uhi?KO$G*}Dd?V#$?YG#M z_yfK#bOzrhJBxkCzhj32!&2;+`>>M~#8)Zv^&*UdOYybIG5RHXgra#PIFN zTD=bY74OB~(!cA|ukRQM|AFsiF)AjhqDF z+25>pw;sf%=jA$578MPxaXY_e2w6*2;M9B2{B)ka2{89 z_zA%e3I3$uhXsF1@FRjhE%-BnKP%=@F^?(tpvWh$UvQC6-g3p4*CpZX6My>!KOp$S zg41}K^8cvd2L*pj@OQ|EQ=S1Shrl`IFDP;fh+Kmrr@$!{e&A&ZU+P2nHt{!G+>Z#} zD)(_-2XwY{BOUK3DL0f=fNl6L|zK zSMGTtzd%Cx8-#zd!f|eid;5C9z5y&$5P~o{E?>t-Zjp83G2wWdZ1%H>*SNMPmzi^j?{|@;LNWJmDto->;ia+Rg zPG?kb=zhkF1Rs`&xrd}F{cP!>XTo}$uH#=dQ99`UzV+@+oR zr2YDq2wmjmlXm78dHJM$`b3VQ9tltSh0toj`z5|X!PiK*TNUmLi~DkMUm@;m6z)AM zrla@g37?am`QlFVamc~@f`ljK8EO)`BfrbVzm#(*!^ia}dRVzf9r;T6hw3C;(IX+z zBcXQ)FEm}^YZd=91b5^natzH@bZ^Dm^Z6Wk_?3IKPSWY5qej9NeG?LW6LRWpCLb)P z9O>VpqGzH`xKe+jQHiHW;t{BKxq$g7zEa$?_%Dvzjbs9YUh{CP=#h;8* zY|$GQPT*7dShp$upy)YE zX^)(awni63`Xieo+ahm8@xBS(J<&1$A+wyy0W&1$%s!p`_hAE#zTlgOcT+-I7~cYp zW_@M3T)PzKc*o%_f-0PxoPZOP6w;;t4^(swbek?&$yqu#}^M!$<;jd>Tt z8v8DWb;;X>#p(h;$BbV$H9dL~^LTOC^54g@mD~_(a@I}CWa{|E&KL_l&0PPy> zy6_9YCFW$n0~heFQ}m$=zX9BB;(e>=>DEJdbtyXw&(_fS2oECwKvG?nLV@ zJPjB#-w(LqJl>6puAvINt9IczO^=+T zJ;3Ow3wTQ?T4Y`USbkwHQE=jy?9gm*{TyJC83+Hf7x1o7e za1G$2#ag=7WdcK#Oce3)c(A zva^Hnc*hgV~L@x+jAYZhlsza|e0NPm5(8K4k)}cecAhhUttcK{&OGppkj~VLu zr(wPherVtDYcTPIq4`H3jPMCfFQIfy0!}ej3LUZ?0WBIn0(ay+42{eDPYC@{m?)hH zH0(&w!qAw^e_qo^H33IjnI9=E=>fhSd`Nlt4w$=O9)NoV%*{y8a+vFZkA=BI=nujl zg|kP~OHabw2^{f-uLD2P=YY=(6Qz?+=|l;S@W4dQ^Xp)Or=$!f`5DC1k7o+pVw#aZ zHh(-I-p4YpvY!X70`CwViyQ^56z>*2Pqig~tY$=>%r6G52z(!4z5?()60QZ!&wR_k z_kw1GD~j+IQvNgGdxiL-Z-G`2eM2*%XF&T2)7}Kl6MY@D*FgIj(_Td!UFeexv>S?d z9{j$f8KIXW*MasN_>ME*F%5@JLx3395+7<<^jTm}g`NaF%(S_%0W&`iA4c>b({4rj zb|TG1A;A25n6?SDJ2ay>9J&j%%}l!mv~}=x7ixu(-^;X{5yve^OHaufxU5IW8#yhj zk&h=!0Ha-?-Ndwono+VMGy^oG0_nQ};o)3=bWW%N@I2ENAiU|CQMf$>*+(Zc?RxM* z&Y|MMEr=I#_Kcv^=LZn-TO2aVBX~ZH@S|b47BX!SXs;EvgEkK|ooO>cJ5fkdn@S;j z48#Wc70wQ#RH9EatrWEta*iC090z;>e!b*30*=u6!Wfk|XgN$fi16+yL=B1T2hHX< zY=k!oD>Fs}<%?XBzCeB>J3>z(4&)xRnV?O9#2?3Mu@S9h+WSDOhQuEaQW+t?;3NGK zL>x^;yFl|Gzh^n#^`Ipp6e=Wa{)K6+pp} z`cP9m#Y{te2~8@3bRtiH79kq+g;B6P58UB55Z-M}LwF;e%!4Gt2oLF_7731`(%_q* zAw1B!K|^?XdyAr=Aw1BaSt3YZ@!q^0Dt(}lb~yyTErn;n_bBuI4zztJ*>icwVfaC& zwSl$;*yg+yfSZ`s4%$ZWy_mNEv_YnkMuU{{kLDpC;rp1j7_>#8J(V{Zw0TTh1R8u5 zZpo_#Z4T2;fQIxHZq6$O4e~?!UIHxvtUAvRIG$&2s>ag-5e4YZ()}zcTv5I401%T4>2hb3DbU#)i zjnG1-p{EVaMI1(SH)u1M_E*rRXh!at=>4G8BF*PG&5(2M$>=uFAZPP$Oe+KJXaVYT zr~tGxOhZZJ9*Pcvrg3+9u)+kCo2>E#`D1E^l zpw$JDkKkRPl{4)g&?-Fyj>|wsu-~)&Mp5O}5mV>X1`FeqM`6>1e=GzQD;%N6%2uqkY09qHYMSg;+ z9-vnxEf-wiC;45+eCTDdM`rk2gG7V;&9xlIT(p8`{dJ(tV7?9Dn+?9oU@K_T!MBF_ zW`b4}Yyxd6XhTdRjwyZ{@N)M1VfY1y?`&`~Xbs?-&wL2ocglAfv|7#J_v)cbH3M%_ zEQFSNR5SgvgE5RBjL1WHSNM5z4*7~a4Ep3?S!6Hhdx-AA1Nv^zD}#Y>4D=mL4>0`* z=uv-B%{KP>#0Bi`F^esde>F^Tsi!kGmAjiGhE&7hYH{RPm+g5K-v523z9 z3OGHv9DWq^MZWeB$JJ##0NbT`JQFn7+2 z$;6D?R6BImi=afAVkt^=EBgHTM8W&i@Q2Y5<|;f#7YdD~y?oXSJRc_>%ms9Bsqiqx zi!f1EV8H+F#N&k}LT~vx6eGGlx*cia6L6qY{08K>0yK=8$pkgQ;bX)ZL5WzCBd`h8 zh>S;#(NPO@GC{3n-tq7sL`Z=MY(k~DDa^4%(fgV2LEyaO>`*Yi(Iea7L-81xEBLY3 z`Yr7V?6)4$ckBPD@6o@6c@XbyMl{rf*RjeX+`x0qG3HqF60_XA)T}VanU!XhIo@ie z*gUt$vjyR^e-E^xXT67ZuN4lh=2_wCNaN5-o&}!SX&l79UipYjYL1Yx(zO0GVMlO#`kIX`? zxixA;7Dn!hJQsdFvLmuPawdE>+8Q|!ek;-uc^qdXdLrJ)V6-R_j2ww}MUO>maH9i_ zxGP;H{3Cap(Z7f{Deu;|>7UZKXUhJaq#l~3RpDIHr|o;}d+qz||G>$l&)GZeFWS59 z|FrkmU$Vb!KZLtEzH0BczlIY_pSK^hzhK{Qf5v{m{;Yk-{*L{m{ayR8{XP3B`};V> z^aC4n0%(p2<`>K_zT4qOm*DMrKfNP|a{!;kd!~TzdK?RIE`sXHK2=-XdP@JQz8~+Y z9{9g2`?rz2s(DoFo!UpW|9aH_J=6j)@A@~^y`+(k;>^QOpqFpg7aD&u&KQ3--Z1`R zoHhPxylMQ+I0xxI|1RfU$GH<9+Kg^slnpypEbzOT;dz4QR~YVN_*sVc612v$9V-yv zxtl2!Y`>G?rwDpBv3)N=3+p!E{4})J3ARBSBRd-LJjBzAhj5x-cfbwtMWNxDe>F_#_2?QroA7MGvt9WwAs(!yU~H}zF2U2I z+@cS_+=J%;p2zSU#`7%P7vfotXAlpS?PVndoW+vSctwn|E@xe7K?WmKLYqf@w1@x z7h=R%e5@Gys`$0y^H{_1mJ|b)mw> z7OPBAno`S)GO{M2X;+~*_%>`RW21b^` zc2`ly$g+_&ux%@NU}S9M6xenaEg#u5at>^}3igehH?keJ{Y5<^yGCMMGG?&SUpWfq zhRVuHlnqALySY7)xlc1kUN^F949W)dyM=x?(F@iWRgXm3fWFsr6Yw>`pD?$En@=kS5|oINR$@*?YB^3oIYx`NWWD9cYo3R zktikLTg82=3LiM!ahFbA=^NyZaFzevDxJXLUV3tLd)dRvKb>qxIG-YV{^JE_OJ898 z4n_BDV)}u?XG)(@bl}9lQ<+wU6Fy9{^x;u2l%Y++eYfPdLd+`YB{P3b;R94tFm|hC zyuaXh=}zG%_ul;Vr4JD9xk>ovm!b`Uzrw-@CBHv^W9bfd|1rrce|_QX()$EQ{P}AN zx~MjSe=NBdoG+YIx?ON`Z!cI=x{Yx2UiQDJFj#t*;N-rbpuKb}<2%{^k%HGsp))wY z^r75vM<7v&!)y@l~lnWWRnA4ZJ%bMuds zu4nvi&?%gIO4l*ImGPMci%QohewIJQJ1GA5lc}}nBiKBzqt#*Nc-okx8JBLm^g+P= zmtwwk=`)wU1o-Nu=QV58%cIWVC8W}dYRwwCA2O^P^~PvJvqpPHM=Q!J5*1CFRenqP zwu;t@`4wHDdq)>ltf*LDu|=~=qtTP4WuuEnSAsr%bX~>fift9UU~edWwPJV0BNc~X zpImyh;z-5uiqo1^axQwLbl>R9N6!R(*60NlCo9g3qqr81UNO!dS2T{o=^4F#T=lrg z%{9c&*;sg?;3a8xcf9u$s^JArHe*yhwagkM*fV^ zppCwN^xkn>#_fP@-{{B2?HsosuDeSPMCXov0<r&z0O$28~m4U&({fIb{LOQx+~O8+T;faoEbsV&h&K_qygO-CnXSdc3R_ zeA`R!DcxVz0NXv0lO?;#E(hP#OT6P=821X&{94)capx<8ny0d$Y(`~8CHcVkv$VEs z7Hqeb)^g+9uxv$Tw6Yww-pcVMcR_b~%K9&fRZgj# z4cnTsTPo*OcEEO9**%pjD%ZodW9*vBO_g^cl!rN6#jU8Kg zL^H=u9Xq@7DDWrBo~b+z^H|x*%9Ai(DLY$<`ES`0{iwaoNAy-#R425Y%9EE>RK8yMR%mQ>tQzNR2HfTRcqc6o-(a^%80V+&DAp}J}~hi%{y^V z_4LZui_TY`ui0JmNX=m_r+P(oQ}voqB~IoW6Cc!aChnTJ|FZGbQ%1i~^KcEue7NUs zy}bc5GU|Uk+U?q1ILnNcXxQnt%)|O5vYX4zZu9q;6~m5OcuFylX=eH{^QY!>o=vdp zrlt9+y}>7Iyc3s9d@H!qcf4jz%`HgNbw%qYc1&D7al^#hCcId)5tPjnw@%zKareXn z6Cd|IQL`SDM}vbk4^oP1jpYG}M* znJ;_%o?P5?g56TA|NPGQn`xS(O{!g%amqN01)Wl}g2KZ5<{U;^l+Mo+r1dMT5Yb&i z<@%+11@1_r@KWX-=BLejJa@y7;l2ToaP7-DajxlWaJt@N_mA2i!wrESw?Bbdysmk1 zjy$3lLO%B!({LjT*08XG$s;`U!(*{-qDFkb=dquKnN>f0f7<>MQuAf|E4U2stM=El z63lp?#)*396FN(d_}cV#*q34DiDJ#MZ?JEGKf0YzN7>z?+4imWZCZryC@ioaw!fi` zvcGA63p@WhRzx=7PMwYVP5LJNW_`2%QJmy|6?fj?ZX)BO#wUow__gs{<22@ee>Kh- z|1fpaV~#LKnq^qC8E1}1j9zffK$>Ubto{s~*59smV(={S>g!YHgvLFQC@OjH}>e>_I%rrCk8J7La-IRNuG@8X%}G2lN%%P2%Q@OR_) zXh+YR*P7RvD?r1Jv)N^?L4DdtyR2rP#{>I1*lsk}oAY7+3GLmPx0|%?b2V(O<}&kV zu>S`3Q=UAmkJ;t|b1|?UbCtPPvpuDrk)BJD!_nrIS`O_Ln;$hlX5Qc_qjratkXL9o z)%=UO(EO0O(j3yTFJaCz=X$)J9FJ|zFn?~I_k^{8d5w8JLNPo=o)UAZ2h#`h{pKw5 zbuC2u>E;}d>A`!(o`CtH`Mf#Z{D;|M{>?n&8H;-Pyt&D|#k|=(>j`;^JrULkr%5ZA zzcbs+MdmNfUzxq;bLI!lKbhw|X_C(D;n13|?YW~?X$~4gf^ypw1rFB`vsR{5>*3N+1WXo{`oo#^2{1&whp`ZCOY(A%M( zFu!8%Grwx?_uS;!e1q8v*lWX+O3vk$)wPJXK3{-p9b7h^$FbY zX7KHEtZ8{o`s=TbaBqe!miH3mSthxS=gp5Y;zWNdlrV3 zFjst-!@4l6eYr5KqPejD$ilvmg<&S;;`~My_RTEpzq2r$>vH+~X%_bLEbJFq*q^en zKWAZo$->TMVHez(o>kJQ_pY?)7!PDprcchoretALv#_QtY!g~qPKr@D z{b5vQ?#C?vu<2unhoPp^2>x2a8EQIi3|2dr><8C055uPn!#_9-cXFv#X*F%-Fn8|r zGila7E;-P#xxo(E`_V8QJCs@OINy|o;}jvsF5&5(G)^N> zP0tyIV+82-uj9m^_@~(jjtLHPA2AFs8OA?)nET}{XSK4eZa_y_&}|K%tG!7bB}mvZ z(A7%&SAZdEl;~H?-DJaxAo<$I_T4T!t=#BXmmwat>V7Be%I}?^5S>=h2*U2!M0P;> zL-^I&WgTj-H6ZRDP*Eb<8pxo*%QFDCu~4mPz{*6c_WG_mUlO z7!TG&iLT`7mOt4YI%XP#p;+{16!8*QA+0sWIq-^z6C;e&oZ;C=RI0Pn>c3kLQNKMT0q-o^CK zGaajcpnrkrD2uTf`6EWBEI17qmvQ+bwb}Gau1kEP!KGh*j1c+c2ImYHemm`vcce6o zgRa3mV15LKkepe zdP5plT5L@=zlOD9cY3sS!`x}c{5(Q(v>??Vw>%yg=8khGS?PGq@^0 z{XN6@hvRX_zwoDVRla(1nE#~1FYc*f?x|t!G)i{mS8pBWj$N}X_gTZ-=MHn9JIwvM zVeZ#C?$A_Je!auod&M12s9ibSs#L$E(W-;1<@v>p`X-zy$#NSbZlvQhZETj?A#tPr zO2eMJJ4{-KL0#ZJOVeJ>a{H@t^K4>&Z@S%d3W@yj7z6K6s1(zlnxp?{jWdHeqmj|# zPvv2}#6X%y(=exC-9$S)q>7dPq?HJw&=GNH^A zY%IIY&T^yEJ0wr)CpDe#@^W&7S)y{=$!-g>{Lv0F`J?fHrmGWEYRszXOT`t*34mJT|%Ly?c$fDq0*-K zY0s2+Hl@=^bdzdby55;sYgctd(udI#xMbWx<2dqzePkY+uz#w3Dt4}oG!7Fk>#{66 zrB?agosN&%gObKhPVG+OKstv>GA%vY7s#|AL()3D9}lPXkQ(i3cq>n72Gv$jOXw>= ztba0;4_!p*zZcE^Wn6 z@+bedDE}Nb;lGjas6>G4=x@0sXn&Q)>ExSsSrq2heRb@nbdH7_`bGO-4QnTPV}GzS zrbbYPYpiFI-5N`LE**Uh`%~>g!zz@rVb4E3rZmaUm}n-aE^+=G{pJqyTN0+yc26Ko z(&Pr#-JH3HNp_ii5Et4K&k0l>U}sEedH0ysr17n5reetag#D4-vq^)UOAhBCWG+JG zNSb~h+=x@Pc3Nj*jBBQ*VNHm&gF5e^IFa{sZmG0TH`6r)t;$tjoG`nkcDS{JNt%Ld zdq#pzd!n|fT9mlC>J-qF76C?}u|0>A3*H zM5}i+fXs-sZ>TU;{lvFwl+0KUYd;X_QT`}Qa^sX`xIu$t$A^7a#~<&( zy8M|tU2d>5=IADR4#(YEPV`HoU*f%u}2;6uhV}%j0}IJs0&Zc^8GVKGScqyfl2xMS4lDALQ%9 zDjdRvXZ=Ot&^o=EhdAlM{s&Q9y~&3z;^8+cvUoZ!;_1v#Skkzr%1I%lMzSZ06FZfS zM*E~3uc}X)^X@(On&f`zF#g6g|4#NlnRASBq3Ph+;X7-?GQ!o~FZ@)Z6#n~%h3~8v ztA1D0KA`xy$0hy`4CBwtveelAdI_I;TH?Qc82=4U`ngSN8g`+_YdPoud3vu z3$$i)1=e}Cg4y*9FmnQzh!}y;{^Q&I|`@&B>H}QXe82@W&e$|is zQTVAhC;mSU|A0N%Ij^oa*a<}k`u*9EO<3DNszWz@w$A=q)#V>t$ zqi`M%-pvN@`2)PL-^Q7)pX1BfNk3N_=C3OKV&pN`Q(siyv4{D6ith|hJgZ6WaebSL znZub@_otu}|LCu{O_li|?5qraoPr5fJ|g_uu<8QB-wmsxV1GNo`>t%msg?M<{jL-~ z@${-;<$dyB4?S}|;Txf6ekXi8FmpQLJAs+U2|tWI&;;hDuoVk$b=xLy%W|fCYs&1k z-Yum3G%gwGS600DcHkaTHqNC#&&_#IuAs-*^+RrK3qrK3(nT%dy+Zl8yxBLEO^Nwi z!_|(PXF#YpU=K(PA8!^HAKHRaVF8+R{_wG6Q@jhcX5JO5R)Bx-E?!ls-houD8>m-5 zs3a6kAVu#0dxfVJ;OfP?q%9jUzmbDk&24JII%dUzq|Nh^qFJfUEJZ8xPnMo59qE+5 zYy(-@b=R;*w-qgAJ8Dk7bE6*}mvrve8Pn${vWuRa4vkilp4@|GJpCEFXzIwdR=aBM z-5ibLk+K;3L8S1vd+TyMU(-Kvr2jT@L;P9xqBm6qJjXix)}Q9Q z$0odS?fQ8IEc)BnOn=0oKYTmV=`lun`jY;L>4hWx(H=WJ>p(+}_cPQVZxEEleU8NW zBYuYc1@TobFBg)Wv5CSs!?VmobcNDjG*e z^^ToK(Rkh=MKpn+5B3mz;yoJmyM}Yp9tUi0^U-O=POdk8oU|6_BQ)>JF#2O!8M+*M%#W9dYvJA zyna6U1|_YpR3Cm~sjd#AQd{O7|zRuMMyeKb%v zJgKx|Ps{A-gwWg2!2C&*+a|yNtLLvIY@QF->lUqAu(s!((z^)Jq!E&~bLsI5)}8jl zF=ylcZ*jkK*@E?J@mE9W{BQ9$ZP~G#mR@+0>rq0SXA$a{x(xNp=Ul5m{eKbiTH5j@ z3l;}%`PXCUuMzcYmIIKsP_Z7@8*uGgzG}mX*Wa>zKgNm(AFhq=IOGRADf?+y5!S|X&LjaB|k%GowzI;4C`ob10@&KUVodYh;ZF6=at z&NhQjf2u4bg)Frwh==_j6)7EUcat<`AO&NVsU*zer4)EMaQ)vY#q@3B5M?6CX9dJN zW@$lx(})EnQ~o>hJZz)*+-QlSH=s=N1L%iHIR+e zavrWrh*{c7d~6=^k68u*?*q;L4~w)O?N1?g)&`zwC1H9!#*kW`Bvo{b!u|~WK~uVe z7!NLJ&+^|a^gE)G+=mvXgN$jQ^?#z!5fVx*Yz_Ee3-CT4I_Zz3Oz85s{^YuEbg{6F zfZa%}sXA7=pD4MmluXbI@sbA}71w2VK_;QMgpPzBfi8^*J;L=m*9!|re&9u_#5I@I zyNQc^jJEz41^Qi*NE=&*zdP{U6T~Flg7&_}Z?0>28@Gd}W&LX9dK5UA6qUGt3ECb( zxgECj@{z9$3-6ZD7sYSE?+hJ&jN3PEZ$ydFaTG06bUGhroL1s5`W7_t zfxmWuZk%@OAiJF2(9c{Cf^HU0YbkBy9~{okfgW5n^1U<*HwKL0d6i zl`?UU^KS{!0;eLW8KoO_QJ?;Rzwe>+;Qpr=n-{heG8ogH#W5YE=6Z%r8~H>kL0uQx zX(MLvh>7DUeGY!){aYb76)~Kff!_d%4Kz9tZJ&VWqA0*s3ghoNs2{_V%N#GvjP{hM z8$tUVS6NS5*h{Q#e@LC|{j$W_2o&2mULy&1Sbzl;8!Cx%ga z3A!0$lz?-P>tCUZE78wVVv#nXoQC@!gI0?{1Mr;S5kB{EJ64YIaGtt@*tm>K*`WDS zGM9Bg$E+f?^jM+;Pn57?;NT!x%#As02mEp1Ujba(p?^*%-6*G_tU#HIQirkxWd_Oy zltm~r>Ayxkmb9SBSD>8@y30z6=_#a%uETH48&$(ojTLySgm$5Z01oapE{@ZC6)MVFCblq`+|y&B~#QbOmDeDWqKA=iLb zI`D}OJbp59qLk2?q?tSg+@_7}K|k$We_+fN>{hh<6>u|>fV2uc3tm$ckrw)EQcAuB zJ${L?{+kSAT$AaYBVQx5urNJrek&*-_x%JC@6QPNO)Q4m>>aVWDx&WPqMI^0nY6@Rno+k8L4NX@D?#06GagCM_jCWhM#IG}4d0m&VGu_`5pg zdKrH6vNHCU*W=gxua^Fs@EEirR(QNvf>MZ5jM5wX%YXC2?|WmvHxe>}7?XT}(vLDX z2IuuUP}))A?M7n1d9*bfh1YLJ;dOab=8j!Y#$SG)*Xu-Chr;XlQQ~bG@q2L$&hPQU z>rRFb(n$UZ{C@=gKY|x_kzVjwG3F#w{9TFP&7_f@2boxe-#(NEJpT_o4Zh@hp6g)F z6N*PLcd(+@ zIr;(pgpM#PbFv7V!OmuPu?N}1>@`UtWl1{8DtV=lR3n`$T`c`sx=Xr8VNrM$^@{h*Gb#D+`pRwo`1o9J?GBIlgmgTn3lL<#2gi0aux;(pB&3bWL<^a9!oP z#&whHHrE}lzq$2pliTACxC`AS9_CSbay>ea(PQyAJcXWK&wS4k|0BTxSzv!PR=PnZ6CN2~IM4yeG6P0US76X=(0^-5g4bjh||Bk*9 zeLVUI7-0>{6N$$XKi>QLy)Ova|38Ghb=zAv%HX&5z4hf=H^23Vw`RR{>su9X^}RLp zRy+RJ^j5?Em-fHB|Ni}75HdldgvJf?VoS5-WR@-O~NdAjnK%Cq^epZ-tDGe+V+D7#(CWY@9>*fs1PZ{vA`{7E zGL6iE23btDkdq--JIP*h7P*XEPX0pvN^WFNKysdx^4Y`EG2~YABrL-V`ILM^ zexT_zi{?@zHBmdQp|#-QCfY^&>0COGE~ZQ9D)t!rfITjm*_G@j_Bs2GeZhWYAF{Wk zW$bObp1mODv3I0(>^;fIE@PKVD=e>_v77yO>?dZXr78f(&9tCeB3i zh@F%}FNL6QhDZ-AX+JdMLb8S|A=O~@YoikwS+O|BwaX$N_T+y<@jICRuw zh`t^e(9)ACe$>jWod;HjvLq3;7CK_e*HY z@1dQ)B@?I`R#^!xA0^|d3R?F^SgtfOiDr_iG>1$_v^taO$PB6_v#6fTrUo*Pn#lrc zA@gZI%_EDbjhqNOzmz)33ABuyM61auw4Urh=5af8>n`ZqQ)wI7LtDvi+CqLwd&wEJ zhwP)>dKtZ(UPS*uub`*V-SixK z0o_QCrzg-8=~ig??Q{p-Nq5mx>ACdR^f&Z8`dfNF{XM;q{*GQs|3t5(U(&DW*Yq2D z0Gg9B#w4a-YNlrfW@1KWhBmdqCRmu81y~UFfv^z$mbqXhJj}~{%nohqXK75y;5*X4 zumbv57N%FRLV7hTqSvrux`(Og2W&b0kR3xe)9>g>^ar|y{z$K7CGS^afT= zZ)6qpCRRyrW>xeSR!whZ5qdkTrFXD8dMB%=cd-WgH`Yk+W=-_(teM`!TIjv3mEOnN z=s#EoeSme+f6@n8H+_ip(1%$seT4PVM_E68j1ADo*&yqpPp~2SBpXMcV#D-lHl9Ai zCeUZuMEV??M4xAq=?iQMeUVM2FCjDgGMi3cVKeBf4Bjl8MPFmH>FaC`eS^)VZ?bvx z7xa6&pUtOlu?6&PwvfKV7SVUvV)`CiLjTQ{()ZaidK;^u_p^5T5nDn3hpi-khAqE> z&L>yWg+xmpBzp2N$t3r}s@+d=VB2%a1MoZVLni+Y*lWatIRZov?cE!LN9q zRFjWj3*La=upj=&Tkr|qCSmxdR9_gNkPrZzw)ee4~_H+m>>T}h9Q2#}Ji~3&mW9paG@2Edf|BzOh)|_^J+GT0irM;f+ zNMDe?Dt%M>wHf}5wHZ%jypr)=#^;&yGcU+|FZ1)vXjVp+B`c6sk=2~lpEWgWVbPwdv#CiUekS``$|vs z+4`yae;7Iq;|;S7%MH&ML&md=zcc>Hc%$*}#z&1Wnj}-3Y0xy?berjZ(-WqbP4An& zFh$MjW`o&j4x8)Do#tWlO!E@+apsfEd(3B>FEanxd{bUw-rBsY@@~t!Kkv!BSMuJ= zZ^*ygLM@Xmdo91QTxz-6a+~FT%QKeOEbm*sutcpHR+H6ZEwUX4wm4ThPjKGmyx;kxi@CPDUUt3f`posCI}P68X7|c+U3x-gBkrZO$w z4Bj1lIQV>UfAD`ozR;%7uF$VSzYqO6^i4rm!Gwa@1ls1?4 zm!4aCVd-V1SCxKI`a_vimRYu-?AWr~%I+!Gl+P^xTZO-3Q^kFi<&~Q&->e$0npSmj z)rVD|R~@KUR!^(Gtwvw7u;y=(s>pM-)wSE|bahMW9_WY>)o#Z?fR;_qx*N= zU-Y#0Z0h+_&kwy)@2cK=dmrz8zAxCfrSHnVoBH1A*Y|hzU)cYC|EK*w4yXrq4?H_q zGFUg*HTd#S`B2l)uAvvk`NkEETQzRuxGm$Z9CzKgJBKTV>xZ`tKQP`ke#iKyC+H_^ zo^bC(-^6_r&z<=Eq`XPqNhOo&CY?3u-N|*6&zb!1DYK`1J9Wa;^QXC{ZJ74b^qT4C zP5*F4=Zte_{A*_K%s~m-TYxX;HYUi9j=j}Nk&G}~T)VY_> z{cc|0ymRKgG4I29U(Gu(KW%>d{Gs_X<}aRq?fg6E|8xG+3o;j&7PuD_Er={=Uof;_ z#)3;0e7kUH;cphcxA5bIUoUE1bort$7Pl?lxA@5=!6koPs$81AG;gVIY0=U}ORrga z%hJ1-sg_M$_So{S<=dCvb&UC#zGH4!k-oyX;@A}%R-CrtS1bOt(z$ZW%G*}He5~i# zoyXp|imaNkYS*gQR@ba($XUsx^i+zBN^AI@c^(bNQO+ zT7Sy_>eddfUAT7V+OyVPxb~{GPp$pxxbeqbb==LzJ-x1R-RgB))}68L{B@VE`^&l; z*4@4Ck##?=Pg`$X?^$2EKC-@Z{fhNxt$$^McSFsF{tXj1%-!(&4S(72_=XoYe6!KK zv1nt<#)%u}ZM=2kJsTf8e(3l|kN@C=4JW*M;))aB*`(Rz*c9GWyJ_5}UvB#M=53p= z-+b@phc`d{3+*o!{Nm=56ele_>4q)YTTa|^?Us+W4sM;ib@tZ9TW{QY=hpkTKDzbU zt*>r~!r6 z?JV2*+nu-Ue1Dg9SKF>>yVmU5v+J5&_wIUb*Jrz3ySsN!+P!D@MZ2%x{owA`_o()S z_eAz|?pe3z;yoXp8aTE8)YYe6bLtzXDNeJUR(0B}(-xh!=d{0`_OH`zr3nk zuRQ&oy(N3+?A^Whn!We#eRl7wd*9ys(cUli{;*Hlm$^^B&$`dMuW(=GzJ`4r`v&)| z*>~R=_A~syeEm$GjbO0Tgg!;Tga?cqSEbgi^;c@W74)L$&EI}Yzl>%DC|1-*zDL!q z>`C~=1xQs?SJc!*^p$#(NsS*yZ*@&gAQ)7tRVr~URR#lrpchvvhs&7rMBW=|2B!6B z-wouc)ZXZiUUjD05Y$Wm?#VP~vJ7LU+g0ml*zq?@Z+nP=1TODg@{ z+2pj<+KD9NAzdu+s5%-<^ub6?jmc~>9Whe*rmY6MmP*P@^AX}li|i+AS*D~gT5?84 zVz^$&0wNYwNprT6I8-q;231S*$Y$~z4j<`dTcvpIpwtC_9IN=Fo*GO%H5X4klxS^c z0=@uIM-m=Lz~>|WOTeYS#o#Jwc7FmV>5mEM(iG^Ii8f}CRpjppOyjkan5I0X&?lan zj?9yIigS=cnSjqC-ATCgV*)-C_3w#~OxhEJtE5RM3QReb*prFcf=Y?n7bj{@-kcoQ zO^Mp=h{cjr*qMO0ksUHTdSF!Ry%f@sX#L7W{e_4bulM?!2nPKFS^_Z1Q+i2Y$5Kv%VD*Wd4BnxKD{vM;~NqJUf>c!t~~i^zJgy5vOU-2xrJ@z7S) zM1TkcyRyQhHsGhR!USYh-k`};QCU$_jlp<502@^LNHYtTFkGpOk$_ENROi!0)--O| z!Tf!Z)oYtqbnev@2USD!%EpyA%JV4gt82GYTH5J$yE5F=UFWBlS%cX^*6G9q@)D7;NP>yc&3$dWI$Pi$pqXY<{V&C z!m6v98PMm{X7e-FOsk&L9qQfO-?lYmDO|j$U|`Fv^1`V*rqwPkQP$Ac<}NRue0=}h zv&J{n8XH>8H49D|=-GX2tHW7?Z8l8q4Vnj8>?7!pCe_W)b{%~^scC)^4Qfr*FENa` zEL)%{)Y5En0q06m68(|WB>`WI6%7IB^b%4o*Iyd1FW_8uWOyy2v4=2X&KGyGzab}s zTrlQ1!4#1S2S^PXyCYyvr;@9A28@QP0tKCL=#2D-8@zHDIjQdkq)8TM9`pl7`&N-Ch-Udy;IYtOS7k5s{ zn6$^G>|otM&yl74??tUXF6D9D=>AkyKqVpE?{eX$*3O3eM-z3l9*E6n8#uTW`N5;Z zM@n+@k)d{03P&Femh`Jp94_c5Y$9YF96krxOFsKDoO3>h!-~yLLb+X%p-U6cr4qMd zavL*X31N>&1eT;tNCSs+EB2;nEBRELuwwF4)5&W2DL$jZiplU zFy&TEhEF1=r;LEhoD8o;W(1Z=#tK>4I9y2vgwYrYd18Y9iHDRn^?oRT??e z$c>sxMRzQ|U~1vC9n*aMT{cAoOD}WltLOBDIyVhl8yYNHpY|=ABa2xzwLa%W&E$(# z_3T;QmYuIHd$(m;N$Hdg-E+^KoMF~(&V#w*9<99~m~nnqP8R$^88^YzkVC=M&kJe_ zoQ0;7;j_slDR9o$GJI(q4sQixOu)O5TjsMOx5Ifwu3t+-LZ5T^r=W?1{4ViBL;h!! zd5+qm=NtufZZ8ZzeiQ_XbI|kPAox$CIH=OdXUJV0b56vi7AjQ^LZDZxm0ZK}%Y-Ac zAJ&F06+Vgh$>5yqheC~BK>5$nOohm!#&^XtF|#DDqSIooiKuyt;>Fk{1PtpGguMqm zCGIx~o0IB09R;?2cJF}+aV`Hd7&ECbc;LJL9B*(4*PqV|PHPh!IGg-VaE=V;oF>DU za=0wfG!Ns1A3-q#qevdNVU;wGQ#wvwnc(bqoS>sgn6Uane}N~jK+6HFS3wrYMC^Ft zlm#ZV0k+DDJ3rB9FvT#9(S3>;`bkV0M_HYG^w=!qljDR4>`RWR0nkoiFJ$P_dpML+ zq(MX}GIY|%VpM?UNn8TOsH9q~WW}^Qr(+DRlDbNGZQH2YDrxek99kowT(iqgfI;34{3E2sw(0h%twg0E~T`0O7Xb;y!a2}~j?57*2&ObyJ; zW(N#~7x)Hwf`P{#hsyfEmh6Bb*O~ibxV_P{(C*W%4U`9sn={-2!-@KUuKaLPf7t_u zG7Y5#ZpjphT&OKJ|GJ_y%WW&pzcjx%lfD+0deD?h$rVC+CX2DmUdt&Rfi}iiFkc+1 zmb$S*7$0LX#%Lc)mvKqr^sZEo)>rJ#mNt)S3>lA365igp=}*R_gcU{u@yLsKSO zPYFlH>L|K0>974{r|jFLq{KYRV)g*mg^(4ZL7oLdhJ~BYL-b$@E`P9?v{G7Envy9> zq9=~hdh)pG`-X5=lRaCTuL}(x`1U6;icuaE2IZ&4D1T`}o29_nHRKDdtum}m5yodk z;`?JF)qdkoVOvJ0Mz0?O$9HOfj6|J3i5j2%BJ4HVfWF4T4*|=Tu%JO@%#q@mGB4;9 z(Q`KE4v5~t%$2yn|0|6FmCCF)8H~6)v!a)Xac|Q!1&Y{TYiNT+08zcT;0oL9|YboCM`5I$ISYa*fNS zQESo5s(MHdV?Wmnt6spANR8;B)L+bYQrTqGkRRQHl0e`eBA1^ zrcsSYdjdTnn~zeMlH2??#xyK1%bXRRoL?b(RaS(L`?Veo$2^*W3?qFc)pQ?t3bQS+ zf{c`LZ{le2N|Bo*L@_m(KWcQPv=s3IXUn+rIA^Zl?AhdE!P#}tHK23?-pyl|M1AhX z%k>wL-{U#ZAo`h@nS8!OjC$Y>%j({s7U>d94j9g$iX82%sY0b@L%yEoq-W-;wOO>Z z$9zQd82!v+(%C3=W??a2m7&(Qgm1lR?%a{1o?eWNXMsM3SF6XK4EXx-tdE?b2v+dK zMBL$bFnad9Q>TQ&({@asy?a_=c-r=9&9j@Fp7sUJ&2yTZ&enyR)|K1aT248(y?xcT zmgZAdwHH*+SQTmBytt`m#;V%LiOXAg#!U1Hp5lEXL-=#OrsSS~76ENi6aPFe?2Xa< zBI9)-=6^BfoyW^h2)>Zv+^>`2shOXTMQkj?XJ8GV$HpayjXxIFn!^zrW0wkVZ`5-l zI+mZCj-5F2bC?Se9n0`pF3~z_u%Ka^Q-02B;CwAN<;Sa{(K}o=M5y5csipq(i8i^ThgLYGKSLzh(YUAD=Lo!KN@G&i!}!dHgPMpijBolyyMS80r8p}Ch@ z5t01J;SErBPf;sU3emPuxBZykP7F`oIm_PZpcf_*d3R$MQ*rd4whnvs+-@YiyEYA1 z)yt_p#p`hPRMg(naDQ6b%nKLw>{-*v($&et-X*56=^1)zc1H2k%|mm~7}7u=a__Vo z*?d2bAX2loB9(74n|N;A6Xd=##$ohg5f3SI+*IY7vukoNZerZ+i#ziROYGht>UA)b7J3)~0c<*F!X_695C;>(#e#;GFBN$-(DgAL8? zWkZF5!DfGC`9Se}zp_ddRIVMyhN7~;!a#qMUspWb6qr|_sHE?gO|9uJ4c8SKw84t5 zvdGkOOMc<=(`VUhL&c3n25q3cv&_;M@fsb4*y_w=+%R)BJXdto*rl#0937(HBxmQS zaHg8%L~xo5+KQRjDbIvohURjLVaM{eB0`CZ6?%ILEnh7;laFz*EcnmP)xu+7Q_DV5b%OlNdz1pKPdLwQ#wyK&U zRa4NjvB3CIz$(#YEmeLM>%lhJhH*i9;Eh;X#LZk04>rqAp7HFn;Ej0e5^>3S(a7QZ z_$^)BbJ$**3xj_;d`Fl?(UX|Pz49zh;j?HO$>pQNY;h=`#cRbZnlN9f@NVpd=W^!6 zd_5yau6k|*jRtPQF;S^es#?vLJh@wy$Ul#`?W)}Hq>kQMg$P}#&a1lv)`>D(%d&y- ztJ?4r;q062cQ${{kG024uIM~*9L-OSP~UGmes*rvqo zNv1=O47G(xVLBQtN9Rjre0kJ?)j3dm4))^nS&-p8dX?d`$@wX8E=w|e=_oi~E0f`q zuE3)PlXR%DP20Lss z(sNY$fZ@@iQaw}USc2vJ4OqrRLA=x6~G*`MO;LgZ#W=rYDOpDu?o2JUmFoX<`dV&sP$+S|(Z;`D^)25w? z5mYzv5xAw2#`YCW#e=2VfX|sR<4-3$+)LLkb`(!9wYPiv7q^$2wLz^&oheL>&8J4c z+fHw=ROIJZU+tg|H9}}{Gk8NKrc^fivP}-B{td~%j4%-;c}rxmQv_; zNUaR#T0w?SqMa#l&b2apGVK<+JRQV53tWqL@?hm=5-svy@Gc~afKRbSMYO9 z1y8h^ki(Wa1Wba*jag+8&-SiyCcQo_+m?gC%$KXp)O7^sTS_&?u<-#`ZJF^nUsr7| zc46f>a=r@YrDvJPmE@P_>w@_(o9>LJ@;er+a@svm@I`J$^L={9yqdkfb`gWP6-7;s zi_2#98O^0;kytf*HO@t?zP^C9-VUc4tJz{F;ZBSR>$ze*G!hp_zCI(CMT9hR=1<5X zei&2+LwU@wzsRlkHTn4pjRWb@5?{Jue7eC1(KKaDQReD0^dZAzvlZ5!z#cDSfdo&4GslQb7##+G2`HC zsOZAlYfs8?>PH$HJSzHW^n8;;^F{j%kE&$&hX>5XCIuTe^-1r6H(dT41s$FYW6k7t zF>5$;;HO$n$%h}VljvK?2{=rLsU(Jki!liw?nXi3+1;Q_93xPML)BPAeH;{E#DkT~ zkC+0xypW@D3sFTV#6xs5-+~ql@Zc8#Qc$G15BKKv@FX+?9kLlYkC1%g|m>;O-cd?_4@nwA9@7TB4=diDw2|lPzsYK!;*2ftFHt z0y+*=;~07q(5A{{3xx@2V;ssX(oh0AT=}7Bfn)e(44TBSF?N^tx+d|=cu%tZSqbQb zsw6bo{={Tm_Cum>q)x8OdsW1sYMCyH@!+k%7{%*SD4v~wwrdmp;mxji-L^g%%0{)^ zi#J61t|@sgEs5GKvy*Lnmw>jGxsmBWUA)Dsz}vaKJkxxT zTg8r6?zjX^H3+7Hf{kfqY|rIQo#Sd}uBgZ=C~-$J)hd0S-dsGnuA*CKn^;{rTpE2+ z?p>IfsnbP@_e=3HdNBOtg-+2PR|5ao_D&Z z#!=QC^mJCa+sy1Zja`UXT9!Kcg3Z~yZrqeDo%z1v-&fa}YUZ_cE^BaFxX)MseepTy zjumpA(Tnc{gF^7C#6nmivBI?i=uBN5$8`j{=GWIwD#<@qN@^0j6bnXjIhKjqJ^b{9MD2`3?Z#MbKBkuvwQI>e=!20ks9p=I2a@~V1ht_V zWiMTxfq1MhlxI-Zg?h1_TZQ}~Wlam~+Bf#Q+pr?cXQB)Qo2{8qjo1f+Mmj`TFRtWEPvJM3g`JNj{mePl;D2WhnP z=%;}Nio?S8Lqfih^08uojC@>1upP);2}Pq0atAsB|2SfXqs)`b;24pRhpB~KHBqT? zVZA*vz9KL=k7b10ikp1(rb?T_*XpRT7xgZ#p_RvW`J01Tj*38WvwKiex3a%_!B8<{ zjzDPoMEj7VD>H|MnI^l{S{vH)mpQ{b#)V2|b$W+7ix{h4Sij=97;n>WAjcNGkDo$Y zprni`5H7q@OeIoB*P7OrbpzqPT37U9cYnRJtyY_79UOnvh_NhKKcs0pVM6!j0c&JZ zt!i+6iz#5{I*rbP0_);(2mVBFTzX%LN$&+h9%cCc7+fVaFHF=HGR zV;ssNQc(grJpN(PUoYk$UYA=;9@VI%#51@%F7eFxmRS2NO{z~oCrn88nrwe!ye`J` zN}_ILYND>tBl0L~$xX3Q0vn-?WVpa4P8Es7T2V`Nu{Q{0s{Bu)^>(qZO@?w`SRQ}d zQn^nys_kB6RJkpcYjd3@*KQS2aWUrMOU!yc->YNh$v1zE{NhOvDO0S)@KiWgVO}u7 z4~6}8E;^sWLX^y~d#qu}hswL!2 zR!&%CD)$);d3q+P^D~{EoIQK6fk)%iJjRo{6DP9tHQRFC7QKZ!GgVpY^fX_M{g|K> zz1ZO7X@dfh$}MN;SR!8R=cr??$h=P=^MO%JV?t87q8I;i$JZ=K#3=+18NP$;Wbg}k z96aQem$X1W*>mP~Akcat-(oa;y@A?iuCBnlMJ|7BQAwnEL6y7Rmf@w7Q|NXIk;p`4v-ZtyOte$DCGwdA_AGSkqvuj(!$ub~{Tw zX_= z2Ttrr)D|+rYx6kdGW>-F!yHtAGbOxFf&Gih#D3};sLt3{N_Z62*jo;Q^NHb8!hMWN za0gC~tBGi1?wg>pk=mp0_s%ZLGwF#BOtapBckZ z;`_F^O%!Z-^q6MS8RQ|2*Vep!usq{Gb=NrK%P2?2vXe^Silb%n&8%G}J zaPgkSw}P*J6dYU2w@}JZ8s)7;kSvp8-BhSomrWoCAB~NCK+9ZwL30&cG?=7D9@H)*3WHaLKd}y=Q zmeYBQr=eUo&S1;wQRaJt+F7;=o29^=T>yr1VY3I84pT)mxJ_$KmqHH7U!tFwZqv?E zc|tZvz0cQBl*LX<;WjP_I=-unW(i8pu8LsZELeNPC=$@_wR~)R4nD@%;`}~J=_IVE zhnvU*F{!L_4o2|62`M;;cBO~WTdtVpdYmxz2(${|be@Pi0{9iIN$yxaMCB!Vrb(k2r>zO6vZI^C-t_G3XG(7kibj6?>JF?N(hsX!fL$D&3!6^F+OfFzT^NQZ>H)7`?T?ikn6?l(@z}w{P5z~XF zO1@#>5K_o4#Moqno1vn8F-@74tG3zF=_twwk9Z6^mdjc+zHwN=6u~Ah-8^+V9ZjQy zI3z*Y=*wKsLJJ@_!(13mE^(#Cl8I;{zTb>{!zSwMUf0pSuBUrlXUDph>Sn9fR~M>k zcY3-tZR`7oxAt|c>l-?`Kh!wCx_DYoxN&}@d5y?m%f0YNsK5W39B;Cwc_u#cOx+Ju~rE zc*HGgV-;NNrQzHiZw+HkJeRybHWtbIV`CG)uaZ)+;?|#!EqS+9ya#*m+%$-H(B=6> zd>b2I+d3W>6pY*qJge~wXW=4d4N@~ahq(VAk5@-=*hW9yUi#YMxNN}6mN*Z*@^ie# zd*#%_essZWe@dZhd|Z&`88VKaN`v74AhOYIqa=W@BUbX)`(sHJ`6vZ(n2Xp;Um}T3 zqFgMH9r6ZL)Y!eEv0+6|<1DOy%xw&JRobF=IJ#;}ry$$h)9=AI3m z-Uf?M)eU7`erHF-(*hHe-!Q`U3w=Aq1X;CKcjKC&DcgFqnk!;Gd4bFfc6#jrke2h;BtN zGG-_%LPmOUIJ*QWS_TgaUj=T_1MVh+DHg&CTAt@{Cqvd%ag1D}!&VU+;nYS2KTTm& zY$Rf8OnEZf5eP((n^Y&F)r8~WEOck`MfKdOHlHn=VbYl_Qhs%bPPWL8CkzMjE!qrC zsV~3G<#4B&wbX5*wsQB^ai0PD3bGAsxWv`PB<>U;aRSbB4gwBq8{yi`ho}X(C!pPM z<|(N{cIyg^sD|gZ?m z;Vf|EXaaS`741%ruZ$TVU6ZgZ^~(}aF*X_6WS612d=#;|e7D_BK?%`@uu^jEM!7a9 zk*p2>cN3>XJ}mMX$g2$tZIs;D3PK=`fxls{0Riytal+db9LY0lBC#U&#Hs^wMp{m6 z!O3{r)M1f8BSl-ir=~6J?Jk)+ojS`Z4J~c(!s7lWl5aID#~1e2g^Ut2wKRp?)ediG zQQ@FQqshCfdAJ}HEH!9D4aJ?qtaoA{opx{RIV3GNu(8t=s_P16Td{(a>GoI)T)DbX zZMdS{CB}z;c-|19upGm2%rUGnGl=mqmUDl3;>1HThe`ht+v|&shvp5V*>Ogt#0VZc znQi0ogUFxi)!5>R*S(OXl%U4B@R)O?_swMmd2Gm1Q<&zt@=BKLvD(V@R#Rcfqsb2R zHBt{Pr2f{xz95T!Slr@r*-Y+$Udi=DynU>2%6S@zXKBao#Yr319kwHCFAPSXJd)mU z%6=x!5gir3{Y0;{FeQFlblCn`b?Oq(?|uRi$cGW@b)YJEzDvkQ&q^){M(}njw0l0E zdkbHcLk^)*NO>T+eU#?{wFVQ9-{Wzw&}E6uxMG2Bq^8eLCG0M876vns`V4d#nS_0% z;*xJr@ag!%oO8M+G=Jg^T+IvZ6@79lHtrT9H<*X*`!Z;^re3U>Y8~0mV&E$<6FRIP zqu{>QUxeh!aPFaUI6TyKd~^aTq+f);yQ?b|~ z#j!}YGY(uZei zbZT07n?swGHH46iDzla2({zf~i%bryZihs>12w)(35yVw_Up8ML#{{jYMw7007qkS z^cv3P=udK?Kdd1p`oqplzRZB$#9@K_N4(n{yoBuKcwt+n^wJJ>rixWRSDdaXNz(gfxEWr<8(yq=)ufN9PHsz?!3e8hxWvO_XE75I6hbavR(1x;oIAz5evC;JCYo-#j zsnB${K_E-B3^rZ#c&gQTy;`l;tBZcZ=_oK2Tt83d52_P%6mVg$WOyU?8zt-WXh?=f zVsBN-^*O)D@H*ZOD1o?{sX+?U4Dk5z>$_;;GX^0T$>&QGF!HV$?LXId7Z#47i zf~k=4R!cc-ld~vKpNAt6)3R)t*~toy80hlw=|nd{YoI3(^eV%~EJnU6HgfrNCjl24m&4&pwkK{1N# zS->2gG;-U*DH4WIJL&)dt#EGV#k*J);;bKe_ZB=j$V=dm-CIZqB6B2M ztN7Y0-?xRV88`+q|J(9%Uqh!`oo~|z)JjFJE?4Vm@Ygh89WPsvuFOme=qz~(U0I&F z1iNpY4St3L>rX0lISOsrUV|6Dt4XWON=r9WV~#m&t!uW`+T2#9LZ4-{Y7F^>l0lzU zn5Xer^;&pnWx#wRFwf;@kU;8_xd^_Q1L`W$Es|Jc&XO+q4O8zzWw#~KCW5m&dXOY?MLfWE}4X z&X616&9C{XWw4i{&YVhl&6lr_BFIdhEd^ZsF+_j*h_dLc*ej56>SRe5qo?AZ?<-{| zbmE1^L%QurZn+)(#$xPMVGt&PLF(q)#JCZ&9Wrhn@mGrTN)H`(7zebum7m~CHrf(YMuq2d7;;y(VWpC3OH3+-dzL}zQfI%QUVMtuhoQ{KU5dqqL6A<~e) zy#RyzH*J7@& ztY}t5yg0&^AHT+DOq^cpb>l6;h?}#CmisO_5`2_upD9yC^ZMwBI1FrB*j*9bR_>Wg z8BqPb4cV!zyzmXL)Gge)cW;vd$H>Hw2YY4e?AcRau~mEV*$rkh^(kTmffqL<|rvJF3a#17#8Q)%nbvyyoR^wIy2PPXV#;r$x{&!& zSCI)+k6z+Ok$RrD0mw8R&1G zGw<~pqcuA`Sluu;S5hwQu9*C;!QbL9*)i`rv ztdr>IgMuOXY{Z3T{|;NU#`%*6!zK9>X<@v)lX_@vx%I$5L`F+sEp3NCwo~wA4Y@bL zt8Z{#jln15{XDM!>mXfUOWP4CO$Gh21RYa-+zh4+hx^@pm=x?O``|+MB5Ep|@JNkV zuY%>|j)pMma4oofWvA;DI_(ym4IMp;vflmp!D#P(t0RjZNAaTo7I02?^o?+L*lfUi zO-f~s*JANyXQml-26MRkhM~cHH(sYGre{a1^tMdMskq|D^>Ufbt15}^q7P;2)A^n1 z&ge8tFk6A4nv3iPydj}6re&DYb22k?^rkc;4pnm)>_z67^j;yP84h*)THipw7fNSg|FKD7U z3EXi;C9d1VwK!{rpOq@k6p{Uj`!EJ*(9!-xT!|)82RA47UL}k?+nb7b^mB(DtE60; zjON0zIP^OOHTv?Rweh#IVwA?IAi!=*0$YAYw2IqF+~+YXe=}Mo-k4@1Ka70B)&Tnu z_sI`FArPmVG%HiWScHa&z%ia;xXb9zvzg6ig;JTOFy*UlfpE~3mXV(^{J1}C)m~~2 zR}4OE&?=fg3%k{tWjVHNZKl}5+PGxko z&kR-TS&=T4ZP2(9Gr0*Xws@WW++ z;v1SFFMRzzVhn194~(c6+Al~0MR|1TFZ(+8tzxWiKQ4~>)j-!-YjJVmisRPaw|e7- zHRWzr=~uU{9h!FPM0HFy3cc#VZBvIgV@pj51m`mPQO34)>yOJ;AG`dN-5H>CIw+0z z6G3U@xS)$&xZVRwV?9W?#qxeZ97>xsN(LiV1%Uo2L&fbdXCZn8^qF^F^{R#Pk(&;@ zoagF{6b)JA>o^w)w)vgbxO{$BuQyZhI5k&JX!O*3sb2F=s#!^#*U9nYe%oWfuL3mW zZrUi{UDiN8_r;X>u{YMEI$UnG_G=*dwFT}U5_O#;W0+iM=3I{3;a_c0C=m{#&U{6TRkKV&u; zlOq0+nYk+#kjB1nAo~=j;q0WE{-W-Ah(-B|F*lza%>#FcP;}yq)^6A!uB$jd4+@Mb z_(_qY?1kXzB!+NV`R5lz(9T^neu{`PHHt_GpkVF z8UwumR8BamGMkS=SWr1R8ocr78*C8BFWA)KsrV^_#Ocj#%QYCCOU{VmC`%sY#m*Sv z>~YBGxFgC3XJe|X{C32ctcRaFLXY#?q=DgM+wBcY+jnciepS!x(t!edv6+FwHGY}G z6J|VnD8eyfFZ)HS9T>sCkj?`}5iWZv9tNgc&^F;>Br%g&S5Q1h@~3_RLwo~ct`7(C z-kVYv+Ith&K*S!Tc8a1lu)q-*8!cl`7gCkD8DK4ZuTzD%_jDCD7Z+(#KmIZInBiY7OnerE>WlnoR0>gk^cu&=0gTQ7 zMn2(Xz%@@sQzBm}f~eT08bth3btN1QzmeyTjkLUA{`{~dctYE>U$px>=hjV~orxf- zVqVphEfWhW=A3*_G1k@T(l5f*c^Y$TgR!<@_^f$-$4@DqJNNY(uccw}FWQFpt!Sj0 z=Yb9lNh)tq%&!*bOsn`i_@JT{dhU=@s^e!sgSHceC&K0QYj6@gKB0m1JSoVJ0pfcP zyz+aS*!vvM7&kM#K`n3mi*K*Ffgdu^7ds*d+Y=*I>m5VkV;9QG1=`!)8FAO)8&B~A z25@8$orE)k8pZ{p-(=@5pWoRR_f7IDCN_HNe3WKqz8_cK;P#PE6>5=%h3)6(Zl-K; zK12%|E1M;;nF(}1_#{yF*U6j4huE)V)Z%7_`v7Mviig(^9DF`#^@NDcUN=?Kv}{YO z6h8xg3_ic2@x(<9hd(@2)`lxUa^`+%FH$mUF3^_r7Z&xqjfKt) zR)<=aWM9HaKi?~KS1X`QJoKf`MZ*Gg>sfUDC&S^YDq=Sc}>nPRM} zwe-v?D(u11pEG0Hs;PrHnIB>DIflstruczS(QneT)Rl8uCND38{p70Q*X(U!(VLFe zrt`C@V#df!mmZOQe}S56tNtbUT%%(5agBD}LB{^YxUoMc)!0ugkKkzZ^8QOM!ATik z!|GoPn)Y(}lfS73*RHanLVP}rr`x=kcy%R?C&8Ydu?{{Tor0oxI-*pm%o!pVl@}_s z;&hKw^iFjrJ4de%7TcDD@(aV}pG`c`Gfqy+&drEEU`=z2Bvc@feYvGnPg|>*zPRur zmgDmuL86Incsko?$WSt0k!+_FdokLj1RMXy@NoQNjvK0cj04|Ck2^*>_&esr=N}TZ z7;VLNXJl)(=^bfZ>=zoXH)NUXGZzGn6GIXAk~DWfHzzyaRLz*yt9j95QPybexf3)2 zSNdeF-Deq>X~{N6FPF{T6YG`PnvCcRmQ2rBI)EKQtO@ht_oH?Y5D;JP2-DpNA?1u!#&K!p3vm&F=0Un3$@o3p>(5u&gDOI8Z5P&9cEpAdYU2RG0{9{ zp4OVHigZSwJ%aOL<@2Q8#t3*c9zVdQ5-5YaB{Nu;D{DEWCGRkYb^-rO$rGhA>dl6E zS$RgCDlZf=WqXTiJ!Y>V&)_hq)v62)j@ih(BzdHiqNd^lTcn~;+jqio&A!35kj`l| zxv3*3L$Amv?HRO3x%-cIPsd3wf-jegn1!c?#bSNjq#cgc{(~Gcvbfje`3(idmct}` zR~~&1InN(<+HdGsn6A_W!#|Pq{f~0V!4e~Vo%=YUk#2nVhkrQ+Sqf!!#EipLQv4kJ z{`stNUUjDPC=$na+)nTK%TXkc<#?Lih?#`X!=q0ehdIVn#uu36uu8l;bx^WjiWu4H znJ$mcX0>UPpNvSRjbS1V{m6vX;|w@%C?8iQv7CIbY1Bs}#!wN5er>{ApjXJ$=JDx2 zFpd^3=VNHfv5+3qJOq(J9;N?|Io!7~f_gsCVWHfNfg{3~e;vbGY#BAscfS8#*7WQN z;4g9f5A#)=BOXJ9mPfxyb#XDmRAO-b!JbNh1jQN2QIb-(`Fo%CBvM{m=OPEG+Ia&-x$B6QpEV~cs zq^-a`_MxOv{6Ff0N^=uQyyVxBQt;;sC)`82;-Jqe<>PGYx4CxV*^h^TuUIb~gWvI? zs<<{Db$5*Q(ox(_mqpJP9G$v%;HTE>Wi&&HPdtqt#Nk(ar7r~bvAxSjy4stX7?bBD zbsa!b{nX0ukKe{c;;_pEN4f$m2A#cz&j&u-(XxAByiNu1bkw*q`$Q zrz3fnvVf7`BpG))?Yho|8Dn-ldIDmY=J~Uff^T>(;5AsKMluLaPxw71vs!kIYVUkg#G;4;UOP4c^5iSI)cO!F8_B?a34|Hs^W2R3$9`Qy6psaUck ztM}fki)2Z*+( zWeH0QED%Tm63EEk&$;iZc*au#zdwF~8IPWx-#ho-bImhL0_qk>j2$8;tg{#y+0IS}2jE1BHAA#>g~+7I2(w&ddwDKCt) zEw9Z3KUvGM!OPILJRfXtsq>MuG&m)ND%^XfCZ}ZOt>aaJg8)Wk^-FbuVcbETNMnI3tg zTrHB9oBUxi#cPuCVv`?VS!(jvt{xZp>-lPnhcNS0^@kRiyExlTcvOY9*-sPP5v!n> z2Y2L}&}?pLJjsiMb;Bmdc_6Zu5;Wc94C9Ii zvb=)EW53^V%jQ(imTP8r9$yn=d$fHAr{f!E^YNMUM!L5z`h2+~=hr_mv%5!NsgT8` z4^x*t^}=TMLf7!2myNG~&C%h(eOFgC#xAz&ja$QOZ`?C?Y+cIP7k5|3ob1xTKs;PF zIE-$brq}44Ii@5}0_dON^rLSKMNlPs?(Z4JS4y3pzjYv_mOlJ94hEiWaP`@C*FzKV4)D6eJW@k%MIK(Uz|#Jtjx-41_e2zt%Ugc{8WDCx8#q z1o#lIXrkq2+7OHqN_+uZk$?C!i88{-z71)Mq0@GATNX@y)m)j3`llNfoH8D3B*2_D zU&fQ*v+L#FVfbF#@*PX&CT?G4*Pdc?sM(YDKA+CyHRSs2XE8d$^S|im2q;zkI|kX> z)8KVJqbrS$6Hm=uIwDIN!tBu7E9SBl4m%fug}V+*b9(zZSV?WO`jV_9d4AV%*-bd3 zM3D8Rsu&(KV6Q5L{hG6BL#Uazha-ju9~T9Cck%--En%c*Bho~u>)|@_cFga@%K1%- z(vctRBDn{m6gS`<5CMo9{SU3k&E(Aw#q2I=hL2P4LeEoyf0p_lG7;St06iC)Z4c)f zO%MH@pL$a~rt}E%g!4XyA3E_b>~89I*bxmV6Z?)_kVm^i3bZzw9g3}1hwR_Otar3N zXbLsZ>Ww5%Fe;u~MCbgZe0QF0DqY=tJzf0`gzKv~vUTyO8)@7c|6R&SMPAZPxX|dS zjR+>y)#p}Axe}Mo)i)C^TfW4gtb5tkJjPoTQnLrDgZqXxh!RFNZiJ1!>h?|7U%|(-`Dz-EluJrEn{>d4t2iS((~z#RAq@Kp z?m?a|CdCsqVv^OCE*nI8Xd$&QGvbwVNsnCQezPs<{|hf${Zn>ghW%q@ZvHZJjr<;dy@vcfsvj=p#J#(Dy`p$o+x2%fuWyw*ql7S1cZhWVoB4Ap ziu)QfD6Zw#-zHyQMuw({v!Gs{U2lX(fKL4Jm>>lOab`fRPPT_)S8>xY7(;i-!am#Q z*d{D~tl`f%CGo!gb2cXl`@*)$1vma;rB7fLrwnE4W4CId@vndkLd6==_y|@eyOpr` z&uE;{al94gSwro!Z$axbA>vg=x(iuDv%3E(X4)ye0T!m5q2$6-K~p&rT1aW~qmx7C zR(bzZ4YpId1N0o&5y@}Ppk^obo18z0HNZJj$zCQalMv<#J9G7+oUY5k7WQu^{`_}# zI*^Y@{Qdn7cy-_fAZ+CS@)&;tBa|>SfoLa^Iiy?<@`%v+NyLyi+NP82Y^wLbrhb7H z_YP033o%wcI9}M5Rh47<3)c59#OM0AOzl3x*wD_t+O<)yYiMG@WXaBz`?d_Jd)LH^ zTl@W?@;5i`nV*l1rv;W~TjmF*{6qb=NPpBU;V>`HO(v_kjqCXlh*M!0_Cqt72!2&H zyO*=QPY;Z9SHvb5{Q2=r6NWWXhx}p#;1qWZ3U6RyOiDv;Lt#h0e}5xsnWGvmJ9zgx z!E=7!hrNqE3-+PBH#(`0nI+!A>D#vKtCi__r)Ix-*x@7R6dxBIEUC7>XbLi&!@rEopmkx#x{n+;753k7U zM-z^-bunbik3#-^8*>T7+b;Xg=2izEOXn3zD&mGj3-Gp*Vh^|e$q$ckaM|Do0A69H zJ(FrBz+LIb{e-gqZv=QD4as_-6FuB|X!GZp&(V6=Mozx#&T2d4RIRgE&E)wuu3edH z?~arI50u>}d{bek3cmn>m?C~yoN`KV3WXmsio@DTib;*AR;!iilKbnf9~cd6QzntG zFXa;(*>u37R$tMbI+g3*#zx9KS{XMnh(88)2Ue>4h_WV@%9NP&(zCV z*k|i~E}xm()<6)~ialZ#m6dv>SSg0n&D-+P=}&U{;o$+^=B<%+E;)7)04JcthLCB# z@rG@=?A9AM$%*HiZ{FUW+jh$qNX#dS?r*Fh1o`HV9aGQ3+tM=}C^-@>I##b)=)4L4~#DvpvkQK=G;wY}bBfK`A zjm%9+E^6V;ARLEEI)6jRx9m@wyhfctEx41m-tKNVFFkd6Yfc%$m(Sv7Kb36Z!)NfiliaSqNOS^?f+K;$ z4>9EH;kw-E1Z(t3zDAklr`d}*C7qD7u=s#r4H9BO>2t~wgCgVwytt!DF4PXsuADK( z%;Oi1#y3v5`%6^4IjS)Rquzmj*V<{Oaqe3)cBndb(MVs_;~y*Lhy0{mzccF54qP(pzcfkOpHzkYxmi-YYjl0Qr zOZ5+$x%h=LGMD*DGKsus(t(^GUVK{4BdcIENpeNwWMMAuqm8Z&sZBom3&58^yOW?MQh-#A|Z#7Y^DqyQ^+)Q-Yw6dVa)*C`XsIaD6O zdG|3oG5(BPWar*YpADzkgJzK`QS+zrv;2SNt2re1gp{ryM3nQSxADxKqJ}S$Bomsi z{6=}m$r8#B`912BUuy5o-0LZD8l=h&u(|nY0^oHiTti@-hJ?^V`+%4RqWm>{RAX&p z60RXPcw<~*S{jwr*TtTIe1I2?KV2mHD-_F+DuUth5NIo3U}KiQtU77m%O-!&cmXX_ z+r*I!iZ6TF|6n}=gaF#Gm3(ei174C`sB~_&F}PFS&b)f3LEB-}Bzj$NHj$+Veo75zWKk?b zq`5|w71Su;I;5$%D8l;B%Na+)X zgp(3Z{9jH~Mq)ZEwrxt+VDrHS&pRqu+FKH#XE`0k@L%TqKnx z9`3cw8vI6U+~i2c%-daspedfTMraS9HmE9WO~y>YqATFc2E5gN<2Gw|TnwZ{t=D|> zWqcpfG)=VCE8eE_TO0*vAnOd2(~eTf!Cu|GZ#Sb0=`u#|N3E=nq8=Mi5>Oq<*CP2- z=@f`8r*I;cIFBEXmCna_wdhaJMu1v$F9*Dr^C6a`9eOO$lJTK>4BgS$_T;P z(APFNODz}<+3VB za+SJ*o>GWc+KNRdoq*-b(+uqp$&Fg9MeI$Kf^HP>kYwA}9}U&N9$^n-5m2=n!tU#| zPo#U;f40fG2LI9a?RC{}^0C*S=3CNWka@5;zKk9UB4nXjEWNyWl{k`LC64?ozEzRe zP}nDY89W#TewtJq#6>{&W3>)AI`|DTsQGxcYO zHrcFZiw1`rvo>2stx{?%W{Z6j9iUAspAcSknz=di%q{k$EMAlXv37LWI&@mC4nI1q z=zZX|q8<9{uD&aIyK;mFntKv>!H$yJjRmHimpoHJa2)t)6kcxV zypuQBX$yiAjv;zUBMOi%{FLt(@sn(lKMFC711@9iHj<816hS_NbFw5QfzK1*2^$#2 zO)-cg7s_#}MO!Ye*~=cFQ3#TJ0J+;EZlyg{>-4)EKIYPkTI8L7>TNCgNu{hL4W5n7 zzV?b~q9+78zaY_5EID9?;EfP&R!BAc3r=Y?&9FLnuDhemec9x3!9L`&J`{2&0*no@v_%7#J&#j3_`X^j`1S6M4AW&k|2j#ulL|&`WBD4?@D3Ythf}%3lc%YE? z*i!S3T*!aGqW=IC`r0WZ_Tpz=fI_2ECfpl{0&zAKPbk@^RS6dD8O1C~{NpuukKKFk zSU0Z+$?;#K0+pI7N`VTg!W3knTw$uTNGdjs>Z(6Q9asmvr%RA?oW1$6IQ}n`1tjcA zHW&5GJ zcI!>EPHT_di-UNnbz||AEANY2Z9Q%~ir%Id$KqY4u+uJ;#Kjz7rl}$tR^kxf`EaD6 z2!Kjwfa>}bx#tkiI|0XP(k_VQvORrPc{J#t-?tcWkQ6QF&3`x zlDEv)gOEN<{E)<}5|}}}X2(iM-4Hn~Hf>x{{s{61J4Gb9UTfz^nH1@i4Zs+Q$unN# z!bsjkGFh)^kp-DAT;KZ0fxs$T3W-Av+oD0@Dwi;gQmiivswtD8HH&8@lV4pVzYtVCQM3`J4uUsfX=B*n^~%hWngp9F>aQ${CMT5*=M$l@D4MSug0` z>-`1kA+Y0#XTTaLD?Rpgeu0S)V5D%zh}Ix`T)4k6v1buvpbc{B+n~)S)r$g_Yq=S* zGsuB^I)fTUv;^Wn#OTI7$^3loiq5cK)m^LhJK|=>t!5{71mU@WFKyi|2+7G*??fcc zdWVXGjQx`p;*s0+!lyY6IqJT>!mgK$ffZE9Xc-$BvnLggUI9{?*tMO&qD0^Y=8(Ck zRxQ~*psGijHLB-EY6=sIf38Bcn4CI^>Oy0F`Ow;c7*?C)Q4*sGP<0NDm0Q$@8!ih( z6-X0sH+Z|Bf~{{&X1z`mJjhi;smcU>+uZ(V`*g}A`%El9c5nR>D_R~%^9yRI+R3*- zO_}yk8xoSc%v2CPbwaxUQZWa~UKeZ-7!i{&^vQfh43h>~q*|3JbqE6wlk ziR3YePNg$5$`;KLO;4>0kltKrpK?raid70U zjRRmH^_kHf-EN1+rq*`mdb&opZJf!CghiO6v08fK*kpKY&t1=%B7>nuH&drebLj?wv$1a&*yE#&kd6sy1Z^zW(>Wa=H-6Mf; z%~ZQ$jd1fxn1zU(o4I&&)qW$Ol){tGVFzZUKJhe{ zELMY7aIV=Ij{9Pr4#V1!?uG0Lmq&*U_E^$3Z3uX^?ji4I9AT7nG8M4fssVRzG;cI0 z4QuQZbY)2C8~X!W;@z0c;=3J@24+|F_$cBTOK9pm%aND~1KJkhg7 zA=m=4LiR9n&ah+G+2uLLmQ%A0j=WE6%;A(KccbHm&6+hW#(3;8VvUhCA>Uf$ z5d@X$fg^LDIdTM9ncn)Jn^VjlVA)nju3MABd(n}uaK-+Iwj8fvZb@SpKyy~=!bL1g zvlXp0tH;R%Iw>+8ZHc{8eB4LkPA}<;SGOp9m>U)Ir&Z9~GJ|*B&?0X6uEER)R@Kwc zC9?l+iYn<#r^~?tuplTqfb@t_#ED{L>70wNjuHD%Oab-ZY~8&Md)#Es_@-QLZBa1c zcwE>y+-dQ-JKcTuc0XN=m>d#X6_fpRtaPExr2maq6U;;dv4NnkXfm4@{V@ltBJ;$_nxUD)BizWL3h^UXYLi`Q5-3kBwKE*|{9&a}+ zWu<~*3o71eLkg9gIuks{gC8^IlLW@%Ha-@##LN4^HfhcT+9PWuOxiS{_p`g|S01;f z(GK1d(Ramsqb`T0oBhV*^#pDG2D8_#(+7eEf1i8F(c^cNJrQ)Z{jAri47vN=zFsGb z?C-R9e&4C`c><Lb2De0nJKbKC zdL~h^ONeZUr7D;=laJ;t6-zs~RZ~Z%9Gm52)1#sSe+2SFc zK*_`_cu#hBb0zUNIMH!dSc@=?GZb6h^}YS2xzU4t#V{Ej#&n8y(<2%6xgR%8qvgiEqY-jA8o#*$zkKO;~b9bO))grL)MzLyBSTzD^ZHopb zk{xI(jFeO38(dQ0X2B)s<7sOo=5S}i2H8Ezp=i_EI@zO|>Wg$4oDtuHIFLfEfqF_LTG%$|3Z?3|GdN3zflwS~1#@w$JVJX=cc&Dae(ov;+jMD1BU#ZC#$ z$__VCc7U*RbQtU5`Uvi~`pa?4&Z19}3Ag_Ss3sxsYj1-D1b&%ysL96Ce%{KUH@+vT+0`LjmSS2Mp`gT=Vg zs62W#w0(h}Ux%26F5wPvO~U~$A(ZA9Qp|`$mS`lYhVYXNVVE*(bj!=9=3l-hH4!oR zhJuyN`M#Z_I-7QIqIap(v%Y8TaC|&$Hunen`-|07Hkr+&a^}gGZCbi&L~n@cbk&2y zqep9kkho}gcw;)fwls1@N~g3&bS9f^%6njBY;j$!wu@FH4K4T#sOFxsMuiNyHW?8Z zUEMptOxe!F+57`s*PYct0O^Db5Q=9YkM5)l5Xn@*)*;(OGO*Z1%`A|WkqzPM79X~v z8&Vz!VKK(L%3!xj-RmkMK_5~kYShw(T~Xvlr*uWFL8cp_>9>%rd%V5Jj8Vf}tqwC6 zGhuX@cHF(p+SX~tmbT5c^_ltg*X7+PH0cnj4qOrEc4Q~xIZ?BnP1aAG14Z4+%()ZA z=U?zi60AA@$s}3X1TUbqP-#js!%o)-hW!bGbPw;Hn*vdk+j_jVhvBqbq>%tTo)a4&}}(H;e<^&rh+BR2Lin6cp=oB$So z>vM1?I8W+W83&PAYP6g2h?yXG`l3=qrXi6f`pl5FG?$2SHs=!N*g1!ZXd)qRDO9v| z&*?%%&A0)=MLRbnx)F_CHCzOexOYX4Ss`Bi5~EfGw$34P^&-bsXN_H5_{(3y<3?Bt zUNk|?`^{6Av30@{uobj@9Q;NayrGQ}6rrYV-vYX)ak#O+>`cfr*K(h6V0)^c4Z2%S z7(3Z>MoYN@U^Sg>lzj_t_Y@>0^t@F~8-j#8Nk|x?+*4Kz6Ybm;MW!eOyOG0noGIbO z{2s_np?fk(@rk2D#`|?YnxZ#y$b#TLrJnGE-Ks@t?!C$KpONuYyCq?VbXeIL;;cxjI zESdkwW;~{aYQQpFs;>fSrDzko#~9CbB_i<+(X(4Xk?HRKLUQEy0dpnk9-PS+7Ol;m z@r|9|2-LJEZf!CaO4Y0TvonBQQWiT!>5!i#*UcyXAU_ZBOY;ouY12qF?3by z0Hs3F95$eIV?HIiIPjK9l%^4@OtPP{18_zK{sd^Ba~AvH0=515kyGTpy|6Mx;0@uJ z@xiZ)LZBQwi|7~AbfguzL7?eJpoz|GN^|CrQh=~cNV<}Sca$q=d&%>KTFF8h;WC=3 zqf!pg+ZqW#Z$-9HPD&Mehny$0Q<|$r?$3?Q#2=n4bO~Zqlqtk+;b}r%4_FTSUdj=!QNCCB3F^tJ<^FY2rYQaH5BXiC{e)yG%JdxMv!D9O z^Nyi$$)adWcZ9q)qM|Ccoz3BRKDA~r%=U?~+5Rj}p(3i3m`qPyJ{6nXUFw@N{l#7| zxywsK>K)e%%K@eFt2g!^Sj;7-7o(HBUaj;_&?{_|=pB?}O2`tVtC9?>$mdg~EKRyD zNOvP8nV}A^f<1&4f_9dBTuEW^X6|M}1FNwlyR>=RHd;73)u~p07x3QY=(2Y@%!Bd4 zg;*`GwP^1&hIJ42A}Oxiuj=4fhHFQyB@h^j!XAVZ;Z0^=WXch z#ujQH4?U~asWd~otJ9a(us&qf;>;Cjr3^@oSW~=;%V+62jX}zctXxMUmU5_YmFG}l z-S(0@U(T$;zj#M+D~xhE`bVmhbmu9)**nM*fBWg2u*{ZJzHkPgYda;v4rz`L5`9tx zlBkA_L43uzP0DbhE|$o6!qpn@nJ!(Wu}D`dYv>95YFjVA>Q=gn$dw@Qm9|UI<@eGY zeVSkO;;KfhDBg>o;_|PNCr)|$U5LhzPqQ={hG1*IeErY!Si> zd$~ai$5z8rz|;GG&McB>?cO>(cR{%+O-V(y`3R8E4$gBxr$QhS@>0Qx4(s!rQNP4} z>}F#kqVpGSUOGGKe{DE!COvA6hD^@cvHQ*q_f?P(1>M9fKf83 z8(6VO5gZe~SWkS~PP+FahQLHjfo*qlKQb z(W-}g2dll)@!(`7GKNHAcXi+Q?ce`b!YAx!)J0ifl=GwBWmkO7%lBDr;Em;v$oNk>JBbZC+1K;lGpUu>}&S&iCp3!sR#VdnTu6p^{Gj09fmzzSt2O?T!EwV&O0utrG%B@-f0(6xAyqc?%PP$E2Rvlp_xGl}?Y{pswu--&iiT@J0zri%o&EM6W6ja|98 z^zvy3TBm+%&3OgZ8}l)Bs^l#nsEr>%aT@R$0Ty^609+6uq0sC}=Z9809#+J90^*9( zLya9So1}6;Rb%k_0;W?A5H;Ns-EBvRx(j9nkX*%qggMH**_46c~-Sk zTPxe5-X0y=))~#@Y^^*69!Mon5C~Ladm0CcFb_6Dqgc1`lh7yuPOOn6u>eO4h?6$t z*GXK}fTm+xvo43(rt9qN%;dYK=F;>12DbtKpB#4Nqp^Vyohu5>1uN^b(<8<8xs@2z z;OMkCbO*LhZHOkAuzt3CEFvE;$}Sei_8QgYt(DmeIF92z%E^a7-N>Om_-FDV$lb|H zN1Ipa^bWt;)~g-5#X|llriZR!U;_nvRxr}(4=E1?r>qe_l_IS`Nbow)sWA>HL`TqP zdPNFN$h9o}P|&2;uhn#E$gLMb+h#D^td_w4w9o5>)4Vr{FyoYJzfd;qd%4_0GY!FtgCZ_F8y3o*v z`(DifLolD(zv;o&q}IQ=bqiy82%Z*wx0{A+@WB#&e-L+rKIAq*Pw?Xb&3Te(LRhE~ z4hI_$U7XYvV1iP52|FMh!iNV&WQr}DFgq=J`vX%B5pIa)6_)Q0t+CUEm^s0j1f7y% zme*sjzAfp)No%=kI(p4V+D7vD793kn`wd>hf1i96{wm)C`Rn%b#H+KR_tMW3lsnh zK!8I1WAi$gdCP&;Dw#5-joZTc4ddOF!Ll`FE}9m${-!n4KN1*Pm;?1CD?+yn|cKk0--l26FmR>wC}xx-n*8kej8SM?Vs>kf%S~t`}is^MJ@#V;D3dt z%s1kkNiK>wU!ID8&1Xn0Wy_RWx`*qw$5!S<>^n<(DOX*Dcz@8Q@WN-vF@;Uueh$1S z$K4t$wZhy!@`w6&SGBI6$R>Z*@B~M;n86$Wseentv(_-v-R}R;+~TSaJJC#sdS$$jtni8Uhd-OAtH4q6aGMwP_G4lo%Irij#36GuV9}+g6c#yO-{U$oM;x}K%wKO7CsP*$0 z#Y&6GBGhrQQBs^6CqOZFf^0;lk2@H?TP8^#^Z@O`5`;F@p*_o}` zrLkeZy8h?Ph7vkmSGQFMw+#n|wrB6idYN_SEeq9a4-O9>ytX=UXydSnd9!ycbk1MB zX7s8(mEzW;1AAUEr^dEmioK{BvKBH6I}-lRk`+e=rNjs`={!ek86*v!jiE5;%9VwNss=$`YlMu3!h4x?qJSQN%9&<^Fz5WD>4q zjat3N>JM}}Ojf4>DN#9($v)B3e_)f(ll7T9GXdtXYb;v5q2KQfnbj;bRQ58ZPS-VL zLLR5JOFg#hf;}Ov!Qsj1v^s;`nCM)%ccDM6R-4RWM>vZ9v`f1;y6_$$edOpae-=ia zpcnw{Wvr`J(l_lDaLKz(p1j6dq6TAS?V$mufAqYWH3tS=zTpGo*@3Xp7#YfCt1(lD zJytf=F1mSa?AA*rC-8F=Ka=I1msCcsKRjCAdFeoA@2+_go(c>8T7$RZC+}t;YLeVy zbq`|Lzm-vc`;n2su0fF$nexJOQ5WB5J@(Rw-A9 z@-Z;ll5((Anp}<30Z3fbbfW}Dyf$Pn_ol}?(IVMlakxCCb^WP@;i$c5zC5)}^Ur+? z*?_vVX}T60J=&L;?M?@D&Q7hv#6k{xG4Ho#ri<>O$%Wj&!6;5&_>->%MPw`g6%@@h zZDq;#-V)AmzLSS(Me9nLS}qQ`=pY#laZW#$=k}tEf@H1Erf0YK)~@NWbavWwrD2b~ zzdJwPq19+@X1gP|Yr4F9AUHd^?~>UoO()r`>*jOo3#b&D@15|aYw;ep&W0Y>#(>>7 z*%w(H3yzhoy2I_-IrPBBZZYm zAuj3sE_RI==AuN!VTUAR>*G6n<+)I1D$c%Pzcx5pGWYeFeSo8bS+BbOpldNVx~<}w zzHG%5Xk9@tP2XB1?T@57Or0J!9*o?k`H{|&tgTPapI?-Bff?iG{pg^Zh?22OJwqOy zw?7jc&g(>TxNKVbr=zih7w#I!R^4{kSpkF2p|hlV*X`KkcO+ctiS>!m3rCYZi#gx) zSUOnn_wJs@z9oPA@7bqN%M2M1POg1;4mcX0AqK|}IH=WMu$r79gSCGwTz@`=Y=QU0 zN|}U1?KTN6i(_VexaanNm_`@V^qZy@UUlc2cenuikiQIhpukLoFc*;da&1=W08LXh zHzGz!2VqdiR*l+0g#u7GEWy1%%obf+OXsGOYe(aw3yI>6N_-@qU)w!#@o;T+&F)H{ zpj_CW>tVupDOvVyHklLSJ^6(ZSNE=L&ved?0)@$mqSZ6GE<3g{kzx}={Rfk&-Do|* zt_`e>*QOYYR)Up{Sh-%%OT7@=Rt|JDG0~{V0np=@X_vv08YIkyi3f zh+!)}n@*O675n|wF}L`Swx5$Eyj_-qNBtk=auB7~nBpjYH;dnW=~D10Nx|8gyV}1} z3TAMS-O;yx08;QXFIfs=bbB$nJRjZbK$Ji{By7ZII-$~(C}9btwnPe3iR@=gbfg^h z=QOc|vHo%P>d>W^?yvuaY1t)?sFB1<%v(vHELHHv^YO+wq>5U2Lm$oW4ty?f3hoZm z=c|xQ>BLS^3-|1$&j;~+ihut`z8*XAxsQK-QwHm?{1oK)@9-JE8)uR^Z3*_BoXI0R z62$K!_9y%b-z9|i02L*L`xJNMr$%@$dw>0#^7mg`<@@{DtN8b2{Qfii`}fJe{}BKE z``H8aZ(=?3EU$PH-@_+jh5ZB14A+Y`O}S99uMJwpO~>3!fZL1Zec9mCao@)J2RdyX zjKRUg6eFy_YWQtJLja;pPpza*U@$b`Dc3RBwax;Uvm@aWj;@3Q?yyp&G?)!tzJXM* z5-}Buk)(RbpLN0SUdW7B+|glsDCIIK^_mU`vl?ut!RL z0RkU|tmCk*ajZOtdrX;3M6TTszc`wn)qZ%^RA%fgJ1TrfdOdW^R8!oeirxX0?bx_ zmc`H#7z^AiDf{HX_;-z}G6VWOa&G=KA9w=+cl+ws)Q{19dE7TE z-RDeN#fsIwDf;+#J-v zU!at5&rO4WC-FY=*d~OJg8enZM-?J_Bd*Wj*^lz;4N^qzAFLw;lwF!r0xP1Vu&S(2BGr$c@)X z?~sreoMMf=qA@C4puL^)pZbfQtTQn#Fh{w^*B21cUczQ_*!y;lt=kb7#GZ-qoYmQ3 z)9Ot*y)6>4IfKE^2I7uH!WP;%6d#V^2x$k0VuIh{ujTtUWYUuZy;+;yE-Is1?PEco zD;jm7TBo20E~glcpHpp>7L>M*T28H!$aTuZ1HV*Ye@NaTm>})*^c<+pxZ*WVwZAtg z$W-_48e81iq*`NdQtb!@*#MCpw}&=VV{COyUTwPGyB@jh%UYXY`5 zMg_-ZH-Ntb!v+LOqCPTM%0bh@Wgo6nObW6DH`p|4x`ksEqi}=KtaBFJ!TAt!9@N^R zBUp25bxJ{Lu~(xV$89#+QQVS16NbmCRCE1daWJrq35PqK|TcE{Aw#3*QF*i!cmOIXQZgwSa z?7E@%madyp*cBG)W0T+pXhyPmAN6BFuMPiMwgOCmx33Zfp7iD zKRw9j3~+jq9c2EQ2`*)sU(mL`nLv_i>l^rzo4+$5{5$`K&lP`~pccLesmK(A%XbU! zT>b_}7|7Zm6fQV6q6AQ4iWnlDA*>ZN5p$B@K{!w&mCj`vcd#$!)a#wGzMwn4VJefE z+8{jdOz%2y5&a+NAI;B*e8 zbfI-qzTyBjYN+T(xEDKgpjdypcmIrkX3A46^miANTXN`t9T7sagO0ILSGp2Z3##y7 z#<_OhIk+ytpj?(P>}LnD$}S7nj9nhQ$|OD-ILgxXUHo#l&!_ zVojEq3%$H?oAB(5#$i#sN*o5)$od>y^8k;V;WN7o`zb-6u}>aQe2nYZZuS$l8}neJ z-V;{ji8~t}Ur95TDR=zjYu5n{z?#*oZJv-b61RrGbn*q^dEkT5U;`g`W@j8HU!Yy> z#@lHXlEMSa|5tb;r|S;`SS&2p}xTWONe2X zp+i)x#GD$tvyw!DI)1W3{VCQJ`ob5Fe(BNBwa0P3M#;K@PYEYVkBATZ-%GF^y*BZL zXBtm%CM%_6h1rpFgP%_OPl8|kLWp(M&kr5HHuR__ctUt8SbtjisQ*2}d;JeXh%#v7 z7uW_NPV3xwKN_@3@6|AX<*v~0V35!p;Ld&0osIV_O3&AXP6)pX;lZG1l>LTzA?ZYV zqBRMTk3Zk*Sf_mqf7oyS2mj$&xJz*lzl+~9t6k#X!D6`sJK-+O5%Tm!NvDG8q4$af z6l2Bzp*)(VI{_y&6T{jUH5xb_jmn%oYPUOV4pC!P`Jw%uHeVKG;jQ zpv%Kf%(YtbPFBj?j2EE&DQAmwIUgSy9}psPI>;Z$2T2Xc$n8hYc<#*PUK}_99PHh@ zU%k6Kv*i^#cf4{(zI$rc9f@c)s^Hq5%y?jU`=E2!t<-jC1VKbC%T(CL#C3OU9z8G> z>)mu@Ai8-e>gs*vKY2@8k3I(DKuPcIEWCj!8CKD)F|+&gDNFm2d|%MXhRCjZbe3s zP)&B1osog5Xkt3kleQvGZq2PJl{e(=Hj||TKzks88=}?OXrh+F4GwE|&79dBA6_%( z$B&V@xt}Mi0c(gE?VT#C#+{5H5wka)vbzQsikTfzM{w3<%7IR2d_$ZRo`SbONu5(9ZlaOP zJ8GrBUyu;Gw6~i5M!1gWK@4OJd}7DlM+T}#@7}uY?hA)TE_uU-h3mF;_iVp@e$DZ% zJ^8KIZ@rC*8wsz;YTz`4Ild*z(a>{*RyQ9CS!){1qea3BFF_+Qn(&^&x(ew&}YaoTv zFL9m0Rj3}lXWQm`j*X5UyJz#}H(WAW95`^p^vq2M2L}({G&6m}fq^?BEEi=eqf4jC zAXkJ5>-rs~9)r?m87Ox}l$nSvMeg?V%x@&dossxt3WrxmTQ0sNd^RMBmAbwM9Iw5k7eRs4F7NB4Y~B|?vP`gB6n zCkB(Z>s?NL{VPge)XNO4H-Fo0eLbxHvrb0`^tqtuVQ)fL?#Cc8;BAyoKya&&#AGvL z0z&UAJ=2NoT!#LhO{a^+4F0e;W#+QknM`ITi?8~}GU-|^o#By-JbOm*HPJwb-3m{| zhUgZ=vb&g`EF^r#4&b|l0CNjCYac`XP9byy71hFtQyd4ri zjU(=oBv;!J`;=pg$m~D@RPQ>oVb^4FC}v(9`iG>~7PXhs(Q%j0y1o8I`;~&J)E(N) zwqJI%+B+~2IatfTcUK|p9v!If+b}x1Ry}v#1GX!vhm;5C;+J?ol0+bc$(?|=!kQ+i zP25AmbEWAm0c^6i@ij2UGb){4kMch8`IsrSN-hG8xUDq~0f#)@=j<;Hx65#N}s7BhXLsciCYf9TVD zKyefllkA5?AZrCX`6QB88wRd5h5?c>N=aF$co7^OmktzO?tadXV<&?`dp1m2{r18u@0E-u1HsW z%Z30SYI7123*#2wB?B57Kffp{jU9mvTNN1WUeZh-1FNKZJDX?_aJ7qH*rWEC=`oy4-JWm4og z2hxEQ8wJCq3FDo^D5g&chLj(bCqNe2A6aCrPgRbp4z91xBVN_LwPMSy=`S;X#j7l! zx4>jIQ}$%0l8O1Tcy{Z=Ki(<0%&G6}-aJ#ukMufR1M91^Hy%QkHun3EL#mPfjlhWx(x4ZAYk>h)SbU+ng#5X10Mg zKDTtsCeuOVI-IFA?y_#WX{o#;%gVNxCbed$yl<+DZSdB=0Ig&5J>|M}d+n-}C%bR2 zY3jN~Hf+z?hi9*y^ZEO}+PgMaJa1MQVD@f18(;@6uMF7HDwyyQQRf%F3T~{De^`nH z!=XUx5>v<#VJoSM{0JI58Bt_WG>C*pCyqZXHh5$r7D=z|Emv$4<%vycWo}6H9~v3m zUF?mTL)mC?)W3Foev?1$8{1m{H}*okgS}u1P4>rXK4o&)9~~@tt9?WJ2cKD+%BH7t zxdoqSaQGdeQYAVvx^98ZY)LY2LWo?5KbTn+*x)bF_i!ZfoC;|73jYJVgXx5205a_r z6c5B7ItY&_C^!XToa+*!5S>Ant=sWmv-cK!h>tL#c3wrW`@QZ^IJx%9wPETL_8+~y zy(#?J-`<7GKGQp$p6Q*b|G}FuR4<$qJT{v@m*_jRgh(K-TqXq2-7LvJEArJ_LhBx>VewG+?8ohm;=5Al8?Cpg#LngLxA$W!Romsb_ z;YBXM<$#7*6)O@|IOL<+N5Uk7a}4>&f<0~jO~Nb6z3yk#Cq6FBp7<1dS#@2K)feS9 z>tP{OiLkra(vI(Zht)Ea1K)TX`8SaLZ7rb~A^sp=?sV^cBimDd58Jc(bZ=%O*+ZEp z@#ib_?h{X}c=uoVyRmkZ?FOBqn3Q*frsTwi;Nnq@6Eswt2<2L!$VZjbbV=SNSVD5W zYn*nC1G|JK<%OWXauFBDcfUxW6^7l z-^yO`3Td{&L_h0cWBBunRM`r)3;X&)x0#%7ht^^?n>*gF^ZIp=Sh=t`I{ri!cV-zS zR#TP}wBmu~>!AfNg%*5fgghtLC{ltAT1YKC%TEU%S-wDsKaaWx^K2X7l3J>v}e&Sjie!#V7jvwvHND$y)5l9HN@O+Ry z@5cQ-7>!zZAy1!aJ&*DG|Ds;``LFyl0EO^s{{4$Tr|)+%mExQ1c3|?4Dm=^%PMkq5 z!29t;Iz=2MP05_H$Bb`UQiKivHf>`1d|5{5&oBB>xGwAqh7I~qy%#?WVSfOBhs;so z)2S1?16hkD?!w>3xKFSIGxRf%v07qotjh8qmcPob5Ppfb@;AEEY-3;9Dr#NWi5bP6 z>{r4pz9UN@Y!6p(S|Gz7u1`E)V7C{ZtxvFrX=O8PiTw&=!87PCeh0qC6Lu;-#Xc)| z(p~X5i3=5b z0yllGeg`|gMZSyXP5}z4pg>1EJZ@*LCr&k<+OHfCs`9V!`Bvcl&EHYkOVaZtD)L`Uzrg2;-Z3jAM2r!hNz73FBX~|$aW8ukOG8~q6pTxGvc+nC2|_qhbo>l||g@U)oX?d+#)J7J1=QblOyqhkFyd-&})p+VxOh#!X$ zkEU!#tRsu7qDUnc-c$cm{g0bi{|=;~{OfanVAlGt2~#a7?hwO5A9in^&}keNtfUvo zfW$77AT-}=ah{l^l%Xk-0A<@2`GLamz5^xSwu$^CDo3s>Rd;3s=Z_E0N0STnh2UVn zT{QVp{h`2MY=Qktyc($vrS|6R*`YhCn+7%}#@5D%c9mzRMmLQgERFB&U+e6Sh>_l& zV4#{AnTIoOaATpCTm*kE{|UC^TZF&RiATyPZp&YR>P{kqq3o0d15X7YS33MANJvmG z#RqLxhglFc-VRS#(6jKuhzol<3r%xQ8xL}y(8EEeRcX`(Wm&E6gM-KWc=<)J%$YgG}wE;flC<^dc zNcxq74l{Lw4`c(DTIhCISFD6`ohU_$&DW z&0!EUtOE@RvK@)VtRie84gs80BZ`!u(`py7WmO0kz^qId+|G`#epBBmbhoQqwzv;X=f&vlPS8seIK`IB#zE9rFUi>FoV$b3(YIdevKZod4P_i9~8{ z|FL15(YD7iFZTd{waXe*R$}Xga zo|O(a%Qzq3zmN3pi4)b0>CDEV(M_qs=AKyEtaZly(TqiFPqJ^0Zb}!Hio+YynGFNc zlo{6o(F_thl3Xd*KV06@+p~R$3FWQb#hnAO{A9>APzd!*g^|=1uHV3Ly?;lgw6(W; z$56CqI_RhrLb+PVJy;Au(kqh7zgPY*#?=LFMBPT`6x%7^QgsU5WEr;TR{ei4c4et; zY(u`dy#r2xK#T^Lc=2_A`^1=5j@h5@BmY4tUklQp80B#pqdd-j zO!rSozpusbe;c2>q~|HF!}Bl0XA_s37bv#ib34wspn(V)I0IBP?-DK3^wf*6!O}nn zYSCJw&llixjDNOq{C${zo>c%1;4Op(hU*{U_uVT*fdVvsjG&0fCjnnnhNnZ%qG~f*z zHRb@@aJp||Vd279{a3w<+4RP8cA=+dQz4cf@%yLxGGhU2U*2EedHU}lgRtk>Bk;~? z6b9^ephLt6R?B{99Gl1`hPt#gZsGX%dU_p3_9Tsm@U?W>4pKasmve%lBJp#uhJNHhZ)3I2d8EIWrUk$T-DYn91Iub7+m7okpvp!;X#*0eCc=&Ya&Cz$^?42a)^m zLu6-CzK(h7Y=XIv7dj#>gXQP66fci`E1 z-2W|MS-PL!>_C8cjc=*TXG5}^$|6iT?{H{31kvWVVq9}LQ+2VZQ)o$JS(Eq;a&>Do z(!cPlk64cz;{YlK-S7Pa`WR~1s|>+@DIZ5C5Qd@y@%g5g1dO{2D}jkRGpef{=qBVHu9d(CCB_9;CJn2 zqr(3vkY!5Ion%G!4XV}u;wfbf{;uIjw2DN%X<@g!KiTEWe2J&ueDj3YWgDm6;B^e8Q z{M7#|DjZ^0V)byMok@%ZfS|!+iMPoW#I7_AdxH6x)si*_3?sf!1}6@Zdf^Lw-CnQN zX^s|f$j|F>@G;)Z%0MevU@LqSd8QR^9!BtmDXtdE*g3zYY65hWyNQmup~}-BIG_e^ z(@=isHt4iIw*^Nwl~%pp?&=Ab2AmeXMJQWbogGe{EoAK!J5+k3#byrKiwMmbwOt*+ zz>f(PBc6D`)%aKFzM3@0{#s$a8*+3i9Nxw%r#dJ$Xe z*NN=f)C_yL*zXGu19maR{PLXQ!^;nzlF5t{Z&sTx@KDg<2+|+AcW?cK;w#JFm-w}e zLnRfm4zBY!jP(9&{Y3Nq>v4B}xeL`lI}`|HAnzG%<`bBC_>twgFbYMrCmXhSW5$So zz-x?~gR%73#M5Dw(dz25p}39d;!PdeSRjz`O8i&J zleGODPTIPkTFLAQ-;&X17nDdE5FBlMe~t4Fr$&oi9kB3c+u5X8#jMh^TXKUOnV4bD zrdaJyx5S8lk#ND79RUA&H`|Pax@X8m}PRqHjw%5Myj9g4} zSz@>1+^&)P99Iq!u0E3fCvDPiuO#Ev)wgV^jfN8^Z(q#Cg0P1@DISDnj8hye$psz) zMvb*&{TE$mzlB}zYFo5D)6=f2uj*@CE<9&9s*7AEu3Pb(WNX>jwDcThYrTX7f6nIC zGLht)$8$a@4&gb7A2;YZ``&XU+#*oUZ zPrUTL@1H#7sMaWUuto6>Ne@eV7KjNVqu8d04?03&2Q=wB_|HH{?1+23@wnF$Z~Vlb z%>mQ=QGBN)d77H_iMt(ol_;3~)*FTAcNjf(rPwK$ufzRe-2cCg`G%NP=3eEaO;wO&+u4jTkZ zr_-Vuw_arHG-~V)9ih`D;dyqO_#XJ}fJ-^Y^ssBhr4<}gIhA9CHje4x9J661$1Dgv z;Na3p&uI>CH4c`Za}ozn3Ek{X$ay{Yl^fuK=Zwi!*pbi?@^Kz}ved z-qxR%x25sSotd|xdqh3^p!hkwpKua|fq(Ik;{zYEJtFGwifrE*x*I$a7t@gGZv*mJ zDH)BzCj$fdE{|yR(FaWiJxL#Qw=Hlx z4!XE)nMpq01-d?enrAmx`=n>L>zcdTB;iV3V+Y;m!{c%8viBtXb{g5+KAPX1%nsNA zhw$F3#e4AHzex}MW~BnCo|Zk@>A3lfOakd7e2rPf51bu&zRTAiMrnwW&)XjbvMjl5 z!HCD{4+~#&^u(h5en)pKR`NwzESSv-EE$T^3D7*Us~!+vPv{5P{!+M6MxMxOBjovj zF`0AtbFq-c5w=FmwO}BF!${s_mv}tg=YTUgn|o$U3Pbu-JsT)uh^p1pc+^{Xuh(N2$wQ#u&pj0}Py@?S8w;&kA-56swV~ksjD=I2( z2#8CfqA}hqUSo`)A}TS&xFX_)i0if9-#OjQ4C;OV-}gS>`+PIc{HD5=Q>RXyI(53c zx|+-D=Ok7OP9gYMKl`v+aQ7O=<~fDcg0%uTHqQdAmJUXK-IE7B{n+qSt?){ip5bX) z;qlS2(4xf@j2c?&z6a+jQbcHt#a>uqJU#cMmHjgxU9@ird2Ee7Mujh~f7-E2Ml-sa zT(1^sMSH9Y=Nq6{-G~VYH=XDb*3-0SM{T6*TZfT*IS1~x= z%VOqo#Gucxv_ay%wb)OZK5p23^c9y>$oeZ$D(m7YqGxb31lpzF^?;yNRXuBplQ-d>)jCg-IWeoKV#sYr*atA8Mk`~H=+b&d!2(T-L6mp0$cZP=1C1k6c zwK=vB!^xDsVw@-uZICvqBhsAC=OnZjc34wdGzv!Q5sS0|S!HITPA%s%Q=^#nQ zhi8o2GgAxHhg?vyN83wpT+-rx#tUF3n%ZKCM=44EREQtE;=a2Cc@0 zo?33+wQ!Tv%hUUXfTvtMeB5R2mzzbp*KBBYZ|U8Di~-*Vk&+ zb@h0vW7Dqlf<4-GXwf^oM&|(UHviD1i`BK^%K`Q3csJbl{Ij+Ey*#~eQN_orc39_D zF4Y?Q)_uO+JArKy2R`jm`yT=ITh?;3T3kI`8isjzcIeZp?GWE;fwla?8rkb<)oakE zcU_*=uwME3I)Qx~X?4B)BcAR!abVc5vgOd&XCWr98qYThXi%+@n^wcC<}*H?_3)0> z7i%`DQ!A)u?S_N3;084txwRWw6B~S#xi$1^*cO{OTD`hI@7?&5T4C)Q)okGL!f2W~ zd~t2{7_OJ#*)%9O&Sc5HVMl)$FS?;rbwLXjmthSvuQlT_qP6k0)%SSDw`FUK*7H^W z$Ockd}M!pKe~&A6~&7CVjDszTkvc!Q8Iq=@$IX zE4_!=*d8>NgTNs^0J0Ezn05%Y8ezP}2iH*bcnfCT+D6y~E2bJ6VOQjN$_QI=HThXw z8u-!KJiGc|9Mob+3yCGMS zh(V|){u6P(Cl1e#P~LDc2LHo2eKd|+FpXr;9E}zBFXB5Clqts;F-WvRsYEdnv4gqJ zIM9qo>ICGEL#$cPKfssHQ#{g#Blq)|2mQr3gO)#bK8!;Rao}bwFcxXipq#)wbqCdv zi1o)6L6c%r)NO{T>`?F%k9l`@Z={bue za5S8`@OPqWmiX_KhNGN6OE(!bV$hm6@K3zGhxkM>1gOhHmshJQM?KJvpic;7>ymj9Er>=-{?AIih4z`_@il~FaWn=x@dm;p5spAD@rX75yMPB8NhF_MNF(h@ zfQQQ;l|tUw;~&dp#5EJYVojYzx?p*f6^o?Y#lNx2$wOAhvWyyHkn|Fncwog_)&k`$ zF$Xrbx%iJzB3AlHJS`>bVEx!7=ue#VmG#7aJh59}HjoYRDDnY#c}>J`61N2Kj-{sX z_?k(2R->i3Bb#F>Rtx#OY$;yA!oC3P?t-UY#6kI@_#N*bxhHK{pB4xYF<9F197zal zb*K!(Q!K4z8`)OAgy-jBp=3MxvTTn#rmvuVd9s5zEW>4l>?k8;lo%?b#ZlQwc9vaa zSBWz%`I_u5dx*KRr=TZi7?q$_LKePKVcK{ z@mltG#4#~U4iE+MT{#dd8wTOA_gEPxhsdFFm>e!g;QXPjd{4&9kum|cqn)@c6U8n$ zO1>|XvzJ-=ekl%?(ay#CuvQy^BU1GA_ zEhftEao6JqxmW&6{wROKj;+6lm2#i>Kx~HYUWL7Q6B`5Um%oZW@_;xm9?0M1L9F!q zUA!d^$y}T<9L8#bDe{OsDyE8$WWM-N9+L&~xIBTk7Z&14gH!TPStS3${M!%f82`p< zth4f*_@{Uq=N0Egj=X@?Ud8f~ct@6C<=17gSNuyX6U$|(ydtm4Yx26hf#;HL%5r%N z?<%}4@5sCIp1dy~$p6TPQfUItG9GTlKFqFI$>FBCYt=LltvYs#tf6^nH8pRo7Iw0( zt<}N1YJIVPlAl&zYoImM8ex5W6YVLiOMY5wsy&0dR>rRT9y^#$Cs|A*EJ zt2S-eQz=Lb*6dmc_7@D(TH`%!ZMB!Qc39!rUVBCBfIXojw2oS&7NtdFeO+g*i`G?p zRqLj`hP669w4T^Cs+aZ#-sbkE)<=6w>#OzC`fLBx-qzmH259eU1GN}!kTzJ0)#9`v z+E8s6)-#XL-qYf>ky-*)iH_3V*OIi+S~A|FIaV8|jmQ1>iP|J>vi5;CMf*^jigmKn zw2!qd8((p{UwgzjA*JP2uL^Quvy6aF@#$cdz|$PpAR>%|;e!(JU5= z3-(#E;#|w!QVmybc8hXxOH^2@TRbf_EMCH5scG>R*l*b4W2ue195wOOP!q8c9_wm& zBCEtt;&b@4R_yNhilvUFuEp0<&*EpPZ)sp@h+WRMh^>}Ju;|sX+u#5(P_!2vu(#?j zVjFfk`VQ}E+91-!9`OVGTo19<(%8}jD|M^kjX|G@FU1ToQ+#ZB%Hof=-4|P)7SqI! zmZp|xEX^#>iciG%Sdr1(@|>jwUI6^5rKRNsvA_~w`G=*IWxGr{J&YLZshKK!)m@mE->oBLJVNRSUQY>w8rD%y*EY^rcxJKkI_E}!G zw70xs>0k-BL|8gnA}vvtXiFzcXG<4LSIetnr=^?aHA{C(4@*zW>v-hv4NGs!o0dL! zwy&?HpQXR$pO&{R?^p&{-n9(0#9&R>U`wne&N9R@)H2L6+%m%Qo+aKg(vo0Fw2ZR6 zZ%ML@#$FI(EMqO>EaNQ`EE6r0ER!uCV9o9+_%UCJzr+d46!EwC1N*L=#QUp?#A(Zi zmZ_GHEYmC>TRyRTY7HNh6gM``ZA`-Oz^L#j{XZhq__rDVLB_wG{%wIBBlY8OK1R8Y z#<5W@ytQ#0Zu}c`!h_txN5%|JN=$GIPaK+<5cgj7j)R9M4IVRcNPOIQkB+g4$uWZm z$0a0NBL~MI7ygnGW0I{=%%e4mBXyDy2CE&7DmxlgbTnA)X!nS!Bxe*cxQYmOi!!)~ z(z!6wxfQm+*3ooq3ycWU|D&R;ow(-eogB>SNkKs#ovPGg3yiY6bRHCw#OXnSQPn#; z%4l7>TDv%jyQ3kVj;-Cg7z}mM8M1as9v&YXSG}vFu)zzr!4?=CYV8&?cuaDfwOgf7 z5F6pr4eVIE>q*_6lI$UtsDz=`9!}Lq8X}7{>%?{Fl2(3qq#BjaPnB)j#{n^?Wa==hk?!*mXN8GlZUhJ;$* z;MnSKJkt1}K!edXQ7*k9Vry@wVxi22EhsSD`etPtZGn-&I=4|qo1#Kp-*mJg${-kR zNFvH?lhKx_s2XopX~UaFH@&HMlk1yF!y(i+xwEWq>1@B{V7uB|vBTq%;zkc2ZGCHK zQq0)6>iryvI%CmxYd=mh4h@+{x3>0YavuFF^~)Ml*)R+_R?@&IT}kmDs1t}V8XOTq z#~7+ccZBmX%9%r#l^e%x^m5@5#=k))JlHM9Xl#rzR0lhF_ZaNpDwcb}8pq68<0|_E zJQ+N8H0tVT(C%pP*fGQ-u9A+yzQImJgj<|ZeO#3>%Iw>MLO7o-sEyw3fl<-cpQ==6@!u89uqk2Km8WpqcRkt@>Z zwy3}wiB+^8S4qfrS5I^r!AW|1lAIU~3A2vYn=<+lK?DW1ag9$*7&_V|nZ|FjQ=w31 z0^>KrI;OIP7{BJYHChxE<~qjF0$r}?d_xXVW{ZrLL`ByaQ>6uCjJ_LVjNdUle#hwJ zcbv}jI0w_!##N5rar*e3;7HUJBRa%7!Fl`|5|3_Uoyg=oCRWuwdm!5yn>~4;fJis~ZTggG4`jh2-aIz)2eNfRygp^x zgN%BEjCzBN#s!&;Gx7%+{015P1{r(>8GHsA`~{i(ne>hNgA9IxOg_zg=CLUUvmS$A zn?c`Z)Mqp5u|>G_8#8!56f9dr^80_folg+NXP53uDOgGIqHUy}fZX6o|*6kpU4T0)Dgw3w|4){0t(0vCS z8+_>A0*;OPbuYnYk22b!dl5J`%10Ue>N7Or4LL*^<)aKa=rgy?9&N}W+9)4w)EjM- zi#Ezd>*Yd>Q5a&3z7Rv3>Z*cdRuhFpUUeF!lPhcO_74S9zc@(VWO&0`~fu+e|P#=y24 z_1cYk?MCD5X5)I6)DsfnGJy=ogi6B^VsLIuX!cN}#!!P2mYuQ&;lX?UZ(~qwV{p>O$lu1u-^OS` zxFMPFU@P1J({O~C7Bt)-6K;^v{W%y6gN*Jk!Y~+QbUzWt1{=o7jy=LC4+l&)90uJ8 zgO2Xg!2lR+>E~NGHUy@hkKx!5kbaJXW1|Cfe-wtm;6ry=aBT3QyDT_1_|Tmc92@oP z&It^HQC@dmaBP&1GWa!IF1zj$Al)b*W$>-L3@{uABP>& zqbjYaLC-u6H`?>aF|4Q|&pFoEVps*eZF5 z6&EV#FL@jtxhD@xilbw_<#6VP9HsdJf)Au<&*>p@SR7>kT9GHFnyabz--3M zZPN#0kj-!;Y~ePoV=wp7qhbbgB^ZQ3VfrkH|27_j6X8I&8XPk^&UaK?((uGsx?F;D z%vf$#$MD|O^k`yWa3UJ02dGf;5YN$=0(1(=Luw?98968}Y4q@+lvKlvX4=GRkS)+w zZAg3~Tx6tkOkhxib>#2_y6iAIZg65ktR5Q}Tn)1f&Wn=c%osM8L4iTx9>|zHoNIT9 z8k3Z$Q`X&;Ae-)<;Gahg{@H2-+2H0-yHIcS!DEt=;@}D+TrD;+9(Qqx8zO~&59Tw% z?g)lEf{_ST9}VHe<0Rb?-MTUu9)+NF0{0g4h4T}gKi!B21?m1Y+M%~6$nYO+5g{J2 z!^aMfjcXMXpX@d&e#~f4awpMHp!&!$@yWwS#p^ul`A8q$9}^Q#$q9+cX4qqBQXIs_ zZHkW@O{HrZQDzoTimyDNy3BgKVn&ThN*r&d)re0VI(%?UJjjEL2lT*{A)in|8wL%Y z;O!Ky>}p%E;RpxY!m9Dv3}vpyComKup&*ZL5`zubIM_Hj2sVTgYzwc(CsIx&>^0~t zC1z0KSg4Ch$MCs=10y_fZZs$^K5?8Q(Qs6;pOjvy;dtR)Qu=XlXmy>LP%sqe6t+2q z+c<@Tjc|}tehQoUDQxDaaImLNzcow8t5O>JVWmj;(8wC$d=SJ3-Z~Mk=+TJ4MBdSoR995`OL>21cT;8j)1F zGRQbH2r|wrf^^3ruw!eUpz+Tg7Zr_(6b3UPHfe$%k+Iu?T1E5<@Azs|?^a`DlEUMY zZCHHFo?h8oV zWB47Ihx>XG*SjwRui$Q*#8v!Tz}vEpkhnjKbu+jpYX`QG5x^+i*_61aIT#p+J(MNg zufz?oF}PPL=`JO3vitz}A)ZK;xc@i{I0w6sOWa{x09+!M08?ZNFco(eCGID#0cOff zU^ea?O58Er3fwNY19M~!aJT#(h&zP9ee&Nx-1`F_mS=#)vKV*+YXv3Up9A9V91!oG z6%u#jY5;3$SV4;%Z76xpiwE^NT8!%i82jcD; z5ckx8J+BhYq!j^iFA0cyN4VpIyF$Pa z%MkRSJ9^C<_f8}vN%EAq|6`G@aoo$u>9{=~cj9B*bfUN1&~qTTlZ|xa?nx{t zSwOWTR)D^087ykjJrUe%m#8Zeh&4lu^mH7KI^c_S9aMw(<=^{%J&=3&X#TN+<0noQ z{&DKhqQ6f6edg@B^A|1_Un;p=dgbc1>o>}7mfxzledq4I`w#x}5Q||gF0NKL_iET} z0V~hEv4dWny1w=N>NjZEs4@0Tdb;T|&7N)E;`tW>TG;}Fg6$!pVXfP~+`eN}bmy+G zc7Oe?cLv1{pMvU{rv_LFLC-e!#6B#sxbvNW^(WJWB{I5OZ_#e>grs<}Dn2GT0e7@q z--}Dax*f*JjKfC8B)umRM!q-lJ>0!=VTyaikNDR}x>}NJi5{{P)Y_qkM49XbiQ?{? z$Ur_ll!YTbl#N`(myRTF+~{RK>7PCp)Z>cWGldJzU;V|*h&MU|;X(o7I040E;U3;U z%3n-|$3fVO@YV4Cz}I?o1NQ3i8n8E~;wmb_LbiwvekfdR6L;nCT4uZ)nd_1*2DTW8 z)c6*CfJrT;3)yOZ3yhZ*_yAkAXlCRms{k3mLJ;2+mM@Y-g?Ql~KfjO?koEkMe@txg z^Yb~j>n$e+lszBRGWCT+Esi{2*y41{(iitUe~-Qa*5^H1)p@?ji(W7Iy%5>5K|uQ! zyIZym82iGg=exDs*W&8)Q~weAkI?6bwCvyFfzSe55dFZefm6_fw4Sc=qZVCkq2_{{ z4Ht9Nucr@GCj6Ix+Izk)C8@Ft-S{8npOjS{cEl3>$3DInXjk{j57l3J;1p213Td42 zN97g|RC|qsz_SHHTui4URajh^$4C|Gu-+E6x45}FdD>Bz?2*K>-V!?5wuirmY^RLx zAsdH77T8V&T32saC#-J(8)S|jyI@MSf>(b zWg4d_7f}hc2It#T>{kAcu=6)?V&Fvkh>$~wA7M`kOtrV>ke=2X$0>oW=&R=-2L02wHGe^qN^x3BU~jWj;KYza^yOpFr8@100{7WULfeOk zm!{h8gaidP=vmk;;-3|R4o)i{5HNJnh}gi=kl436boUCi+V%G!`pKvf zoqz1mT>rM|>-a`DGQKmSAN#JJyxu-$NUweL4GRbhXdnIfH@vV*yY>t8Z$xP84y`-% zEDkSx^xI{3`wRLvVo}7scG2{GDfFd@dZ=A=hsmwSUfKhUZk^q3 zM#nwv4uv*tT^#A(&l(yLIW6pyuur18C3wGaASrK3?~FUGz3BUe@6z-YbHe(DebUB1 zbP)YQ%0s8W{C3ER-iM;L#aiE5(7Aiah0v_9>@e8tY#V99=2=@yu!^DqkJ~%uXvG*V z$M3=4`_-k>`HomUr0x~MgZg6!m-+crw3)81o$aP>74Fl^oIPd6>;F}c)tys)RLS4H z)ouJ=IfZ91s(#cRq+R=~NL~N4y}GNHrM#87>YsYMGK_$#aL`Z{e{oAxv4b}=ugd-V zfe@g5_w2OFxI1Sm>sR|vG^2P`_SYR%bmkLv;p{;wLv2l%{TiP6^4&!5`&OrH;0!8)w@QZN+7fR&s_s702hjO~sA_q~G|fmn#1IjHM_TkBi1>jZlT{w zsoyBypY6^3&Uu~`CL7=!k1-iaHo-Yg_??)gAM5ezlJQ@lk8w&y-;Iw7xGf1eTJOev@RSykUD&itqX;#JRQ z#?ilW8b^?IzPfk1uR88%qnQ^rxUxUhPr44NTd-Bc;7-Dp&6lvcF~mjR}5J3Ak}G(74T4kaaH(e|LGJ}F2-mMex>M-T*wx5gfHfjqHI5P0ROq!p6VoI zv>(5F(Ak%=t!j^<+ku-f3!m}iGCZzM4^s!TTdK>XGmw?5uH&Y?rW(z@qJGjjaOjJE ztfTtHsjpyjDArNNIp^bHbwk%X=M>oBwREg*Ki-EzJh_jA$o6xl;T)#9Q2mAZm*z&A z<9ziw+T|15O#I&`$`uR~_4`c2n2 zb=izD{yi{H8?)sjDLDK;(>J1H>YGmU5q;AmE0OfA^5BsG<{ifvtn^V#yR2u1-Qn_j zUeenq>&PRdg}SHPUOgW3%YDoxT!+KfLY93n)&t<96e;orDo7s;-GIOHC{6vC^quUE zY15tOpC^~Vb&4l`)V2REn30=ks(7Bo}$^B_yx!_zL2o)zrH6-@v8b<7 zUh0@rd+r|X$$4|rbU7IEG3O&yk$YO5FNjp7$F5f?2YP0`>aI>(-E!1XS#NnB^P(R+ zwePN@UxfD&_RiHk>O<5A*_h?4{-ZCP%T>zO%yr}-`q#=wtqzP34S#-{i<5C?_IJvu`Df`0l9VJ=5B zk9XH=&i6MtfQ_H5Zaer_cW`Xx_m55#BF#AWbI3%eWuh)0s+VFMK10><&S~c%?|b=EDeU}F#~u2s?izOB zr-PgYvYsO9=sZ(|x^;A-I)A)7IBW%zV--Qm%Xx&R&?9D@$g$7x(#a^}|< z<3AH$kd2cq)60R6YsZt-E%afPa!&NjJ}7szt+MPn%4g;^|3Nd?AscnukV9@-Tjt@e zlWm0$P5PzILJpT82d8olx@K+WKh)L(I)5e>Xvg~=qc(K$GD&v@aC|qH}!Gyq;k%5%`nDLGc)`@{hKz? z9AoN=W4`;J%04Mwl@_e!_*#&kFKi_~q({`p9Pb3`7bjb7q^r%CFHanwsETP!9Ieo^ zLq|8EzqaB(8{@c2IVXY$stVZA`=-uOn0PqKeCYpVuaWOAY;u_~4(?Mv*o*sV(XfYX z=hZFP$-6X0O`Ray^5}=`hyGC&x*dTn=J`vXgN-zE-n{CVH_dYNuP&S86=B#q{^c6} z1KU1*E;Y-Lj1_dkxg66m>}2H}%W^kM>Hq4c*&aGJ<%D{TF{9@)P%jRoCrQH4%&$*pSZREcriP`3?wo$#fe zeSS3W&{#LyVf_11+Y3%r#`~e&&+W76 zI`MB@PlZmF8Fr~o0d$jS9M2K2;79-B_!6#{%JJv;45Fvk3wd6Ktug)D2Wa=%gWX_@ z(6>mt?a+0-KGap<)Q7~o8|f0_b73c;uL5>f6{j`De8DuB&Mm!PIegQU6WKGPFDqc9 z;7d5qF`Sod&s7@pkAGBg!EQ$^jb-HhH~7Eluw9OHw8fmWs+2>#;lof~l>>b)MSZ@o z?L}!FRBoDG0kd~Jd`?YJY<`0reK~h{3-Cf7vqD^zsyv` z9nj|ivb*qE+N-}DedfsZ-$JGhG{>A%-`zbv;K`*-dxTUzk3hZ0$ae?N8jW*P_nb>p z+o_yWStD6pg-k2-aY?orV;wqR*n>Nd>x25k$(EGE*KpN&SAUY)RU*X?)z&~bvr}Gz83_Ya(8+zMR1^O61Kl1xi zNjlGZ{6jn#zPk&^e#xD_W7Ap^xrY z=wZ;JIlPQ?-L%1u@k_SH^v5dmSNiB^d*%G;T!!=&W9B}!%aF|#)Kl!xahxx*%uJe( z{~P^i+KDIm4`mK{RL*H2>`J|L242KLaZJ z)$ibU=D2{Y+yC!5a?L4)10z!vA1D`o>TO4#nsoGkhd)Zk z=6I-@hmO)ZJRbLWjJn{MkJMlAz1U9xU7V}He>hNiesB8xAfYbPe20&(F{k~l&z0O(!BI^j5S@b0V6{%&@a<`~UL#LsjmKbMLB=|96jM#vUw@GS+}1 zjFpVM_7rau&DfFgW5&UZ0;D@ukwQut2k^_suL!@Zvq@cb9(DlHSDU^eYG8Mc zw}db4%z;;#46+Os^?ChiL(w=R%d98kbVf-=dB(#`_e`J6Mw!hsZJBK|BQv{a_RSoW z8J{^eb86;{%=sDXGnZ$s&)kx^8!KivscE~XVio6X=)0G;OBMdG3P<sc!8q(B`Q>b2K0;8PvMFj#d}JTa$?7H z;%)mJ@Nr}NSmJH_SWds2(~Hvg_#(ah2P^ZLf%I!Th9P~XI+K%zI{(@=2z8#{LoMC2 z$EJO)%D@w)U)VJj>8E#JKzi}+ElB_AdtdEqwC@bkv(%@%nnK2eTNv{g4-+CC(%&@& z^>6t8wDy-e`F(|UT3v%Y&#CepyFuR{^e;dTvmo0%(BF?R=zl*JZ=9U3?(UqYZCAH; z^`-WI*A%U|{;dtIDEn>!T5)OndgS_2-9VYSs$zFB%AeldK|7*8%}If*2@464>!>=o zb3G(aSf~zu7YoS~-cx^nmkeD13OCTY30kw&vG2Nr;`N+^qzO4awOtQ?$XO0e5j*{% zDL~F^);o&o-L;?VZN~LBLmks{Qc(x6O5RA$n?iYa-l4p}LN#u8B=Q20ceXmR^B~DC zr!!i;Xt$r1$Kk_j{Pw<(<7LF3rZx+AZH1`)#Slp7+V1(tdv(_-?H5(PgKLki>JDk6Dq1Xq_r!DIq?A}({@FG6T*DPV~l9Q&Jt?D&Jr$H`fW4xQU!3fy2uj8 z{0v>W`KC3mjG>9og}abOZ<;0YV?ZCo~0NXbKRzv8#-9 zBj*U|#ts|GK?*zeap`?rdLNhGhtjim&F9klxb!}f$j%5uBGtj+ja^A7U6Q#2(h&%6 z$7s_~?$WkFkj~YdL68pNzZriQVn-|J(v_VvAfs!$HS9o*afLcidV3@|z_}qfAl$;Z zOC5stVZ3eGLE~*(6O_KVH61)rp2LKoFgx@Os;EAq>SHFHQewdfmV_w>T za_ccStw){fQRjNpxdC->T7Kr+7HE0N#=em1 zjV&5kuAN)Z{zAmhRe$gJk=nog4BB7v?IE#&ygwQ3OGZ1AUBFIbcus`(39Bh;f#2M%2guTI z+Y5d#e>V~Qp5ELG{GQn1qs>u8TVlcW(d;5<$1mSD0oR3ypRY~=kE#tB4`DM23)Spx z-JxNNwjV$XE~3;u^)2Y!C!{?e*EYw`hF@F!BJt~P{QBa55PtFajm2*&elzf!kKc0q z{Dn-ztelpQUlD%TX?9(fa9OtlzaNpl1;6#iKlFd?LHr8vJB?one&zT*geJJ-=Yt<; zu4{xJ_*n;j)`6dO;Ab89SqEO$ftPjQWgU1~2VU0AK%IB+4}MU#J|t7N9W#*-TYuD6 zsMDJ=Fjn$E@xfRj%*Nch03(LNuq@wR*K#n%PGj_J-&#uJbMpyUnJZa2urh@IX2e+C z-UlPy6_^J(cg9E}3P(iKH4n6W#xhk7I?#*nym{PuTdJ$FtQ=S+)B-OG^R2cC7Umbc zw@t&|bjJ{LQu_n8hMo(eH=DR~n$(Vft_Azn9fkLh59$`|Uv~`QliEqlgwPc7dQip{ zWjvXx#6Edj;4hi94{2umc z#q)dEpB2yV$*1Z0J?x%}=l8JVDW2cMPN#Ts4||x>{-S;8$vy02iYNE5lj(Hq7&-|1 zg?@#7OY!_3_ASNpd)ToQPwrvY(o}p$(vy2Kfu7vMo~3wlPmZQ1_hd3Xvj?hpW)FLp z;+Z{A$1{7_zZB2xp$N9j2|nNQE@$zwDgn;#zH5@qSFsl=9Ylqdx5R>wB( zM~O4pnD^Bs#6V-RpmC~V%K((vq<-D}GfLFj7>^SBvgV=0ErfrAb~ZwZtLaq7i7jJk zJZ%Z!Iw~+8sE#=(QM#oBC16#FDnjyVo(Bz5(|YnfyTwGDWFlZp#U@vXFP!1$}*>mYOoBK zve(Ns>h`j=B#Z1UY8mhl$HO8aHIMNMBUW)_uNNNJl~-&m!rr@tYZ*5%ZexUnLpyyM9b!&15 zJmcu6&SiZ@v(Ba#sFSIJubG%1k#DjO>?YwxcDnL`lK>S-`IfCfk+2p zPk)t}J_=Nnc za@|e$N3OdY))Mvgjga@!I(iQGOO=Unlcu(#?Q7Kf%s%J?u`!j(W!X{gm&}JKmz#;Z zot(EV^6s9Pj=Vo?XvujY8{~CI-ji!jao!)1*H8T`;~}{AO-+FuYh|=UYmehPj{-kq z!TXKO1u|BZtT=@gv29fmyu1$?PpXR9{l{hp%A@LFtGu4SCV??B8bYlT>`K=sJQcph!o^P({ z0C``|=mxonLa1AOMNsrYCuoMnvAs>{TTxo zaR!P!Z5VqpzRuW-5oa?2mo>W~sZ!k=SXspL+!9EtaP0`tct_n|xdeUu+~RWdal^59 zNC(n4ixv@$ z)rXiyAEx0$G*(?;8lglZEgJad*`!wBqE3Luf@l!mzEYXAq@yvf~$4 zVeU|8SNcF(N>`g$D(tfaeD)oUae7EQfE+GHbiq<8cn5U_`D_4P=`>Bnnch>ckb)Kfs%pOvdfe^YbFvaG!WZcePbfL<#{3~XugEc99#=+pq+(&dHNwn8Mg$6Ia) zL1}d#wDIoJQnZl}Hfm|9mJK@yk3@)7J3y~+HLi%E{?pXPH5#3DtrMWvR^3f)04{Ua z-UFApt9OG^Ip*MZ)r~cK$@@=jKw7Yr{F$o=!~0)>GgWmdZ4vb8@)|#Ic67C`2Je6L zRnWr>w1Vv4$**aSC`!>VIG;xaOz-}(66#ZYV9mfYW~sFCXtnD)0r^#5;05`4tQ|%68|AhFyK3JG&$Zuy z@0M#;5v8?bHF&ja_A|Zm6)nMEQQCLl?=aT8!@^>2*a*7;lh2q%2>$*|^#Om76!4XV-O#WCP`bYQ z1X_9v?aftxE_qu!4V_CRdaGM9e^*jZfNDijFHpUPFjoGo8G%-pujVtQG`t5u)LKry z&Ayf7(ceNCXG&|mLG|jgQcyjyW&}}PxPS60GP zRK*CVs~^(_5XZ}=fSwz+@p)Ict?~q=U9e6#n^`ssZ7&AmY$oM8xID7D0lez+0*29pc@bEH(TlMrqc>wMMqKSf9?Y6R z*aTo0A!x&gNcoI79@PXK7k%9oa=4k=f;e9N031J9d56aM>IdMvY)J+2y?P7yE(GFS zJB7~9&a89+-Zy|KuEZqP=JkXkTAC6aAOFY8d*DFpH3D zOQrr>4KH5ZUH$+y6s2HZRDY&4Kn)jHqW{oWEl|VJReMR^3p1z&jNIkoOO>A*P4y&x zf_hFa-^LbiA5mR3fT%8QNoSZT*=WO+CAeOoZX*U~n5npqrG8!AooYy>>jfoCm!pQm z%YQ~2%2xC=YG_9_tT@LtH02t4aSgGk;Ra-h^M;k4Ttj!NVQ~S~uw*~i5KA?zDy14$ zoZ}kWaSem0hUHgTN|#eL)bRIm_QvMsqlVm-=OC*iE2dE!mXxE0iqv$rwJxaP+EN$l zhgCMzkhf$AYRF5~P($v@Ak=UHz6Gu>EMJQnD&T+6?mJ>x9n5!xu;V}vMra7a&oQ@;c~&(#5^d1E*lo;nyM$u5|!0x|e#Oo;j*~QHhw#IFAwc z1QGKM<3h$2jH!$(8CNl423YzTI4X`W#wa?oA|3rem_>;5!Ir6`aQ+TNJ?QnN>q%lj zNDK&x0UNSef=%|IbIlbkA+eIVqi|K@+#=l7rSl+2FK2lYwAgbw$#Cbw zwXoG91!GW%StrPESTP6E%Uo`!JAEj18l?)swVyhRJDGLk8 zOIg6@dh@+xYxG+)9zRFO1|SSO_BCBk5sPC{|H&om$!}S51Xoh-tf2dzKP<&nMw&^e z{<#`zdq-VP8A4+dUKZx@S*;0Vw^5}i1*w0shcBy6nW~#VrtvvrQ<~*hta~tovDQj z^0=H_SG!% z$(GHbI)(2y#c131rDQvQS^5dlUbY2oJBjeSkkWKHS}p%-5WGdVWs_m$iWhu_cG5Y| zcJ*mWK5X4@OUa8YTkeV0{)|+32(VCCd$2Hp#=){i)O*YNpq;yBZ$X_>T}ml~$4h8o z^kDR6^kJ;SSeMb4u^ywJ${$`Rda2uATo-RJVqB*TWXmy~v~<~Y9^H3%bl>68eFu7u z(Y@H0M)!ixXml?+LZdsyjyB&wOuDL=Jpi z{+Uy0yf2MJJIlU44XL`#kA+lgEa^|<9eFU`ff(-#12DFrLra>H4lT+c9s05yb)H-t zN;y0)4(pFFX=CHGK&?$TbQ zRZDY7tB?w*<#Z37k6evynb1`Nx(PL>cWHn??bLiQv#qFS5 zAxp}jTkYphgg^1h{4{t8FCjcY)L9fj{^b%Iyj{<)9>UvoUoxM@?ZUC(<?X*O57WP=CvzKhkxw zg?mZo7Oq9hOPA2KkJ5z=;2|~~dYZ1Z!ZP6M)8aXFwz6a!U1?pom#(x9y#TqCFL?<4 zJGT%vgmdCb>nsoQB^M=-w$C9C`r;y+c20=dsgOwFq6E6qI^&cGmSa`v*E8if#_^03 z7$-7LVw}wQ0po{^QyD*E{DkpS#?Kf(XPnOX1>;P{S&Xw87cwqlT+FzHaVcX8;|j*r zjIjCOa~&i6bA&S(H#2Tw+{%biiPRm8KQjKrxQ}r^;{isjT0qXjjK>*IF#gSm{eh5r ziLr$7Iw9V=1#fZwTGHijPLnQw(E@#$ySO#!@`3`=I1>r3kXjH2wM%@!iaNAgui9n z!HDyuxlM5&hVVy@`H68K<9@~ijHLg4=w1zE0&5S%90WYf@y8iYF#gSWh7tA(rAipD zaQ>@|*C__x#1`CdY!JJ{d?-1`g#9Iy*$VJ$HGzIrASK{8~YJP_y z-e8FPms{jE{hfq-*YaQV7DB#f`KvrY?;AAVG)Qk5l=)TOEqEGl6gFVK8f zp#E;as_zAKycMt(-UwJ1Zv(84dzw#r2Vel+`WMXa`s;u@njN)B+|e}O?C1PmzbC)Z zFBb3eqqq3!@9;C<-)FwPZxY_xr@yg})~nDv`ph@FZPu;0=0*utI>=qny*u z!&<%~zfvW$%H=ND!y6bgIc6J&cXBv~aTmwzR=>|%0IebX zfn$zx>Pf~z#y=P@GG1dWW4y^&jxm2gK2%q}ip7SwKus`uGS+0Q1utWPR-42AjLpPT z3xpQH;ns|87+)i5T6e}Cj6FI2b;e$deHr^P_Gf&X>EmnB(4w(TlMrqc>wM zMjyu7jCB~p7>Vn#boB#>XZz=s$T$w;Iy7R25n}bs9e7on7`HHD{LiP=xOubbO!eYd z<6$Sye_0GWSu(Q_cJd_R^El^W#v`x}6|kANKrNrc$2eT5?#xev%_KYz-&TkgboXfH zAe=1_V*Uk2sRuJB%TA1)8M`ocWqg&f8)J9I9*nOu_G9eN_)o^S83(9}fxR&U+?qX| zuA0rAfvckBpN+>=QNm$_@F~RC@iZsQIfb^Kn@MX>uFZOg)j?%*W?^*@;lCN-SqoKDBeUkN6#4*?xHCxgT|dw{&Rk40z=fR_floIq;n7 z&RGP{snP6qkfrO~4)C0OXFQO0l`|_IXFs{K7RXoBp0E97xY{!pSM1cuuZkg$lkX-# z4{~QbpgX>^+tD50SqtcnFJf@VcWwu|Xp zGsb5bpJQyn*oyH*#z4kkMmu90#&E_+#wf;4j9nPJGQP_A8e|92S&GR80tVuYuL)G3Vc)DZrNaT?>tjF5o{e8(pI8J}is%J>XpGsb5b zpJT*6CCHC=SOZ^V#CzBg4ra77wqXosjAV>r?8MlGu`A=NjIS~FVC>1*o3Rh$Ta0}f z`!V)se4B9qfq5M~u@LKW2Ot9#hctDH5wNr_aL7LJ0kzJwUGI z@CL?hjJp_jtJ7}}!aPFw1IHX<3b~AVj8_=1GL{SR}%c$*Qn4Kc8zKwP;5-e-J3 zsNu{V=)&mAXk~O`bZ3OELY^9owdmO<&6mUV82uRQGd5ss$k>FjDdRJY&kBz(f7F_D zxCMu891dg*;ut%JLl|KlQExcMM=(ZlOee-Jj9nRDW9-h@gRu|eTa0fr&+jk}U>wNt z!#Knn34~8GLQV)nPQYf2&oVy8*n+VYE5=!jvl-`LF3zJV8aR(}KI1ox z3mI`Xfz(vSm5i$x(?r7=eKC{OoN*BC@SGliYatE3wBcF^VHRT^*61`5hZ&DBV)jH# zK4SsnamEvjm`Ra}nH2aZV-e#y#`BC9880!G5X#1k7<&l&Gh)0U3=IO>7@<1|2Qfl- z5XN`|j>LT32k&erOk^BICN)6&p3f`BI6`R>=A`LA25E%IF<1u z#!nbOW&DisbH?e6Uog&OoW(esajvL7!zveYcoE}b#wCnP8B-XSan2PSUd@=sh-<0S zTVRa@(OJha8#tWK;S9#jj9VDDGH&BM-*R{d<1UW*f$>L<`H68K<9@~ijK47+V!7oq z<}n`T_~VQx82@HG!+43Ygz*aJzsh)>W6JS7#Y9@6F(ZLiXv|2^1fwToO~zWn`{OLF zHiyyw$P>WWnh`bvF&O!uPt>|IV*DfAljC1!#8^QdjC>$QJ`f`x_%>61hj9QSd}zc& zUw|0ZK#b}c30TVkIg3PSO~uF8ab8vQSzDY}5zc2!r6+IXN)E3<{jFpshj%jOFz!Zt z9<2fd9%4GVjCqVlInPPPLdHKBFEU=?JXaa7aZDNGO~!I{{-b%aLS3HfBX2X_VZ6(D zkMTa^1IC9cclvakfdO#_MrfgU&4tmG(aPw?h@CBwTAk68^Vi^TO%B&)^yQd(jDC#u z85=M*WNgCt6hF!2&!w7*8Xv9Ko?&bz-WVUMJuAc~1se8iMU5EY2nTRZ8^;7PVw@x1 z&fyS7tcyba)||5qBW4ZciC~Ok`kgpW7sjrPuaVqv21(e1u_u>(ov{~VAI|v}V_%Nx z$Jn27Ajikx{J|e*kR-`rT;p(#k7pdo?M>j+_c>=0<7mcY&OesJ<2e6##)%v=iE%RH z2aF$b{;8a28q@rk5bsZc0%8WYVCKf^Ph6*d!A`0+O8%K?}tX?`4p z_kYBRp<<*+6yvb&eFE0NPZl3wp8QZu#SA%3e2lkze2Vwje=eqDW&D?zsb-1UVh(1e zd3f{uH)5e!ff;C}z{(=bIO&*aGBCqz72k^OVu#o%_KJOCzc?U%!=@9zi$l1JorhWH zh&YOwra+v)mES^s7krU8C(etD;*uy4^oIKz;-)Ca-R^thzIY)1BNRPUD_x}(E9|RF zPw6FVN^j{SYs)&aF4o`IlYX+kY=Cw6jV0z9*<7}eHW?^`uqHo5hRQJ68Y}bL%9mt2 z`Lb*;Uy&VTI9BR+#7h0nSe@ThzAC%P?y`q`UG~GO{C~=~CGHwyO@5plB8OoOeu7NI zI)1!0MvmsEkDRaO;7JqB`HGJD)2MXUW7kz%ptBT zmobm=DCa-PSjhMX<3+|RoaZXzHI6A`yvbOu@+T$1hMk>U1{+3*{ndeY8SgRPXMDi; zP#yY&Y#0zWjL<^yuwjI*jM&izVK+v1#_EiooWBN#YjU_2)?1h0suR)i<@kDxevI`Q z8!$FxY{K}IIx#6p^XF1cMfIs^uw;bIIW>UeZ5$3{v@?bHE}-CMyfJ^cYz6mU3$F^h4l5L5kO zs|dFX@j(D=7U53Dy^Q-9_cI<~#FZB0Imq}s;~~ae;qk#N*et?3jCUFDG2Z7qIL}4d z|1iR9`XB(dpU}eS%4lWuq*7SXL+Hg=lhK>87NZYiZAPr;nDVx)%Vlwn2b%R5{TS;r z;*1#a4H?6@6nrqmv}JsWu^nUOnwF^oSM2I`Z-py%S20ZuVI0N?4;{S0;{w7%2f{-~ z+sDzJm}D2&veNOMuw{g>Wm5*gmQfhC4B=E&@!@XxT@(Y0_Q8*2(GU-d2F&DmSTuxp zayW-^7su=t)hCaIMI(eoLmu=1YRqNKV?4_FPcjxV{=s;W@e1cbi%{ws$CNSNWGq*g zKO6*$c4a~nSTw>rjCUFDG2UlM{B;)@N+M*pRUa<5Q~i!$GiRRBuyJ`_((JWrWSd(1Zizm3PdbJ{p2kP%)P;_Vy`VGLz#%{kjJhI1ZxWyl%D^gD5$E{t6nUn9A}-Vyd-?8#+c zXY9p@cea3&w;20!Oh3l{i~~6y*Y(GH!n#q5;FY1q;T#{&IFj3&z^S<2kDR#P55)C; zAiOfH4@E>jG28R1k_I{7+unPQ;JT>?m#5f5DkW^z1q8R4BA&S8WuBWAZ~G|2{C zCWI~{20Qhl#$3ic#-p77Bx51tAB-0nuW+8LjMq4(jPWL8xjNjXJ#;yLJk8F8cNp(7 z-ebJa_<-@D`h9{YbQuU;CbUpIbeYhV(aPw?=+0Q3(UbGn;BZY2*JkwPn0ky@y8$}& z85=M*WNgCtlsYnQ2XvY0ZK}$X4{FaaHWMQz`I9bB9t&M23eaVQ130IRV*(lJO2Sy^ zGUW+j3}tN1IomKomyrj$42)v>oj6Yy#;%O7k=&rmggqF0a@p4zdolLmoNqDq<(Phq z{TT;xe2h9geh74#S_EB2jl(%Uo^d3%H-S^%=bTB5qZy&g$Um0D<2e6##)%v=iE%RH z2aM2V_faV8mLj2?@9>M5X><{FAYW@f_oM#{b3Mo5xpG-2eY) zxi`7F*%v|r*&qCd5qHE$5qGqH)LOq*arw4ZEv3|zqD4Oz zsUj*;HW3loM0O$~+x|RzG%;Pw5R1g_Vh^!IWELRtr;9Vix#E2BM)4-`X7N^Wv3R?9UtsToUbGes`8osS zD@5?2z*qCvU<-y1i;swpioX<}7FUYTh|h|=kFa12wy?Qx3bydkb*Hff!&Tzzf!Z5# ztT)6r#q}cJjX}Rr{6ORl3N(DV1AZiK7C#9bo3|coGTb8a#D%<7+$L@pcZfR!$GJ*8 zi4lhIHtP$@Jmx@0ZAF{0L7n?B!_^|kh z_^8O30{W-LmEtqvvm)~(7xblkoS3(Vd0uZ{KZjDXk4G0~9z$jx!;b^s&ppVz3Bz;Z z58`=4=1mwzi!oxX7$?Sy31Xs{Bz84qPQ*LcJ;^Az;TrMJfwvajk4HPca1pa17;SKP z7iBJUb_2T?oMtwJiTSCxCGhoi2bm3VWM*^b1{nG8MrPGpJFtJ=Vyw*275!qIm>?#J zjl^WJv6vG0aQ?Gc+4l=#urkADVskNDY$3K3TZy@12k|N~U+gG$61$1L#lB)cae!DZ zR)_<|!QxPHn8^H@`FpW4Lsm&7Z)3!4v94}UhSQ#;Sml^UdGprUTi&I2KQqV6E z8A(CDLtHBUT)fYUy!LHsYQq)cgCft@^9#)LHRSmkt`|3m8%3V4(NiBmp06Rb66EO) z@?5tt$BbFQEh1x9$kbDiF)O%Tq|QR#Dee+K7rzkqh>Tnjhk6Y%as?T=g4hzIM(#Py z)X0z;8B!znoMuL}7UtL)VwRXA=8Ab@zE~i35<829fpc?iqDF3dZ5nfcl1Xj475B3l z_&&o~hSm+V(b~-4MH|h~75!qIm>?#Jjl^WJvB><|N&C#)mbn$w%Z6!Ux|ks{=LLOt zv4>b9j^G+In_8SE&2(|5I9Hr6-YDKA-YniK@^yMrW;P9c-in%ZoF3u$Nr&luX*ua_ zdWM@;?VxwqaFzJFl|1Dj7Ge0NxL({KZWKQdd3TFAo5YXA%_0^tXQ{CWh($mw0%8#m zi-1@J#3JC%z^Pg1>2WqP76Gvch($mw0%8#mi-1@J#3CRTG3yg!5fF=jSj4PP%nYjH z&D6GrSjU{D#yTL@0kIB z9mDzJ0z>9rTXQ-v=jhz5D$|yn^CYdv9mShzMH;>@?g-TW>?~hQvVOLSQG+Id3p34g zf?=9STNatNEG!bci#@~=k=en7^2RKjDb5w=i#Lili8qV4imaeO{M*InEo+7!yEA-A zUuX@kk+4cKK4@;W@j(zD1o1%-9|ZA15FZ5bK@cAV@j-C2_(|Z0$*<9lGu$G6CTUpVy$?=vZg;~(Vqw} zi2*~?p9twsgkCXBj0jwq{F)spHj;*3LgF+QQ^ZuUiP%(3H#y-6#4t*NIxS-?;)0mJ;h!kPw0e}ihVfWwWdsTyJ*S;q)b4{ z1T?kB4}$!(NN*xCJ>&3MkvC>$E}-?jtxGMnsdWvX{s`>6I*Qh};VSV@fgi}*tKvzk z@r)?yD#J5kts!;QwAo9js|>qXnO*i#Ixk#9DGdBD{j^;o_7r=Gy~R>-qKRp|f7LCt zyZ6l0cSvUQ4!9LQdkHg6t!s*zhkIt0X=|UGebUT4M*h2G>eSh<@f}329^XOa%J3aT zbA9fn4gSeEzI`XUqF;;?6T~F3k(ewt7E=O$p0b3}aBSMWlm^2zk!N0Ho_S%B*j?-) zmWZ@=I4-RnI8B=A;!JU_IA5d>nRMtwhBu41ii^eD#rrI4${y1@1y_gK?h-#2zYzC``^5v|A@LjWh< zUC}FsiH!nZPr7I`QVSWWg^bifMrt7=wUCiom~M_s-DAi|EzA*f#XK=zED$@1oy9Hz zY!2(*Ogi0^T9MdYWZhQuJpwx>F2?E&dy2ip-eRfPFK~v?0b-e0E>;8%PrH}i55qy? z5OJtDTpTBk7bl2SBHz21?l&zl&WM&a@^osTT527`S>oKlSLAS>IA2^~XuW90)~3v0 z_S@mA)y#e~yh*%SWIPuQ zCgtBs^E=7Bd5z{}@%P&6kCNAje-d9&%vUA9A-<`YYsEh+bY0+&l~v4OHvEe;?<(eh ziyOp^BHy6phL)YjT!7=oT zVPd%G6C=ea#gCR8D>+e2mZq_oBBqK>#HM0~m>D?8QL?mG^T0P%s~N#DY++5D`l7>I z1(SC$f@5-$E4iIw=1G$;7D(Soa%ZuNSg4rY#NLWiD)v+Q0~DuRq;!)0U~}CV!7&^v z4%5EF#S!8d#T+YEN;6IzFIG!GIk2gEF(WwUO3u;|XG=dvoU8Mlr_gH^bD?;hxJdDD zkbI-!-z455&8^~M@iy^x#iyK-|D{Uv=Z5aB##XJzsW10j-H!Uw&=pxr6**2!5R=44 zVzSs+ObI+yZQ76*DmT$eG;Ag|7qi6{VoR}=$XEk0JBU|_j5wg_C^F)J+)eB)_7!QZ zqo=hFX{W;qaiBO@94ZbIM+CMJXQVhvnla*7u~Hl-ju)%Ms{`jIuAyaU(&znBSS?N# zr--+SOT-%S4sogYa}lpoZN5u#p=ycsdf14dTYYcUOwL2l2cieX)koVvHCo#)_89Tg&9SJD4e42jIU;@M z$ax~Y=Ewyiz2?ZB#jZ-B(B#UrHm@FUTAPs8CZx3qX>CGUn{c9)KIut%$qe~U(8O8v zmBm&~qp!?xmN++Xc2XPaNW=N!0z=-SGJM#wCT*eT_~mi$(Q~|JLK}LHjZ9Ayy~TeH z{C?6xp4HA&kKkF&@O|+^eV2!ko66nvB_FGvLSM4k>r-(HY4VL|k*B&z3soY|j(waG zY3Pc6F-}Ynlf*`1ve;Nm37oCEffnqZarEcXrt~g)3QM^gKS-e$TEb`4qQeGDLj-%WuE*I|-?-uV7pSRkyjxw#;af3{2 z7QSRz6Kw=rLEI>QAo30)aW;t`iJL`g*~%s6O}nZ*N{`_d z@iTF&xJ}$H?hxtO8-J40W8!}u~h71t{%NJM@JXYPBttPdE*bc zB5;WG2a1ElA>vSRxHwK6FHR7vMBefnZ@wE~IE|y+U^8ogH0O!T9Y9_nUaLGT)P4Ll zb>yy=_fsMbUC}Sbi3wto*howk8;dDoYTyL>W{J(j=3=(kLTo9v5_824;#Fe4*iq~x zb`yJxeZ_v_0I^)G5C@8b#i8ObaYSJ2@DhO>w=*Oa(L>#ScV$G#b`u zgdd5#lYwkJ$9_D=g%K<99EQx>fXv%~TSew=ATw_R?hyHIBKg@R`E&6LagVrPJRlwt zzY&?YK?)}WKMc#I-ZeZU){5UNC&riTw~Q~@&pdW$8-K80{$RiS!G7ELgZ;Mg2m9p@ z_S?oE?6-|S*w0(UhV<@@SV<{2q}>LY1p;ZAL1uwK+GLPfAdog0WEKeQsuT(Xr^j`$ z`&sd0``QDJ{qR*wMYHHFu7Ajat)ijrgY3bZ`;1<{P8J=>7Q) z{a*v;hUHRs8on>`JrVSb!wzpk-TC**$EiF2I`$xSr%8vpbDZiEVR*a6(4=N3JrX{gB>H zH1u{tdOIP#osiy6NN*>kw-fSaXtUj>w-cG(PDpPjq_-2&+X?Bhg!EWK`YIuPm5{zl zNM9v>d4-f$NO>LI z$&^<}{SB$VA>|cPULo~2q`X4PE2O-_bkf{Ic{R)ubHrRRPs|q!#7<&ok@89kg~)uT zniBbxDVM#?x1!@mK0^IhJG_|s&v3rDz>xC#@zBR8uZC+xTDiktq`bZ|3CO$&$h-;2ya~v>3CO$& z$h-;2ya~v>3CO$&$h-;2ya~v>3CO$&$h-;2ya~v>3CO$&$h-;2ya_lWaB|pk%Bvys zCeSc%0y1v`GH(JhZvrxJ0y1v`ULE+pqLT7zWadpk=1oB6O+e;NK;}(A=1oB6O+e;N zK;}(A=1su+0^bkoz_@`S@0-F0MauDzZKfPU$}yxIL&`Cv97D=6q#Q%aF{B(rzTG#l z7v=aEIb=QF^0k!X!$U7qjty6de+rx@=BwhzoJ*N?B5;oQCj%GB)hY3eSSvEWhd9jd zfj@}M??Jv0I5%{Rby2(|1`KUWbVaWiCPoB)sHkK-(vZ196=h~_5M*u;j1`$1gv{I^ z$lM^v+#r|~_?&GMOHB?J_{gnId!?Otv!lMP zD5I1g9oUOfzI)hlMqscmMqrGM@qydGuCg7bwG3%3Lt4v_)-t5E3~4PxTFa2uGNiQ( zQv%Np8piYM59L)nzZ#~Aj2a^|Y7C3S?jleAXn696BP^@pN%Q;)`L-UK>EcY0b49~h z!W+e##GA!i#l_<7B2S_f_nRkC$df31J+PzyE!0njZ;I$^kl$Pv5DAJWad%P zEb2l-RwRcxVy>7c=8FYlC$Y2GC2+RydDDXBhK^8KPfEmw7J;0$@2 zC(aiah}Y^U3k^*_)WEwr?SFa?;#<;|T5uoEiL@^CfScEO!o=bgbW)nFb}x%uEBw zOatf_xvR+BPsp8wjYN9HkQ<9BmNg)ZGJUl4e#*3AnwTzTh(%&|v4>b9j^OyErcA?W z(o7d;ioCZ&YV*Y##hb*N#al(bwMZO#!QryN*@|NNqzspfcZqk4_lS(VmTBY_u8{no z_z>6QNi)|KatGie;-lg(#izxU;xpp2;`3HycYTGpJjwKP!I!Ma{%7$nhO5Na1K*bK z!@C&1DXtech#SQZMEoytHi;jJn?>$w`OA2hgZ(S+N8B$S5D$sph)2W|9Pu&J*5$Z(5r`LocoBHPN=&|qelEjHV!#knN-Vvf zey+qayofYmVuUrbTLoUk5HC{xvS}wnyad>{V9u&3BdWRwXFqf8M0QT`bHT*oSksrwDfMBaZxW;Q%miP`Xw+3=9r@Q~T? zklFBXoXBi=WM;!dX2V0i0#rVTy5EplNX5_MT}CUC6^iLl!2m17DZ$ zy={>ZoB0)x zQVuEQkW$`xnknUwQVuEQkWvmQ<&aViDdmt-4k_jEd8F;p_2LF`qxgaNp}0x>NZc$^%KPo6lz&vZkWy~AMf^|7%Hw-KOIyKc!o&#c8uXDOrM%y6Q_3Nw98$_5r5sYqA*CEr$|0q^w2V@| zwbZoI4S53)@&+K}4M4~nfRHx;VY?|_ZuwRC0D{$Px zz$d*oQPxeGMPhfcSnLtl(mR8)ZrC%hrSD^mJ{h^USSt20*O0Qlt7rse-LOn77b^mv zlKwz(kjPttXn0Ey4j0FX>xPWl^vR;EZ%uffvTitETp;phD|z6} zR_^BCDC=9hcA=~r@;)Z?i*aItm?Uz&3FUf2t~X?Tn3A)U^;0FwDeH#J3Wv1KAZ;_q ztZ>Mza7b$k(wc&l^?b%ILsAbUI$lzc^&AP*8!Q=0XK;s(Yn5dJ}IO5*h=oTmh!qY{|M#PaEthv zxK-RHZWnimI|EfJ@D95dOEub88%_+x*wE1IRHf??wWm2FV8^(yS zVw@N+CWwh5t48!rpd6pdKSDV+WF;k-BBqL17aFV!rknGk92;hdIbyDuC+3RB>=F1n<`K%VVNbY%a%|+@VyW22TwBWV z*$!qlyu};i9^Jp;&5@CI9{9}R*4e>r?}da0;jsHp&Xxz`3=wO z$2)z(^SUA5M}Tt!JByA`vJK~p3&d-ctA&Q<+vYJZ^Q3SHQjhf*!}CHj`r7^rxxk?| z>wOE)4o|>81YYd2##tTsv?Rs(V_+w7)=2-RDK$Jtyxieko+BEUj^{bztFCE0M;N{@ zerUBr{>W<7JBp`?HpKZP@B*4omBJRQORojy>#4m<>F+Z#PZr2K0$+9d#Mv1*ORjdI zdBuE56@D4`1`WM|gzgs)SQ+H-ta5^VmdxQDQB!_DG=3}pyzyHQzXkDI5WfZSTM)km z@mmnT1@T)jC9pa##rUnxD~#WQX(E0LIYY!Qkg*Gh--0C~ek?Ml_kwt@2v+Wp{Ia-8d_Ay}+`bXm(>@FDW#sjNJzRkefj!;dzGi=V`0eLP{`a+i1#8tcrS?eg4|(vA#kEcC9M#{OJcxk z%DXPiYVWZI@7289X1o`ncrQb|S46GxUJ&oqeT(s45bp)?UJ&mE@m>(`1@T^xFY$J1 zhxa;AXr46;8;dDos@OzqD&oD0j~ed<@m>(`1@T@G?*;K*5bp)?UgQezh0Lte?r#|H z#c}aoZTxsIbHpOCyI3stz_*>pdl~i=dx^cpQjzx_iWcL&PLL*}#IQ_ctQff>u%}xt z-pg>1I7A#O4j0FXBVzEi=-=hp`tv41O6nD*=H1hq0_jbG^rk@Cy^t0!q^%1ZiS(u*Hx}tl>Fl?dE16${C)8al72^#d6UT9#3H5KUF;#2h`klsSL`P;dy>7F zJqejT2?vUUMP@al87AUc+m14x6^@dIIh2GlhZ0tb;?3f%;$o3m&ZM?Pyj>dRP@=g*Tq^!tToyP~FpTF- z!{y>#;@#ps;(dWv+4li)h4`TOP~aVJF=H`?4~vh8kBYw(pB7h&&xp^8&j(&?*`KkP zmpZaWu4T37U4XzJ+Z|yn#&DJRr@*%y_f_%rz&@_n8{(VddU1oeQT#ysP~0SbByJWz z4jk<=gW6zA*UQufhFiqX#I52sal5!f+!@$L%w3W{7rzkqi2KC@;vw-H@rZaLaESbW zuj8H*e-O`$7Xsf9dQrS21`K)jG-M4R=oQ1n2y0?tAzt4w%A~*ukztG&E5?cOVuF|` zCW(y#hYN<`^$i<~DI(<&4Re?C#;y9O>5F2dh) z-SGD&wIZ>*SSZGWadYpZbdAS?4@; zlw(cf>RxX}urA9TtZKj1UTS$*o8>OPD|)wm5AwbCy~y|5_rnM52Q1fKVXr`b$bQHQ zXT6qRSn;gc@(B9J?8neQZZprE^;@2RPufqymG(+2g|%FsA^x-Wv&3Xg7xe#^uU)0u zzhxC0m(^WfK=XV1_sDDPH5~U9`xW@6{U(~V_FDG+GwVRdo0VUz1p6KP9pbb83!xkA z4ao1??-T!T_9o=be9JAu{>1*2Gh!_m;%v3I61v^qLFi6%>|=KG__ zTWBFQaz3Mllz`3?IG_$IdHk!^*^ zwqoqR@Fle{`(69L2@TrTMtdVNYYf57yu;(OKgG6eRvG&23ak1Y`73)LdSgi*`y2ZR zvauweEXiY^v`?Zj_T-U0`R%jz#X5U(9T$6YWKWLl$#tSxsUT=iuIwpN_T)OrPE+(5 zP6jl#6)W3vu`0f(?6knPQe|7=SJ+m#Y|C}>ur0?aa0;w&r;|gkpjo}ha8vVXuGnoNW4)sn#s;&?2qr1p`XQf%EJH2=3*W5 zb#2$V*13+D#{R;wzZ;Nm#0nkf7Uwo(V~gQVjZ=fhSfndkjBwUCYpe)aWRxs2LKYb> zi%gJ3#>*lTWRc0T$i}kBI9X(hEHcr(m2a7a*`KG=3T?No-9cV`4oPWye_EH`=^x`Y z>~V!eo4qc*tdJB+IUU`9luDZ`_x-0ms8Q>VQ5KRKj=>x0b)hrrrPigE)w;mq*C3@Y z_y-;Pleb&yb?Nj`1oudP+=LMJY&y?pby9<_LAuB?KqAA z89SsqnsGLhtI77ikTUlDtI$@Yni*$8+npt4rP3f}a>W~TnOt>Zce+v+49&ee_sQIs zb6?Bd&>&@&)ax?K>ZL0}(PdWs;~ov6S4a$4dGwq zKT26NRT^|zQ6@BtdwGQ}%jnv7p+tH3zgL5lg-2@8Wtlsn&@6mW`(9ZuXMK>hHER#) zHArP4-H4D>6_RFzB(ujYKiYT2KZI`0dYqIONN1$r9!6Rg(gpW0y7eJlLtdW!QF2n1mjr(ep{<_h`mHkB1}W=ey{;Kn-4NPL*4_}W88#ja zZH9flOYqk`&9d6O(&lZ;YKEt&ld3|}jF7Z|7{?XwmXK~)NLmq+ zxN>!=ybzMsgrs$$R5pZkpM)gruWsM{^^(b7-Eq%_bl6`oUh{;I6grRQ?Ls>2FBq@g z6L^Pv%^5fE6_P6IC567I&{6fe<`e5B_L-%yg$+`+kCZpI*@0$pNLn6}9tug^)w+1( zpiV;9e6RN9e%I;V2ubgTq)j1dTfJnCTet6#dR_C{kaW3TYRF%~m z=?We8)u3y^RpMyaRox!D+ZMMS)OJkU87;Piq~#%LWk`A`B%y0@P(!COr-VR9{ zLlVcR+v8+NcRnQTZt1DdK}bSZzeh-SzFunCE+oa&r_wT|UTT|b&d~f?HV^)f)UH0X z<#@J+DYQ5w^$$rVbeaimP5EfOTk+!n-|B|uM9X(8SJ zARVl4kEeyCR=#>&tMm0Kx8_cyoi4i;C~9bZH9QrW z7LWP0@0A-mw+5-z8^O@Dqpd=I_llO*tY9c%%o8}1Z&vV+?hIwyAYJxJQhrYYqx-ZHA?S^dR^-VN%!nW zq0iU*#D?}u>s9sT=nARsUbg+9b=|!r&y>8Ncf4ZXqoL!TC4a#>8C9L+M`~hXG~~2R z8+4tdBd5U@l-V-BHYLITjnvQrXsF{G&b;B8wt11*y1ETlt;5R$m!b$is*U$wSe=b%SwyD}uLt&8 zvtA*EJbFXxp`m7PNTuPLHr$Vfc4vc~wp-7!<@Fk#VH^BV!*f)Fl+JZ&Ka8iUxCV*5 zJ2mD6u7$R>n}0vGnpYF6sg?FWtmXB;TgmI+uj6&QRr9}G!|Q*yde?Tos`~$B^{#k3 z;Xhuz>!;T4>Squ5muq+Z)2dyjAM!sKOW{MQjPZLJ;rGKh{=AIvC%`1W!RTeJ)Kp?L zu`-C2Wwk^$Yq8muS&hxMSP>YSHQ8)dW$Omb{dBc9o6G8Kz0sHz+HBTn8vx5$rOjrY zw!zS>)n+r_a-@~adTpbS$FN$P&3bKBaH3XkFsrq>tk*UZ&gIW#-M0DgMpkZfwRW4U z)!S_AcK%%MzxnFqGUg^lGBfEuD~6d#55kA6hb^CF)^GDMOZu1aX=X+Hn3MFfmB?Jm zRq#*zHD+z3SK;e?Ej7$q&tI6efxj@mmAVmrU~PsU^XK8~%v-T~v!)wwKk;>4hi~id zfo5Gdhwtegf@XC$hxOe~Sh1E_<1LP_6PkGxW}P<=Zy;RasmXtn^0DdzPIAb8JRn>|8q+=Gg_1b>CoDyDRYv?LsSy_1}uw zs|Q~djbh!mK8ywRGw;M>MZ5>k3UGrMFJc8aIGpc>Mz9WC6~vOPMyvog3(jSQqXe@C z9R2lXj3rwA+R^IQPEx;iwEDH1s9(FO`n8*=U%RRLwbRwFouPj1RP}3Ts$V;etMW7M zl*?5xmgkY>d1QGWEUz(|6#jyi7mej*6J}Nd{MYtpEH6=(mnh5g%JRIjJg+RzE6elB z^1QOV3|U?#mN&-AWW~Vo*ov_~z6*~1#ahPx!eoD8vOm_<<83uX47={(Q1OpX|>k`}1LcAEDpO zpCkK=!~V80e{ws2&9THC$p5n~=E@cu%N84Bi>%;zFg{fHKwd8>r;#RzDFfiYNWxikwp)~N)P$mIn{YVMoZ(yUT?zg~ zQ~!VKK4-s|V2<{!`&PosvAIYq-4_xP5{@7pcUQy@OL!M)lk1P~l3?-um;5hC zdX3QF9eNsRopc`}?U1y_u8lHndDl8(*G8Bp46X^O+$8BOa%9?xToWfb=A`awLXFwp z=(LSnfVI2UPGZMI7a_fG*ZOWyY@;*J3S1dyUEE1Ye|FZz-!Eyrvku!ap(~Kg(~3#u zQryOvGNg@2$uY(zcG?%?--t1H*B<2l(dO>wD*H4*I8}<8n$ijN7NpLd}sHHJsnYi z?m4?x%zF{JXgxK+sup8KR}2@c#OuWy#dtANj1zH`{A@7_+IBvr!?p)H>tK=H-q6Hg zZPQwqAboFX`b+N5*CJfIv-?)e!Kn3;o{QNXyIRs+Fom6$<( z$_slei+Lrc)JV>{m={UW=w66f8P{LZikQdZGLY_c--_HF#pp27bCFwO$4FWkxiR*B zr16Bl9mO59xu21*D3$B&iIE;jecUG^lOzpr-|}TdWf5XsD_E-_VId+NSy%qgL-6vx3 zL`e6$Z-pmBu95UyxX*YB``OEvBHDx->3(-*SZ&N^BRT8BRA%g#_-Jj7_FeA26+=Z1p8~Wz3Km@?yOZD2u$w$5~p>aGh`Q zty8>ZF?mtDBt0FI6FDAfJNsUYi6Z3*+_w>tKBQ|eUy6A<+(GJs^g`G<;=Rh9>7-nZ zx7S#H?-1nj8gqs{vCFp=@*$riYcAXyG3`Z{X-|c{hIXi8-{-D~-V}r9wC`}A z2zyeoxtlQw(Z(k3bpIOTi@pizfk0WrqVQ_zHbmD(J&AOy`-*34_-W~$j^68kf|31~ z+=!@Nb*U_i-V}M!?CY%asa!cd@VfF-%s8@wzQFl_Qsrk41|Dln{(1K?Vl`Xl=qsY{ zjeZ5G2wOT5VbbC}V@zChSk*R3{VrdMKIh$sF3x=`DlcLyQeGf7dVkal(mfZI7PS_s z)#XdR!(Jox;(BBmkHDKDvC+dQUannu`BG%D*PK_4`-D&a&0gldNR{!L+}(rxe#i!|1Ei?Px$lb2JL*C$WR9?QH1{=+7o zv(9V$gh}NcZ${K!Nl$x|B1Ul)|KO~nT$xbv;a@1}xaE!9Z6wF0v^9w&6{Nc&8j?*&MG$4aa2L@tySyku<}1)Rb%Mg7da-UlgUD&~d&U+;`Wy?B3?A_Zy49 z;P&&q7lG9xW&74DttNG8we`K|D^YGLA`VARG;y3--(!ALwtVg!|DecEkUF~)BJYhf zdG)!2BGyM9RJ;qmd0|JzrP>P;Y8TA;qyod zpJ|TomH(MrWNSSKGWbMhe)T0(M!@- z;b)C)Sr54W@T01Q@g?E=T+WSnm&14X@IKbhougruF4qO!y72eH-;=aGd~H}?q&w}} zFjLndx&E-ch#4F=)2)p->!lRf9{l$@W@x$A-R}G0=Y4M=-ERDx`9`xfyTwb~&c zPJ5@fH%{@&-Eg4tUaZr8+94*4#awcX`E>ZQcNaF3*7H)qHa8 zT<5E>1o^XE*9r4EYdB7>>-Sy`A7mtFm$x=-honp1BVlhKwGEVog;^_+GAJ?Ac;0fI zUpl?RR+74F4|c=t+^~b%W1aU6Z+}TYcvpp&N!spx-d%_EkaNkqI?UYfhdA1A!uCkF z%ll;5HIgoSANDRq%5*M;H3{owBoMuJdpEnr+WdB{8>|cb&Lwx2cdyYg!Ehoi03>IJJ1Q&zi5h@A_XrYY z$qib~1oy9D9`7o2A5!x~>FCRybspKj&6<#&YU$o_)-k8n9k($7o_XKWZN`JlhF;6 zbTtWVl(dZ5qrqd2sX=MIM6Nw1#A0u!gW@ERLa+s}m(VTJbtP}-f|7fed)$jf+vW-c z(`r>i3QnHob(bp6?Uan9pu{!vd#jPNYKUX+<;-flcqY4mbo$C7XuY@FJKeiH8@+yxS;CQmc_>o)w14QUF}e8=eJ~_F7(%N( zxSgx*3evsk*+9BI+-05`$M}yP?z5g7+_OgFSaVD|_GI^2%kS<(?y3FW(YDFQaI@{$ z&N}l}6EO>r7D}26gU5q$e0){wH=S$`CMcF*kWDG)(XjRE^}X& z6GBfGNtsqrk#pH?!;z4%+$>UY?JR1V4U!^xjy+m0G0)nB&ai9o14!0^Q2O0!Oh_`? zU`|uaw)F#huTabfoE`QfZWDA5IJ@k7oxMnRI()IpH6ClQVt*R2-8ZyzSbGCe@%S5m`{)&t}sg)JoZj*;{|491yh z;xIG1)*Ygd??O-R-;vXsoGyeqggk6Q?Z4BL94sxFYzI@wt1%&WGKW_8%euhT3#N5} zo|?W~Bh!-HXyeHwrQo+U(X&2?;6@3qVP=T3^U42%xH2HEZ03r<2~u}&^;k3K9p`7v!C@< zXuDF|2Z^bJ)!{Jv0`ZKv(qLg9n7Js}i6P}{eJ!bfPk7vaIH*(>CE3KcRS6!R*! z`Ou?yop^&dLtG@@Bwj6!6z7Yh#fjovaiKU}oG1QFoFHB!jujV(Q^cv_WO0r-4ch#f z{^;gB^>(t{zWoP{vfiC~^TIjShB=cL&7=NFqbGP9qYek{-PUn>t1jDN^vovHYm#Gk zu)EkLc7J;K$JkHV6Y)Ruc&lLv_2CNMdP$r-dCnroHx=5iOr3n)bbIa8sdE=NDPoHm z3nx#t*Uy`mepafi4^+%FyxPtIRBZJraiVBYm}?XwGSSUAsKeckm7 zuCp&LLgP5b1KGT}P&!~lmQzwXvMkG~>Qgp4%eiIbh|(;)5?+eFp0&aMy{7fWHl@In zNZY&zWB&JyqR-vvd85bt@4iA`xCwu^ZvUF#|IUbDJp1lC{k*#Pd+Wj{*6AOq``?T} znRG_f9dCJEyp+1_vf#ESs_y^l;Qwwl;Xd}utIL-ewJ_-n)3)O@;Vp(oEMM>O{j!{+ zeR_||a#r^l0gn#sJsu7oIw;H8JYp!{K-)aZgdQC=Vj$Gs9^RW6#CtYHq-cI-JjBrW zWt$NZ!&EUsq>Q795hKM|(UdV;rO#%x&B*a$W9YDDuAsx{NhAI@ZB`FQ_aS>jGFt)G zIYJj4>-7>Nh7po@auLT}dDq@zh}QOOw8G&!M&4}CKE`rv&ujc!&%d|%_X_{$t|pG6)_ZQ76r-xay|=g^3M z8yfj zMtCYc6Ft+2#~z*+`1h3dGQH_Y-7fA(ca%HY9pjF5tK6&IDehEvnmfsz=uUR4-EnTE zJAqb~rg_!HhgP7u`rV%8hno+*mixjdv5=L^sK8&J zChbR^$9Rf+-1(LBg!6x#C!JqAPdP8qdRyOYjwdD8^|)R) zj3?t9x0RdgwszaNZQXWmd$)som7C}0y9I7X#(~aT_sDLIHD71jSTQz@z3RHzr+A~z zT)Ue8e^FO?w z)U~{YXKLIFhP*Qgo!6;f@KK&ZkMaIyR)XOPuHqJ#`iOs&JZ#`WN}3txzhG#`628IR zOiafu=INIIHHL2fU(1_o{Len7{&Jkv&O7Yuaew2!?5?35?{Qyo*ShQ74eYzmJqgcw z!pK1k&ki;|=q`)z{@5lr9{x?UUS|zn(%~O@<$tqcv!nO*g72d_SkZGF=a$M`bqHC)y8 zyl?oLbEk8+8A0ZWwzJ!rJau)u!ftLaIKX8-i9668!5;N7tSe$Tis4s`Na8dySDJLX zvVW1=o%_H}AN9;e5ectf!n) zjB{V&sUm^$+>u#S4l#6>C{e#fUgi9e*lV4&%+1*C>}H>R&OY`z?ObG^NS@o%@g^x& zYd0I8(uO?p7AqEm1z{@}m`#Emad(4fx|;Wd&Dmb~PtF%xxbF&&_fwwgx13Y3w7*Us zgC5|&$>$g3^3+e4@SpYu|F&%ZJD>0$-D}6ZEuHSHVs3%MHKB&IW1VMt68SSe{h7M| zSL*)<+@X}lB6k?<<_?FY*ouP%4U~S6^n;}zV)S^CSgv@GG~toQnr+c+W9W5%mT58R z&**-<)$|`?HYJ8B@4=Y$VL^=vGe1M~td%S&p)R)3(AHv($FBP&q!|?LBHVM zrafU=n~xF?O)vhe>)1x#wEgoh4jepm_?x50j-NPr`pkD{zd!fG`3n~>T@K(1T#q*_ z+!x`GjEatljf+o6Olp+eI3=}7)3o%A%&cb3vs<*xX_ec$P1|>_SapNadPo6e&*1Uy_ZdxpNXEL_bdc=<7Q%7O$;->w6 z`WI6gr(f&Vxw+~8^b70Rvmhz>>%wnn&CrHlb#-B3VQ2H}GMowV{OH#F$zQYLUfxms ziu4=Y()_Z2;x{7C{00}-{TlK6iC9LJln|p|YHDg!+rLl zL*}&V*QHZw&(3izi;Jsgm6VkXt*96}vDfS_h5d&2>lc$A7B;abNpiM!!qGPa<6 zaPf3+UWekM>HRBG3wp-IW#@CzM~@oSZxWYi{&;h;BS%giJHTI_8Pm0(phNr8s);ko z$1WJVa9n;&W_fwv{Gk;cE9Nfrcer-^>}y6%9W`+5sHUA3GzZrJFfmR{OkW3x;roMivez>fC2QhyKII z^&d4pCMLR|Nl{j6k2d`pXJjR(WVcLh8u}%)yy~jt_~h0`@$(KHVpE!C#wIo@>>TdT z?c1V7)9_%#NKalPJ-T#B$txJ!q^IK~Cr3nF)wEwq*wt-&_v+{CJEm(^)9l=c z_JvJ*6pbF#WB8CxEnBt=9R{J#l^aI3+GjP%>z7uL+N0AyL~PnL@5fR9=j}fImUb&G zY}&DJpQ}0)=Qc~3G%>%a>9~v-Ea%3Vvlf|BI(yONIkuM=etbxdRbbJ_ zkJ_QV`4In^zCaTVZ(-MlWY;nMVnMw}aZG!FHiU^8)QfTQf6R-td^#Q}hEgShnZ07R zP5+}woq24QN!j%5g^nzr79Qg+Hc5vY&lp7>Z^gabXZ@G;r1iY@duHOjWo=-t=ML*D z=5e03&XZd|^W2))&2848lvFTvNu z*WA~}x7hc*Z@ceH-yz>g-wzQ^L}Wx#L|R0Ph;|X3B8nsWM+}V^7cnK`XA##$+#2!Q zh&>Sp{rUa{{#E{OBa0#*iF`8h`N(}yZKFCyb&u*3H8AR)s8vzhqrQwf6m=pxCwg}D zwb3_6-w}OJ^e>`+75z-~@1p+{y)Jq~^vBUVqW4B0jy@TEF2;%}in%Z5P|S&#?_&b7 zgJU0z{UY{2?6KJIVlTycclq_-%b25@w3Fw6TeRUHt|g2g(NpAGAS{sX;R;$fk`8i?oK+Hbgq%r z$k!;YQA(qUjh<|@r_rHCCmWqhZj#(QxlM9Ga#3<=^1$Sg$rF;NCeKM;lzdzAoymVl zem(g@W4Cc+ znA$WoJGE_U$JFA~zNv#!$D~e5ot?Tc_14s%r{0(Pdg}VrjZNa3jBWB*lTA&Xrv9c0 zO`9|w({y6f8BOOky`kxnrhiWhOKY1pGHqGf{b`S+ZBE;k_C?x(v}0+tX&2Ky=~3xP zc-@@ztI`Y8d#9JD4^JPLUY$NG{o(YF)3>L8nSLn!L`Iv8=^1k~uFtqFSL@EL-)`f!DQ@%oHph8y;_kLv z+hw$y-fne!U;EPbkG0>>{#g6k4&6H}?eLeYoU8g@wfw5T*lWC6?QFLSa@^c9fkK49_!Y=+k$R4b*t%iv?#x*ThXGTRYgaNPIYhDyDLK`%bI+T4uI=UY z>euV(-YLB&^nSSah0-3S_m*z$lh$WqpJ)2)>)W*N+`eo2e%&vsU$1^Q^?S45_x%g{ z&+Wgc|7-ov4CpwZXh7+J`v!ba<}a%#TVA%V?7gy0WuKLOUhXT8D^DrUDxXk3wftx0 z*OfnA{@e0Dl)qm7m-6?^KQ7;15nC~$;ujSk4U8I?IIzjU(F30z_~oFCL6w6Z7_@nC z=fO(`-#Pe!!H*7pa`1*B&X9;9aYM?7EFbd4(5^#g4t;d!=fj*~V}?C3?Ac)lh8-RD z!*FYO$?%24-x~h)@Jl1Qj#xJ0g%MwmY&~+|$oofb8M$-h$x*FG<&7FSYTT&mQ8Px( z9kpmw&8WLZtr+#_s8yr(jczl#BJ?+6^rX>qMlTut#OP;6|8Df^(VIpe9&^>08^$ad z^XQnb#(Kxbk4+z&J+|%Gg0bDk_8L22>@{Pr9edl@d&WLG_Q|oo8T-!I-IcMGrIo`f zCst0coLl*Hbt9dS-rCQch#>~|F!zF>aVN6tG+O~ z`Q)O>{U;Bd{P5%rQ#@1Rr}Uk2-INEXyfbCv)Rd|BP5rN_znc2=)Za|~!_-%&{(0)( zrhYPY`_w&C_fI`C_0+T;(?(9adD_Zpf1b8!+E>%QpPn$i+4Q{W{in~KzI^(F)8Cx_ z;q>j(4^KZoBXvfX8NFu=nlX3AiW%?EI5RV9X3LplX5KRM!I{s@{L{?MGq=t>HY;pa zn^_ZP&7M^=>zP^a&N@E3)$EGd*UY|m_OE9DcJ`{-ug-pJ_Frdjn*G`AU9-QMeR%eX z*=J{8y2f)&!(9sVx%vTArXx9C6iFkp z`+feXh|BGoXJxf$(X2_6)29y~_~zhuSsgpKZ~4n5g29j8BfU+T2kS-J(T{ ztkgiDaeDLS+0C2Xkge9hKMx*b0mta_pC2RE zK7I6zH!d}fq5hvZT=u^_+*!ZR8=L4o_s#ypbw@U>mZc=qN@itpER&HImX)!SA z*=~Z;TqRl}Y&&+K4UG(G!r7;W?-TuV8g^Y8!5FEb7{P5jwB2yjMs<4_O^w^cL|Gm@ zO8s%8+SrZ$QA19W{!#Pap*X=KnTr%Wzuwj+PEo${k?_ea}KXnP6g z(IYlCDmpRwWFT-kuxHQiua0JP$ZOTA)!wf?p0B_D>hL+_oSZ|u{=R+t_uqf}?ZLyx zjvY&D)Ap*oPMz|P9kWi~Q4`jzS-}eJmj}z(Dud2JAcae{t7cLwIG=Kt>8XP@oav**h%?}*`P_uO~q>@e3;Tk8fc-;rq^Z=1Kwin?_C@c(1&z2oFM zt~1fwIp>_?q@J83W-tJe0}&WRN=y<(ik4&rOP1t_JCH0}_U`(vz5CYoAAY;`^X{|! zv-jucCtJ3nNs(qI2oe}f&beoLI_G|0-P_&M-2g~{baTibt3%bf6;7S{&N*LgY;0~d zHMyh`36)B2&Ck!xtp&HQGINHY{`EJm2lEA`2S*A^E*=d>rTlC%8IO?{PSeTth3T0{ zgo@GHLUdtqac(IrR9dk2`|6ZJ&V&$lG$)@gOmGBpnkp2Z^I6o}c~A99GCOO*f>b9h z2=A#0S>g7z!_FoheqTAeZq}4DHRW`<{4}I(uNsuD;l6sDew|*2HiQ1}tFw6nZsG3i z?Ch3q_~Zlq{gjfHdEByO?v3jUWCSLJdrLcFwOTx(Z)@{<{eB#yT?T3|9$Z*jj*A7w zekn<+EJ{jNJxwX-Z!@FuIh49(GQ0ggWl_+oO{FsU5#)IeEp|pC`$;jGW_`K6_4Re3 zsCt4b$^72Wwv8UfcR~@pp5oG~-5mffq0Uascc0IR8DG^Kw5oQ66rZ1*aYpX9z2w&P z`!l;KZfZAn4^3NZo4(t8*+gZP^92}kRT=qm#d1mKZE{OGkJXDw@|7CB7%P9L^s5bS ztzP__@^{$zKpv}7l3K}_t??m)Ch<7lOkp+b#!$%HBly_s4A8T?I?{-S|$<*1%lO8qDR+TTfKOB zJnTC78d@5P1^0*sibkW`tE($3>GbU6{9ZaEbGNs*+X~U0)wvrtZrqxhe*69Rb2+g> z41+VbSvZ=+}yL&4utE)oc>Pkju z7Uc8!be{9=%6)Vh)l`m2wx*2Rg{wXn*OU)_|4lp%XM3CeSWonwkMl$%R^?Fj$PEsj zJce;v!E1GWdTGh+Zf_yDn!{mhbi0?9uHV2X%s}>x4-Qhc+Hq9O&&R|(g`%kbXcfd2oA<+<5I6Z)@CyMPngm)T0s9rTU5{wFgKnN8d+ypJXn{MOa zhm}vottpd=D?hLOzN_YQbxnD?rYx^1chr<+m9RQOD+X6nAG%lwqJG;2fm^{Myx!vTpR8+NUjrbA! zT+D2%88Eifd-!58e5-|m!_j&8_}TNv4i9y@EN+hn=v~~MOJdRo6x}&_9e4JdN z;C55iI}5|^!VM+uNs!CH2iK$%#6t;*-l0cu{B4z5n5A5v}UW>YIiwZ-a2@O8XD?48yk7NOin0BCR3@*UOYZj=W&`;O0`%ji0q7w z9XWhxq!U(qps%e>olJ%zxtv5Q7Kz|@YM}b)E26zO{^|RF_sWg`=V;+mg`vX9!f%Uz zA#3p38AS;zUMsg>pIU(#KSxWY8nw>jX>$9WDyWv(jackZw+D8-hn9OhssiWtzxzL4 zot&JWEzT)oFB*4-`&-D##qamFH#DTv;dr5t&E{##ej?$RT-n*)-ok5VVSQbv^Ey>3 z4JD)H!tegUpZ&#OSWKp(6kd(G>6L4X%)*j=?X!fb$XFi?h9czLg@acj z77m3%dwaq7&W#s;@Pi-x?46a}l-h@5TD_j8*6BYq;^gmkJ54gAN`)5Y=jK;KX|CGp zE{@fwP+!kI)}i;#LVvQeiUc_1pL^^cw~QnKNQ+c1aH3d zkN^0OKfkuRm)7{%jvvLhh!5Xr zEA>^C=iJPnQY+ zBgQ(6P|m><2<4 z3G}GWMRYo4G9(j)!HnVVZa`GlKDi{_jolM0;>12={k!&`@&WP{- zdq}4%N@td4Vp*ldYf&2RP0+S`5wueER;)EInyAz$J>@c)X9GeiU646(=+!DL7Kg8~ zvC*a1nyf~BhGVa@YLwhmDwWJo@!-zh-d-#(QL5DviQiATXl*pRzqqgvFG%5~G#NDt zxtKd)26kG&SmUHHD{Y=-vZxxxfL2eO7}ai0Ai>CcbYwo#mlq~#{Ji6_^6 z1UXcU9Czt0H!7^`E%Kn1WpPbeRxUHW8SvG+`-gk`0uF5W9cR5>K0h06AMWlZaVVW# zsZ>0XPG>UOmcjP1!_PhW&{@RB92sn*x@g)a(l)p~9*8u#m)VFn%Xap%SqJaU0Bd)KCCu%ipNXcOO33?NA6Np zKiX=_h!`w>PWGd&p<_p|j{A=uZxO{eH`W;t;?}}~Qt7nH6)F`c8sk$0A15z@kq=Q~ z+Au#C3g7(M53j}bI!$JIzGR?^85?nbW@=|bD&+58OQslOnjKXJ`MB@aDgnp)Zn*kT zUa?X)@eq}BjayURswu~6%95ILvZnlEO<7+qvo?$VaN)PF;Jr%AMo%iOvTW}7EmC!r zWuuQAyu;3b6yB(Ox@b)qKC7zFYkPxK$o~G9Fhe@>8IedTmdNF2&OUbhfuqO9hw2bQ z7pOM~a9fpFr4_1Fb#?76P1eHBN=V;B_0uB0WN!^9-#l$vSVM-o?BT!n%4=`F2hkzCdVXnnUn~>z z`Tz8bx%Ihg|Hfx`sK5L7kkC+;PzJxy9!GAFbHpD*SleyF-bL@`&`9O%AdOa*W##f8 z;W;HH6JI3FTJT?bB*zs9y1E{E?85QW4-Pcwl*w2+n$O$qbsb$ThX%U_pg}0K9$a!ME?1}XwDt6LcXTkpjU62=1FfyF9k0k4y}T4&TxP^E%r6Er z1xmlZTayfPU7q7kky31!Y#^XHR=`&*k+3@^4^0Cse z3LS@=Zp_ThZS2I-X<9OQ?0B2UVvl z;yPt*X%4uI{Jq7cB|d%uJ$Z$fgL`(uB2ua~8l6$j+nb-AUWKb92D^4C7)=)@9Qkz4 z+}%G|@ALb-)c#&5F3{PVnwp$ijZ~=PvO1-7%DyV*%(G8XdM9|>JG z@>SQgQ~p+|ri0&=-ieC=E->?@^bY^FUxjab2ND_CUlrRAGwmCz+fRu(+tqcybX!gT z`?hxuo|fIq4tCm-gtzz5E4S5>EWg9naGuYk8Na zb~{}#wAF$PP=FMI+2LzXWMi0{M!PJP*$!uv!UR-Vu8`bwOM?pw%evLI9j-MHX!03u zy#My(UPh{eTZ*D6-+THLe3e<|e1O zke#pzVlk?z#i7uO;;~3HEjGFuxL_uN7i}}i_V|;Kw>^SaEEtU@1R6~s(9vWyN)Zj9 zwrVXVy)qUEIHkGt`up#{zZm06Jpn2~OJ90AufFllVk)~bJ-4*78&5|<3Kc(_PN$N&bZ8!9^WOA&n9`yrrPLlxdkVNmJrCeG zuc@mur*6IW&c@8l+#;1vrgJM>n_=Xf(5lwfI-?Z($jsE_Qi5-3Wm_hBX#QlBK)X?_ znKVx-@px=1Nj?;ehzqHtpUl&Qz)fZ8-!t=h@wK(A1*_3%A9nsTgMF6o|776iSF9%j)>BpPN*R(|O_*(^vQD)#nQ%C)dk{J%BjS}AJF zoSN^~KE-P_pJ!^yiJEe{TqY-5A5u`0;mGvVawyIf7q-GVDkqe}6(izjqO+6mAFAg@F4`p4Lc{NE1lJ~S-n_NGwz4#L z>(;F;zLpkGO%0AU`4Bin3mGur)>P4h{0dqcTVCHs1G_ufNFtHgj3!fwSR&8M0*Fgz zITH9Vl)^lfM1rOS@n|Xuw*dZTL!HwIxCTZl5+CE6obdGYbdWDoOXcSJI-iP)6Mx7~ zny>YEJQgq?)3hd+$%cdBM1n^pG6E`{O64Y$0-;2YZOEvWaiMz)^gxD_PbXs0FhJW- zh_BM?v>LTS0zE-FomyT5$yi&Pn|lSB-l@^31gX#}*)|e-hib}& zn)1Q#CrJNnZx;%Q*#7SBZW5HPNNv(6WU=+->({Tpd3|9i2z|V_y_ZWxLg5hN;1t5p z#x1Pxw`LO=71F?T*i(fxZzmq#ftuk;)OcQ#N`}Lr4$zrVolZ^gv8!Zq-$CSzPOTPE zh^I4W(_1q$Gq;vwGEY-+&XBCM!8HW5tgo%cMY^UoyxKeZJ3AAJD6@wp5~=-UG9xh< z+S*#0{B{r7^MKFQdpv8Dk=8Op&)=K8HMJJ*Kg7-l74v50&dHUEcoSkiulNnNAIa(D zkW9wme2n&uqzgWV3?;o`CcddE8&6n%ht(WBs;xNwgi*qtO9PQ6uR;QlkJ@fWGFVS< zFQOfD_~Pw7AS^w4rFQz0a-Fqpuw~JQe10Gks}Xs2{GfuNG!m^GzgP!3yRTDC)-iQ=9zu#`t@M&le_;=&E9Hq2cySuyF z{P+QFgEzA6z#A#ro~)WM0wM3)6>Uh%Z4i&{XgnT?C6jPDCDUOzxuqs!N5_DWn+S6_ zEiKI*L!)O;oP6-i+2g|^-I}45bF-T< zXjwrzmlc|Pz6nJn72Mnn?H5O|Dn6ngl~rnm6Xh~9OR$Kb{)%E|rAA|Wd3{lhA3jS; zv-7_;a`NoFxrC>0Z1eaLsog_TFgrfD1>1HMo)any^%r56No6PBaZZjvu*xSoMV%;? zkDzULI$c-yL2bHe-Lj>paGM6GY3pB5*I2H&i1O?r(s{k)k%or*yPE?ofd-G)k2zFV z*V@n!kMC|Hym=?BGebqSwn9n4{pD!KXl63R_ZW+EZJsW8o!DhK(A7oaMA%V&`QP3z zneY>!kKj#kLHaNuma9wp3WE$VTa~^sd24ZF<<`4f&eA7HIK?O09Ad0zmKImmN>hND znO6>;A)C!HGrWMUskg7Ur@yzS)!#&tV3Due*4VhayA6NE?jA>NYwzvpfudk_1$Nlm zg`@>(N+uImDomOcgui#==Hyx=xUyIhq*9ywSsD8$mcT^0#A*V2O}W3OtSXoPm^_z? z!Wk|j5E+d|gFz(|7oz+7x699t;GLdc!ahDiitRM*t%y9Zn9X`&aI*L)TQfIGeSOJNqXntSS=<2`t#-KZtt_!gYX2uoGr-@DJtHZg)5Iz+kf*URR~kWH&fkQHE9+>FW~-B7oYq^GZ_-Zz-+bqBLKJG(*woei%^> zkw`eY6N$tHS}hm_eAumV^iXM)#(D(F;hmngwnBafR`_aCtns(BIAu7Fl=sd9t%}BB zZWj2w33E15T+fifr7k2^A;a{X%2teI_zYU_x_0fYw}3OiA$FUgE0#z!GFfP25s2s*;NmFpv7-@g2>$31f7iG zK-cG}c3R1_#OxYVQ?T$R_`guF3%^W7UO#kPxQ9(MNs1%6$rL;N*m zOS$|d$TnbfAmcTWvf1rUuZ5h)3S7V?WLQ!_nfM}#>-X~t@o-9s)DEp&Mqj1TvN+W-+iLY2upI8Nqp zXIIzg;9$|;ED+=b);gOYD|Wyf>gdq44xNAEi6<_fz3}u4pZnbBo_T_LoTlr9A|Oi~ z4IWX;O>S;)0U=)wW`pxUD#LEr?qvn<*Ay_MddQPhl+?W zRlBN)R9XGb?cX8QR)0r&@DAHDA8%By>H{Ao`?;e0`JJANRLB0BoZAb)iqM^#t97Ko#~lbS~EAh7B95727sy$9GaQEIX6r5 z=ce9#FUXDqgJf<-_9HOVxlU(Gv(IEu%MsKX?(?|p7K2^~gb3#(u~?~5t96}w61hw& zmGN?gdPHJe5lk=4Orja-+}w5+AwO!V0QqbxkRm8`>T8pvHNo8cwV(a)=d}2xAHVeO zjo1I`Km2j!3;=^_Ro~#ou&OfnVynsr-~S?+wQ4nVR%kbt+|LC8+$fchE}DL(!zP_^}4jt=-TwswaM98S~54c!nIA`oWlR( zQRcT(yaqC5X^Ge0=$*GVSiC+~>MxL1ZF-qZW^_54$tzIjwOP&5Y&x085HvTp_w?55 zgxOpw7KIEZ6CA*Hr_+uMCfcMjIH7Wy?RMlSxP5`vHr4*Z!c8=)n8P>;J(=y`%H$37 zr*Lj{V|Q&cR&}QR9_px^N2!`}w5Hr#Q?C8}TQ#5O%Vowtn#sKO`fD$x5R{;gCHVZY z(Y|3(F1#C5$|xRvMe15d>gw|H`ffN(OBd_Bb|h6;bxLLB=w8J)D*N~KgN6oPDYXJ=@apR3 zvM{qc4+q^wL8(A4?Ivx5L5VQ%SR%zqhqksh5mt~Zq)|a6jet9M2J0sxMXeG^A&%o^ zxR8iw4GNJcy0N~M;3?EB-pXW_Y;SA<8{OL6-X&YZc5vh7%P+tD)_kyl#V?9eH)$b4 zT;s`fenMBsBoTSe=f_gn==SzbCK1mG*qVr*qmfh1_A|jc2gttC%I5!hJBNW2kOb!H z;MPMJ@;)}FIhjmyn4D&0Fl*5{B=--AfQarC;3|TAeuB+vE`@e!gRn%|L}r&X8$@pE z=4PcN4MjT>gNV_Y#a5zXHv9Z_3~7={!i%z}jOvQI~>-P^Jq?@(tZI5#=J zpA}^HlURo}*^bUTRDXxqfz!9kuND-L^5bHA_O9|zXbS0AjP1m-`0Tl!$+N2Mh0)Ra z2Dh`Wx~quTx++0^UPmS)mtiz2Qa6h_?J{IVLQGdzbtSbemHJz~A(O3FC;Z&K&;I~* zk9D-zO%6vxgF}iQNO5g$P*Azub?rSp!^6XUoi!Ohx3|kDd8b`;3j#573~yM2xxkCS zeTD=H)QA^V*xL-H??EWGC&?L$ZZE|5_M(|wCLN8|*Y|efXp6X@-d>a0(Wo@TPk-Z; z>o_#jxlLxPPN5-*KT^yl$=b_5_}97Fd0N6mDUva&r>WFcb&B$O{TfOzGc~<%{YS6t zSu5u?w>Ym^I|EFfRoeF;`}ZBvUqFkvR9sX@@4xrn)GjQ1a9^lIayztwL@JdRghEiV zT{v>&$awd0c z3@Y#-0}6-NKnr!6gi?9iS}k^!kVEaSF3!`cnVH$y<+(Ris?vg0GEa%*xTUS7Qg+IXkYHD!RV5yBp?f6rAELBj>q3yT?ey6-t?cmqKVv#yTRC zsi`pHlnYsf)t1j2jBc;h_E0GESLX*jJ!eTsPil>&CbJ(*NX#p ze}5_!jbr12e;}3W?;jzIddPA{iZw)rOAB#ELmd)Mcyn_T;Zc+sDbrjoPYzH=g>67< zvezL($fd#R;b!(@6H<>`$IX4;>vLn|Bus$}^gJz{pAY7F=O(8~AuzuX<+ZkWJpK&y zxLjjED<+LbtJji1CZkrT(O^A04H~&rTEt8QWJTdwSWU}5+n<)7?U%{eH<T0GekPhGt9;Ndnmu1%$Boy6cB z8tm_YBNf5!Zk?b&mS6Rlu<6Oz$=8D~{q4_|i*t^Zs;?*&Te4M^){CXjLP7xLl`I~9 ze|HDFeYjfu{`K|ADQvU-V~6^fGzKPz8mHe6nhXdJ1)T&|)W*hSRm2}FiKj}ESXD_{ zEd3hf8WyD2VREd-IWP>QB0i5VmMgitGdET;Ss=SvvDoNzxY`4Ho-L7!r$wSehfW{s z>+|~Bo2d@k0g6yhL)+0K;{)xjUb_`QiOvxE@gIHn@ArbU3*Z@fPqC3K!9J)9H8py&J2h!@{~dgbTOGp?rm&JrJI}FbbM=2vZB5U?aN5`jW?^&T#GFFd5++qo7jUa4i$oH_r@`Rf z^43;kV}B3hYN+#dG&WK``ihnrnnDT&-Bx-N6exspMK?dcl~w5E8mrY}x0N(t)kI_G z_vg`m4{}LcIs}4vibNxb#nQY$aP;V<2Qj^q;W*stS$IAp>GZ(Bi3f<&rnmRR32F>M zH#}}}7iO4Vu-r6^ipB0@u>?x*VLc)lzo*sh^>SD}#`qknXJ|3wUnNoK$xEl>ibUtn zKk@ikyw05QJH2Ea@G?6b&p!JrU;gx`KP|?;Pk-UrXQ`)3Gq*%H!wP{N!)k3Vy{9h< z;Y9k8$=ptB7AdHw3IQonp#Pho)WL&&NQWOsdn>kfiPmMfN6t<|f$TIhiRhDl*#VToWao15G7 zYimHd$k5R$ubV?1434LiJ60OFqKH19+1yxskDW8mqxCL!A9DEXT~^F7gD#T^hRCUj z&50N!R&|$CWe{Mw?@(fMYd@t!Iy#vORDnwE?}igOVo=LdC6XFiQ?{q=`Op%6H#f)5 zm%k($iDY!b}z;}2g1Q$YLB7}@qZo9d2^ zwRsvGIC2P0)}|qRbmS;}@33M#@^o%~eqm*Gh2+N6%A{ov9OVw;*9A&g|DTXvYO`Z! za`lX!!0Y6(<0HL&Lx%<%8$bQ&uYB?8r=Nc6xz9cGsZV|C@kbu=`;8=o>$ztidGw(N zI(*)z9zXx+3GBhL)+47M`o!bpyVS$juVe;)alaxPmUwfU>{lhB3ds;NjBZ9~>{uJj zjzzgV9&cS)#5oKrJC6U09mmR@skrVLlrW9-_3rK?V>tVE^bd9cD{OA4i^XtSOe8Rx zY|deaw%OHahMASu+3K2_$#iu!<`>L)+>(TmsZ_8&ynjRG|TP-@Wo&m;|;&COXis+}6_KVuGX7>5M|73)=-W5(ozjAgqY& zfku07s>fH?i3yFdg3FSJSwE+{U-isROD^`y;(9wotU0zh!i|2V< zDC?+**-1ULo7wfZcT$OsL@Jr05Lc&KnVkdi+tN}noz06>?3fZPc0i?xY=!r8>5bLp zj8TWME|1&2wpNp+$0psm>pJ_p`+H#e^ady=J)sdaz**r;1c^RI{vPTCBpRXS3W_b=W&c{fA`r7Ji zFTMzni0y;Ffh6=8yliP1uYhF}S_3nlzOm8iY-=CAaQUfIr_Y`}_1J}T7cX6MdEDMc zY$)x=$?MpKlP9IpOaaDfU8^?G9O&q1>+o?XnL+>Q$Hs@LOY{{Dd+d}8W^#HEqky=Y9u<~7U=+0xdTLEyQSObnLxffKOEVsK3S)7 z>ubTiFkdC(&~}f%qt%j32!-1vVkUISKfV0!wd>c>C5+GM3Yk$$OZx&?h|b2wk>lfS z7QJ4u51{Va&tLlIf0(@&J_&A-pu)DfxfKNJh!>mFD zB$v@Mok7o6R_zT&BXAmc-7bz{RvMOJv?#s=v}gICy5{Ug%HE}Yrx%u0o{ znr!Kg3-L~5O`^7rzC&YUR5xwl@P+WECL^gT%B7l}g;j72WmBnlN|a6D)+zVSeNkJwD_?l-i8IH}o;%mtj_Al^XD&TbUdw{s-UlC~p08cM z6(W>yv62W3Ym}ZNmQ_6h6*L-)HUqN@XEG*Z@eEsiO!dM48`=Z7FPFR9dxsD8v^Tkx zV$8x&cn`S+N&_)M>*@r|SyoP~e>gk)gK7)=!(_V~qt*9Fe~HDZER1leOjfqSFX0Y| zafDzi6=JzO5Ewmo`LR!4I(K5Ur5Bg0 z;Hm?oyq0B)^PwzTOl*)wE0d^eDeUezumH|#>F)p@g8`09;BQu_yV)yjw;eFxv1dqZ z-vG~x*5x1Ebw(KskcG+YJ!L>#$aLSM~Xqpj(>T z;H`%Hh1{!7cJ!U#FIHX#LDCy|+&iIacahWG(gGluo3cvpRB?@dD2`l<$7M?uNAAx_ zAA#341;{GH5lM7Nm}qbt4bA|LLrr#t2>wBl26nXB?y|RalPF|D)C=g(575(M(Ti+) zp%ON*7>B@N*f}Fjh})Pz^17gq%}tCI1e>cX*to(X9a4^=4}p*xB^-eOvi4MC z2W%0IjcScltg13>NkoBazeE;MavqgTSVBNig|K>x=No3N%4815$H-=ebhPH5=jMJ0 zl?J+tkCax;&N4A0)W2Ir@J5HQDb@syQ>Wu@UkH$v1LZSv3 z;BuI?Mmt`JNYGiWpZLV{&*Cun$iojm|2)t-1~x?hL!}l8Kn(l)i!&ex0gv&AAcqWN zhSOy>IzAF)+Sdobl+m8|0h*Rnr`H43$;@jeW01?W+dq;J%wE?@o1&Oo>|Xa#v0DZ^ zE}jX7`}!IXM?gY3!3;&hxAzg1mX=v<3`h4J7(yBBm{HUmgkw~nxs7~~e3CQw$|?ig z)?$O~39mRBZXf6kHn-8-2ex;cprfO}8kpIDQ@)my-;I4hi$%b+Qq-c|$n0Z{Hmwf2 zO=qYd>2B);M%~?+&Kes%I(z?x3m3-Q{S7{_pAYx7H+2ZtmRHxpDFKNrQi((|nMz~t z<@bQ5rkxfS3P{k>^3$1#KAsTiO2O?NM5HXtzxnc#s1Yy%rT$FB1#db@w$$J^0w?o*aGP#PJig?zIo|$x%VQvt>ANpnXJB670b>{c(TN9P|q#4PxWhH=zKK%+AX*f347@mOs z4!2sJz^Bn@d~1Kdy?qq;4$)NssHy@Fm*Bxpp!FQ5!>*A??_V2E;K*CroBoiDQuAQD z%3wB`S;A9M!v@2fYI6e7_p!vOm9(h`q^S}qC{uezNBE#jMUX^kz6cn4(@9#yP^JFx ze_S0ee6sL$(OJ8U@$%)N3{&1DZW`?;c~?5HUxu^CtXUJCCN zTV`x6m6{uj#1iR^<(<7Izp7f@+#Y0v7$Xt`h0rNlT+}wI9F3_|t#wURVO4B=07W9A zSZ(`%l9kk`kpg$-WJCLTmDOk|lNa65?!27=X+Y9k*9zFZw}EP-jU1cD?XYS!PAh+N zlBF*~tHkYCX@TC(fut;QAaxA)w%0efl&pMz2Kf|x{>6*`etb;Ygw-0S{vA1)m`EWZ z4=Fpje02W$7P0x8Da9$lU<6w6Xca&qQ!`N=}z(4n)Z5Ie}Y=ZSZ|8qHW+yGdeo@P!T^ z{PwiExSlm4ZAp|`YoQvsKx7mSTAN#Bh}hE`YejwG3lBYf{_N%ZwxS-zuE@q;vH;XQ zT2ZYfFZD_EgHI}LaVurXdXvS3WbIs@uQb8yGB|(p*8Fr%AnL?nwHcU_-(Tk-_hTts zUN22$t}3#WA0hTi>-E+5TD$9tJHs$)v`caGmP|smd2#cW%SG$6-gLU;K03ip5y{thDD zJO0D&tp_(Lv3)2m_92<)0~(Nx6bkXVS8h=@i;Tlp%Y{f9v+H;cn@XW!FxHk(Po z4`Y>uc?xp_jF<)!Qr`M-6qG4b2${)A6dIL6DhRJ3G(8&Iiz@^IT$PTbE{Oo<6SfO^ zej5zu8#%eBzGOp@XtzW8Jjejy7|l|#T;2eRgdSYuJbIkmN7L8l?}Rscs2RRRPgf^$ zL3_J;EoL>aYQ05YhZO)8VX4n2k^zrFZ287_B>8Msz)hx*dr3QxMhI}lBFK@*`moFG z1jtRr;=5q>SPhkM!b6b3Q2?aJ#1(@*CYNhy03EV_1Sxy{1Lw~(1i(z@eDoj3*5i8_MM`y1e*Q^bmK8^cU2FFjH z1nWwcjBp{BOJuY4^)k8IH;kytzW#IPVCW;)pdhICc|0E3&cf;}_+DOrnYlO!*PiV} zz-;_sGLU>Ex<@KObgCTQWU2J<;iE?oL~;1=`1$kXrcbriYV%2i0%!^Z?s*#P>wTaf zulG~u5YB*zoi>-*YT_)<)AHHbHPZ0`KXMV0R}BgBsY@ez?WOl7CnvY}6UO$|jyBe^ zV%Jf&xQ^IWHc{p>V#_<_@=qb7KqgZvc`>>rnJjP>2&x?&J%8%ZkOBCJ&Dzj#>ePkH zmo8m?a;UGlzPI<8XP$cQOJDugZ~Xc=b(Ny)f9tot{Oq&OKmXv#Q>RWGJA=zIac}jR zGj(-f`nK6zjjag5Z1z!Kg&W4_Yj9xo%0}>P7 zePHWpKICw;HMxK15u{N?8Ig24ueUj zB_0xoRtl6IHE0B>BmniCET5;re?)P3BA!qwL!NgbTZ*bCcFLjd{ytpuULu!4J~Ms~ zx(V>=rMr2$26LblS$Dwrnwz^@TanlekS`p@-*zYzfPKJrLNQyp_>iTgt(y7Q_T$A_ zz*6^At2qi>{;7dA13N;PA_Bc>rQ4smyV!{H|G>whT|6G>x*O~39C}=HtRX=PDh>Wm@2IP9 zberTncFtcR&qa8Uz@8e%-(ECIvX$XF`7o*(@XX*3G2mc>saAQFbF$1e!@D=GnX6@P znJ+-H9)}KEMrm>(X9PzmXPt*IbzHo7`7+SWix)3F^pM43G=XiyY%wDlrK_XfVWA$R zb-A3@+uGCJ-Q+RpkZP&6N}viP<=*ra)6`yl@j84r;oQRHMy_a8eV(+60TK#%AW#8l z<8*of?ArwoKYZ!JgXb?^JaNM3^VijTdzB`m)#h|LJRSnElCVm#uij)@TZ6Zsu!Y&I z79$vk*H*Wa_4QsK&G-2={9RhMvbr!oJAGYm0O-%|G)&}V|IlDxaSu6AW%BI`fySm!j z5ogxf-A%FyL8XS9PTLD4+({hjm-fBx;Hg8H8&%}ugGk% zxsGtL_kQ@sJD9A>*_pVG5B;E!v3p9zFDmDkq7SS%rNgE!eJIDh}JAw7fL^MPCyW$51&N zTU)z(y0ANSb&CZ;<|Ze2Z+d%&Muta5MuvwE)Ii;tGw08rIeYfp*>gPDP)L!wz4`t_ ztxXN}ZZ#$mNm*uP>Oa(0`DSG&uqQ~yw=p_4GNoa{j*gBE_jPsmwzqaQAf3q;U>x#2L|HO6erb=! z{=K|1BbO_baw&g8&06_Z5eGPGECwsT0-39A2{`*e_sBS`kwg96-TgzuP1xt0FeV^D z&*vzVX}N`WE%IyGfxSQrL+UE=CA2ghGfK ztF_!wNNQqx;#u@W=gBi?PM$n9FbM}&19XobnY;deyZ!q}> zhK`*?UsOH)RXI8Qy!zq|=r*F<)My!!RESd4-v14)InB!Q{^+vVZ6=-GWHTCn>6gCn z`Om%Z!WVz-C>XM?;-7EV{q{G%hP!IeCot0M+Pga28$H&~zVMab_~PY<9)A4mzxwkHXhR4y!*SNwNSK3|_mSUC2=kiThmVgUhp>56Jl@)F z&gae4ks2v*I|u0}hH{`h7S)qh$M#)m6r(Yva$~E~);AV)DdA0S%Vxv-_rqz-+v4+g z?v>b>U89UQgU4l9o1{j-lWw2iC=^go_$i)JtB`GPE0j`?%cMp+gIXnBS(#d(_%vGl{5~!?Br(4U*p!=8OFs#K1T$_maUM^>Xm(XZRr(ky1J77<|bNPVRkjb*V z7e-9@kvD3YG({qz5a2YXn2_Fj_gcw1XNlxqLE8*2 zD;LR~sZ=^0qhbP(H|PlZ20O)X%yA2Rfuw-_aRNKab|S_~1@-6hA!mqNNQG&Hgc}Wu z^GmDCtIJDkDUN}iqu<4>9Fq&Fc#a!NtV~0U`F(+=0kIA(unK|J!LbrI)b{sG4GA@TQ(PrvmoyqR?7sB!E7#{)F<3M0jW<_TmKLW< z6yWNWmDk=#MhR8IK1ZJw3yOO(iF+6gh1TG3w>&F#mzo53WptB7PU*AO;U#XNph9tv&ZeJGg^+y%Z5DQw^j zNrx$^!Df+*%{r^AwY96OzoX9A_UMQ}z<*yTqqpC&W@$)}!v1hKB22;^+*Mk4vh;r@q@9_r~Fx%`EvFP%IOtzKcu zR4oUCRvX#{hnY!j>pRrSJn`y$@rhr5eF{&^4X*EJxDp%eS7|sYN`fHtNWkZ#F@0fz zheKi9Nl(k=~rHS`4{ir z3t^rW%PF#toTqe3!pUL|=Q%tvE|%l%bJO?6qepgRL3}He0?SVmSbh%mcG_$JXUt}; zLMm1mt+;Cyb~{usz;B{L^9oCV*zhIFAMG5Hb_)T_-S*C&I-AYX+H9OqZO-2O#mg_h zI|Mc~ ziTi<#)RnLi+-9EQiYHp|klEbR^D*$Ds~;U8Vm!}GWJ7l!q9=NB-!Oonj`1T$@OSEg z2P6`qNP=ra@y8Jg4<9~q1OVY_vtGj2>0tI9IeX#B#~y#?fhMO_=J z#zLvxtpm2(8pMdTc1l#aV0{8rE`cwc<7dwfHUda2&T;09avOHi4x8rbwt&mp)b!9p zmp}38=f3u(FMZ=zzV^$X`NSiSj1G+rjvqaGyo{Z6b@5J}qMo@o0EJ&7>(xf9$a<+| zskE3qLos_sDkYUdF5J706MHIy6y~hVZ0hVB6!DS~E*Bvvox@}2PM*4O_S}h)&h~c1 zk7?oHaWOkvXMY#B3(&LvMlkq3Yx7p|>6ZHCLn(xr(IftV5fGsR#Z{{{>XH?F`ZSE> zOK!VHZnGiZ2)UQfAz$aS&prJxKImv^Ywqst9U%Eh)aBB7|D~UlOk&nvEBLL0N-> z?4?zTm7MH=0r@i(XbY1$SlM(xYs*0M#IcP>A`SxQ@!q*L|O z#Y5?Ey|d(*_*1kv76|Bc27|>S#TDN;yDRhnlugj6ZN&G}8n|P#m5`I73UeBQGnm6c zj-`Zf%M&@@0%dhn!K2(>yEFoGhl684QJ*{pNjNJj^nKl|@}2v+ zS>+U3@FDf6spIY-ThsVBKsF{mwH9cDmy7}0Dtu1_HI$K=3L}(d({q>D^sF{p6_*~m zt)&I`e<8wyMc{=+UOhN12(KQwl&i?9CuLamWEFGwd9?`-3eQ|+nd05cPEZK-EACsaK0N^bVtf^-uXPB-K9T(C{H$5Cdw-zV84PV9$?U> zNF)eY{s0hV?CcD(;R5iYs&zW8R;N{~&zw1T?lf?(vu6*$C55jZDH;bzsSY$57{Pv| z{lGDxyes;~@BvzFMuR;Z-U;IDgIjC!Wq`2(O0>>mEHEw@vG@RBg!i_WeQxkv>jiuU zWSoBUt+yGF5%g>%UAF`>RzXVa9-stN+~J59R~?H$myiHvzj8G zPB_E_(EB7)iPbsxc5MXMzcJa15|P-q;U0Y|n$Bc-axL6}^$j>=0xMP)K-H2K8k*q? z9cXQ^E0MP4K(A<#^`g}Rt}%NW8<5iySX&DfK+L4qBZ^od0C`*@NfIUCz>gzY7_cxQ zcxErM`5vxWiLbHe6yggUQ>mJpKUV8kKSJ~DPR))*TM?CE5epZ5pe%0xdqg@}HX{&< z(rJ~+SI=yX17kx&>>ffO&OH3l;I$h7E`3-jE#O*tq`nUvIsVY8ht7|YKuxX%_<$~B zF{{)DhXy2nvNbCdW%ez1^su?H3X9e2tlh0F+`;p2sn}|985M? zHE`3bSy%wOSZ?|Ti=^qmbi^^uZN)2n^l%WcPIR0~sa=>f#d*r)VR&3kox~$cI2`W| z*bpF>c&$p-R_pN*+z!e-KD!qb4V9`5oVCNXT2{2;^N%!u(~9kVCbBM(`02mgsMSz@ zMnf5H*HBp4V87DB52DF zi+;nh)9@r?M3KQRdI7~{7a-IHyoOMaJmNB>#f-nO4l0CwMs^J(sM0;GY&l*ovnwW5 zwPJ$h7=;U1F%3G6l$*`1E#vYYwaK81L~z%EA4DZk7h0W8DhREEN7X@zaMMtMtM&W2 zi#KlESWXbgDw&;7BE|!EXn_IVCKl()_b{1a2jBZy-2=d93 z>j|Lkwd>Us<^`p-3K4O(x-vU2GpGz=}D|_D!j|N*N zj4hpqhk62?eT^*+quF@=!m*J9ZE$;Z5Q!ebF-Ka~2;vm2;&Cdq0HTG|rQjnHB}I{F zqM{~XPFTUYYtzBqy@>GMjU}%co0e8iqJC8wi$<xY_4p!g-c!X~fR;Wo;+ zMJX6(%xEu)0>a!|U#B2I~o-WGbX{LMkWM%Ro~z;ZH|H@CgJX`_UK)dQMU}K^d@E z(qIK7(U?XKmM5zrn0UX@fv(2(p89}ETOADXL7UKp(H=M<5vxV8;nF7XA)63;-X;4( zvCmoklPl^Uwp3!iBUWVJPk;LV{q(0l{lzcvpP&8gU;p{%KY#Js+-xEt7AF!L@Ls^G zjN_QQ7Ywhz_4Yf{TYH2#Jxa|2f+INb=4LfUEZ*3Ze~6dx_t7dnW-=3(QV!fKAMG*< zUY!=J^NLPt)3F#x)df5EaUlg`*^-@FQt!5^O?IWWkfK`=K!|i9?8(GYTC;B-gnGnq z!$7?qQ62|7HMKayZim&IW5q^Q#AbJPD@KV~8Q}XLJbd4F%EgPN+|~QDQ!37gB&McU z6yFA!AHWsDGA)pbJxvZooZ%k8eZ;rV2eWyB%3>8I=iYqt&FQq<1@p9?2X=9qt90yN z4e-ShwbkvlsbzAFR*E&MK^1)PmT-9p4Dl+d*!%ClznKUP4|~~F@if|O&+|Cgk9XJi z2pQ*ISb_^sqfux>troG(h`hjsS?vgww#&J4r-?A)(;B`+D#XnOYcorPQ92@of6b`m zBL^X$#RUNczTD{dVHIUB3ME$UtjjfB$Db^UUYI^0i<8))znf#lzH<`ma9s+zT&Ugn#7JiF1!! zzI^%8<%b@B=pj&80Tn!mMTEZ`*M@kS9(d-lUX7RyM%2t8z6ZgGw?l+wqgf<(1w5?R zL{Slp2)8-0xNQnvDIAeK129&;RZ+AQ5GCjI)qC{{J%$1oeVCNsuaJqN!dkP zyidIy#8#k{_;g!olEO6gK3HZIY$;S5Vm(jER8m&UE z+Yat*@8BBAic2>NTAKla@lk9oVAg<$nq0t%tGzdH>7`7sY=m}Fi-L&hT3=n8UtQhhNcp%~ z(&f@P+-CD8k}cL_yUBD47YFKa0Z1fNzW)P~==TM!fhN1wRM%Gdwq}>xOzs7#e6RXm zkO^)iUWyv~Ya%TJXH>R!pW^FplOMrk&6T}KTAcXk@>lopqmVgZ&oK0e6+PT+)_XgU z-9HdkXw+lO3Rgs%I+}XW(}Frpp)|ol+$IG@USvsajC|^uIS6Czp+uo z>>nJ~CxILJ{O-09aMca8^fYO8dS_clKO&t65O_jMm5MiByEQj6O(JqWr0%PvzX!TH zTALsnVp)1SI)GV5nfO}fp3$_#h`AYP>+D915-{+Ap`HdUOhkpHse9=3!w)}vdWb^b z($YkbjbR|#fW0`bQp+Ne^Mk?|80~WmvUooENrT})TikB@!|qo{WwJ<3d{d>Z&#t&X zCr{=P6Df$!6A37|u>2qSZ2!k!3jZ?3OpQ`;M%m4yYq8naPma=T#f7?GI9i^X79 ziQZV#B3ecZdNhuWWDm5oXmA%VCmanUAxxlAIO~9+a*KS4e}HzzbTT}fLT@xk^U&`J z36GDs2dOxo(8~4ZmH-K3K_CkVM$G|_NzH?o6`mc!T`iDE0k`S|dV}6Xd1<;y0b(ND z6x3TM+qv=H^>?oAMo}{wM*`Kx^8DOpMqnc@k${pvp_5ty9Y@FOISZ4M@4t8by`?B$ zEGRxb$rK8RL>%mIxH66C2|Xeh$c-X}7}z&9cN1KZ(&9#@pU0+?Q!4t8cy3(JFdr zzy&Cpix|2ncxC9I5Dy9{f+0m|xeTESff=chV+iar6LvELGM*_PvLxfv{UL1EDDdf@UtgRs4+BE38m3Q zhA{(zs~Z`QXu?{mi!9I3YA7Cyc!7M_su~IH$F)t=GnsP8#JPx061y%p~AFxww@OF(0nSSiIU>>DWk8L6Ku^kBPytP~1q`6HSdj5t)DLh@1KF!vBB0LMMSnZop2N;oOV zD|}LLBOzr^mlWP&hp2Q74<0=&U=EavO=Ko*1~^%FI&30C1%qm@TqzKw%zTfP9KPUW zc5`^6R4rr9p%gasZa24gbX4$(5{OX}2@UAD<~+|4O&{~zoaOJ~?2W;ob^Q45XZYZs z15O+g8}iogP#9!3;u&Baaexbz6|Te2FTSzDB3V_*{9n&;qqn1JZ?B;JpfRuPqKGBn z{(UTqoet&|(tEaC&$cqc`3-p6QvlIGuaxc=&MF3Xxn(}TaesDt|6e-NnI19D8&0=W z+=2HF_6JlueUtG}Q%q_Md*o5e>F*mF#$?O&gbQ^tVlHZ~bcfZ7X?qjz zu3S4jtfd97lSmzTGK!7t<7%yl5@|eiwltJw0xHY(swf*J9rs_pnR=n5Atn-#TpK|-2-#1ejh!%KIo!wrN>w? zGjtfbof4Liz5T+`ZWe_~!}3u9-R^S4(e7}_;}&jSsP}0IRMqFSX;?k+ARF*Y_88#) zf_=DFA?yjARVj>t-!S`g4*l`h zYKLfJFp!-NH}Vnfx{5yw6`fMk7Ohyk5ZdH(#NhX>;4q zWl;CbIKxg@Q&b~O*(Tr1IAW(-l-=brj%-r4iKiS#L<^Q=Sm-oksTB$hrqlD(T%wPNH zSw^F#KL5!Ws+(iTXrovvHV3>STL&$Cq}G6e$Y3DKnR2PI$uvskv?!@gER{}GGDo>_ zomH`N!)B>m9I_Zy?68fT$qv;^!{uz<#HY3+9~#3en=RJb=5_C*Z`TzOZt8Zq3jw#2 z>k5w4@;ub})y0@k#R17CTOo;k;dqizHo++$ih8V?08wYreUK&~KMkiNkPZD-sZOyy zvdZu56e$s>bc+BDEw@MXrd_aUNa$6pHY<5&wqduM7By!Ok`nsLWCq+qZ&vLtjMvmk zGIin7jXBaT4ybmv{)@>;d96q8|J$q#*O;TS{Z|prbM7ors_g?@F^X?FY2i)G#I4f-4rmx#}c%4vIe&WplEaPde21I9J*!`OITl`WJl?e7r| zl6JY>tW;{P{*dr6x@5l!5RJ`Q21~ymN+$q?s9_gw2c&_w#{(i0P$7Hpr10dRR&8|5 z(MW3R68PYf$l_OAT4LE0zCj#|Ry>|!$En3d7DSW_@Vw`8JioX1L;OPSrW-5vA@DaCj0Y!&1{!Q-uKNa+n3+0A$x_X>^&*5joBjz=Ik-H zSl!&ZihIU}J*$1tvD$}&XtdSx`2~S?1@P-{*Zt9Gy9V~IU;QK}TbT48Tpm;@%^uop z2WJ0`{Um>j%6`#)@D6Y%_LIA&agWK!h7AZHA%ySa-Y0uhczqd^jsJEsu8ga=PN!bTV&TuCjvh&=1iBkOaL%~O zY7H*0hpD|!?(H`k=2gFmw$9B(MQA-Q|A(sdU$oO4Lo(W69g7I`(NCV3`_{0}^M&og4!;Si*k5kKyi&xK41p7)ruUk~9y_>` zidZp+TmAlaiDgUF zJzXrO@w-VC(m$GvX!0C%u|W3xV6x}mV9)gxAHwGL1wcU^)EppZxnDKRS5u zU_9;*H-qJ8Pw#&8(MNXy{PApiYqvBWKYIAtouB;2|MK%abHo^us63TRq@SC_M5cDl z=lyS_pP>A~QaA*qf=lH`P}GNlG znPLe2Ie8AaFGzV)yOujdgsuQrCG`DD%ht*sx3G>>YoI5iX<#}?qD5w_hdbN*`}ZGg zRgQ9U_34xQANzj#X{E{7k+f&!Tl=si_i7+k)o9@8r4@SIgIR^rsR43H;@xWb7<0o23wDw9``8O?$kxDeT+r=sL5E& zZxBb{?a0<SR0rxYXsZ&S4U6OelIb1MO< zcMPxFVZMn49Y6G-LpYpZ48meo!8cz&B1o2d2VK!Oi_-JdLc(KJU{8apOGWHRS5RYy z6Y>ql?n^+h%?8ymrKi&=nz!iikGNG;5?JD#@L*C%5>Q}Z@3%|EMz7w2q3IuXnuTXi zpYG;M^&0;5?AcMf+iJE3sH6_jycNo}rtmbiI;(TdgYBoEeSEjj;*ma_nykbBfjpJl zL52YXormaY80@jI&+Rh|w=fu|VJ?!PVsj#K5`qocKQ0^e<4}lZWME)#Y*;tukyvO` zV42S>56I9M>|G|EMkcR}@qn+E;IkR&{`HI)63pKh-U9$IKRz?sIo zJUDL4w9T$-Rt>JfsjRv8LO((E;`WBftTg%=BnDt)$)6II^h zf>{twMCXD)5oOtGVZR#6$vI69_NDoKAqBIz$q3;d@f8)|nMB-GJLK;=FR@ zOP4R7IlaENyo`z?uRk2Y)6XHtYB+Qc!+u3!7?PA|u_F~Dh?p$|j=%Kh|M@4M@BiY@ z{+(t;hA0?2nB#JG=de<(r7hJGP^1*Qng;9_>R^zt&6dk7T`fMoztiZIDx6U55f!&m zNh^rE;YU{>FDjGKL%c8HX1xrDQ5iEHqmSwt6 zaOqop+Vl`lG(n18fYQj8dS2dqCzBP{MO`(sktDRm7fUNE$SePWv68#72P?VTO@84q zgFVZ(D644=7yCJ8&^vgR%@-O2nuk9Zv21rcS7^XeXcTg7m){rkxNJNP?&mzv{o!8Y z#=VKWxI@0FW!wS+1<0ApZRl@gIzajF0)w~2 z1)&a`I;Rl1WV2fQsd&gA2**}tbgRwSsoK0Vv)CI5dR6H7>Fd1elp6cnyIcDQrDk_1 zsJdHs%H8@%ZRdA$MM8pP(E)-U4Ed!-zJz%1uwBYGBv2ei0$$ycU`MSof<^g`Iw}hQ zgm&tx03?gj!TuJcbwhHhQ*W}3*fM~P6*!C+mSJTmOmM72m1z`yXnY&8+TCOq!braO z{$y32=ucB-P-AWEPS-}_yI28rBAfx5s>pzZO(0WArbq}QpF3)@VhBOB1-BD%`G8nRduN02XA97nLo@=qQ=J}j4N^*s21AD0>D3~*~gA$G{K(+o2be>fj!DB`xK zn0Pqo2A&W-3`hKEE)s@aE@|#iMB{rzJT?0k%|~j*Ru*Uc$o4y1M@c_5fO@Uy3O<{n z%P5{5P!_ALUIJimzfp#7SYt=sa+bJY%H0vG`KNUm{|arp91eFh66CnV(oE2eTiARN z2fvwu7OyC$PcKK@o{bH+CP?liAXCdiGSpPp!ktIcSv24Bamt96)RjH={h3 zbVvshZP_~PbPo;+1BL;+98{~BSh%g^9m?!qx(i(P*8^s3JX znw4Z|VFjMlbFaR7c{aKhLm2|dfNR!)f9*Wz-u!+P?)zw><6l)n(VzPTkO}TV3IKd zV^i!GyQAt}Wql(W@zU7`kN1wr;6PCApk-RHWGtL}8Bz1XC18-8*?9fkx2_R&m? z0r1>E59Z^f6SGWoE?j)$#`*KVhny6}Xnxx4-vp-#3}>iQ8q;S-Ww=zDqU3P2S~Q)_ zPNesrlJ5du_|XWsU;z^Q0iaV09t{{@Vj!4;6K8g25fvf{pKoUE%&MPf+Lel3ms!W^ z+yCfafA3;4>i23Az9r919Mo{s`_sH8w)2hA<^lwM@$=6g?HpIyW2OOf^V!Z}9SAWs zD7H{{utE`6cQO`pZd#7-{`Avl@W*Ug6)f(4V1A7n#lqD3e-k}T{54ZWXg(Z1K~tgY zUHlKns`c(Ke){2uADUEmgn=5=5g&f^;a}ao3#jijeV8@J0Lf{d&1R!f^7||AjJvuj znyCL4=PoTtJOH}&HV&tE)0 zi#0wFvIj%316@tHa(V>H!e$FCoO;b>TUxf+mI2we$j@%TcD56K+PJfKm_w~X-yf*$ z>=`r@UNH0a#sUIm1G`&jHZ7mtegsZLgpeEvoizRaeqVsQg?5FE0Nh0W8WJ}*mAJ-M z^~6?oy7eBO;Bb^MW@Swzj@8!ayjO2x&qVzB}M1g<{aWD z4~{zHR(9w98-}9N5FEwLJeLz9w)r^U$PU z7F{}WJ^16}{>P^LVIQE&xytponAPwAgY3hI7x+%sJ zR4Qpx*8LNC9>8%KOBT5^%U)(PH>L0_CCahHz_nuD<3)enmvH{?!+agTj|_q5|Hz!O z!!#k5kh;7GIWUhTkW(v*R@GiA8R%6ryX7^o_sQj zPJ_1I_%E$sobAF!@%!=j$$T?r)}+a`SSxfn_$);WVgAf_+79VIc~s=+VQww9&0*v1kjb;`TzvFaSzuo zBk45TpT8?@H;m9mxNR%0(dFP;6-bXILR&H# zzlmP0oP=l(1{Y`Hy2o1PNd$vWp8Whag5|~vX>hN*k@kLKIXnsBj3odo^^>P3*iXNS zR^~y2IJ0UM zz<%G`-96ab*_sb{lQT2(3xvp-Hpf1s3*`nkBlxBI0pLMUBMSjoLJk3)=IQ+57V30_ zFJE7nn|bxyY3AwE>2>eox@DJx02fmLy#>R-aj^|0TZ2*qtJ?+CNVZok_N}UG7WKj@ zI1vF05BDu>8``4LrM<@w$AX3Kw0i)`Oh9v;a}M|Sc6av5Bi77lo@n8t^kfYD30k}! z;*~iN3Shulpi7ag2Ve$`t8n7LXt8<-GxuD~>4kmm1iYu)LqO`>cDI7G)9!%L0@4hj zjM+f(PXfi|5R*s?z}O$AFO3I1QUa4mc=$o50BD%c_nIt_hZ2a4b9m{JjAkwLXuP}} zRam-pOgtf7!Rk*fPv*i?(mzc{;7;Z^tBTm3+a{?ziBeSRU0x58NYi4%^D7@=`ET(I z&mRG7Y|BRHkiw0Ohe_v>@V&rugx(?EPI^cEKYdEFDqyL=a{s;|hhB^_zxZ!sC1j>+ zm?6K$<4%Sw55Mv{L)H%&W;pyG!&y?acn$o??s9JOTBs?f&WAD#gtZE zf~yYZ;2232NM8`(LunD`PN6lH;70N|Z5)D45R(*7jlfncC7oL=w>v(lqR$WJaPz9<^m-Bv7PvDg{*esH$%QtG6vjKcn%j?07%dH%NQ3rQ8J+Iuvjz&uRK!Ri%9`h6n=lNFj*sG=q3Lz|LH&f-S__44TPQk z2;{eaMEyZVhDWZ-*nBF3T*1ki~;`x!;zHxL|!Ft}6g^Z=rLduw>G37ZSy;C7UnQ?N)Z4`i z4#ST;oaQl*ToKk~7wNQ{2bCsaXixe*c6eySLbQ zJfLK#+AXGDs!isMIc~^61Ae!7qlYfPreEGj1R|^~aLKMU96eR*#`{J}!=W=18%kT@-~3hnW}Su*Tk6 z&Eq+-_X5uKlRR%~Cgt{ccz$WlkG#Sz2I*|n9mx2x^!ehaPj8T)xXXF|t*^cL&Oi9Z zo9EWgo2?ml@WKPfYN4f5OK?@4xLL-3e(RlgzlGFi4^VQT+pVod9d`2$ z#OjhTJ4St^@Y(HYpaZ)kNs)m}E?N}$*>nuwkQXjoPLQqON(gyTc@)X4!)N9SBT1b6 zgmo=GgG7s1at87S43=IbZ&<^t8pk~b__Q~4y2Zs7B_&@dijcZ-+A2E z9&%ZDTQIpC@VR_*XJ5Uvb~a=G?BV^re13PY*=x{Xs@#1##TzoN(8Ye2*1WS})N@9b zE?v8HA#+n_X%%a}*K7C8pT2f>eszj&@at$};Y(N7R?pq|H9$?DBH+f$Yk=N83%UC0 zm36q2mQJmo2igh*?dz{+7Oa{r0R8*YRX}-OymA%a5!@|ni%ZB!S>L$u`rE*DyLRRJ ztFK(XWIn|;v=I7xf9J+)Z~yvt-us8|e*4|;{?>PY`yc<#-~HCR-~G;SyaTf5U(1Bd zo4$GmNx?JtB9ZXQh3nU@e(9@U1+s*BA4D}}37-!Fn}rOPgpuy$=`PKK5@vQa?#3Uh zabx})v#HZ(I?%hE0k@EmTeahk1UoFn#*vjworNh2QT$Re*f`OeY))V4F^ zu1}D>8|SZ_PKN5+ow36g4XD;CXMEcIJ!UBjyb` ztiIHl3uiVm_I9rW25io5SM@DcwYR?V+{K@H6S z(lQ!?kPXEUMoAIlX*rI05RArwXS}|Ql!Ving-*XK*lc_(2_m^@B1tTZV8o-_;pFoI zfu3B2!uGk@_|$W2=udTcKt%9~BOW4Hgzj{Wo55StJT7T8v^kJQDDDVGxJh+F0W#?M4-O;ymK2D zFPy*r#v33qHSbI0*ZSVh=eK`RtAp|Q?%i(l@WBJb#F2{p*`rTx-+l)5E}#AU&Ye4e z^WH}OTNXK`h0OX(jjW;0>&&T*(^!w^mJs%eL+s7XuU;hI{LIGL^XJyr%odGoPrHqW zZN=Ffo(S?fBW|Y-b<<`?sbbil3Boj6J#+C@u=KtL`m6bw^XDN2f&YB&{F$lbCi#QD zWYmXL-$W`IOCp3RIJ^!OwLh#PXMAxlAp^E?Xhjr!GeFyq6Ips{-Ry;NJ=V8U6K6o# zr4fJwraNJ8a6^anRT-E;Mjc?i$}qA80dSB~^_tTWWN0)tchaTXErSfqu+bqF>Z!hC zjJ>dvrIpZZMo=*UKs;f#JhA2GOXZlW@xo~3ZWDi^?jyX?k#ZPS6PHYM07FqE2^X_P{1oRou?$?m%jKn@qOT{pTD9Tlr<+gKW1hZm*?jQ>BmhO zgYG%$$Fv$6q#tR<8__d9J?ufj*Nem-WoiB@*k~sD(ht%8b<`>lQWNBI_xlqtC4v|F z)vqGXNR)dx5?;G;?MEt#3dd}tpg?Z t3=+)GE@JF#YWy6c1d(Dunkel Dynamische Farben Erfordert Android 12 oder neuer + Schriftart für Überschriften + Schriftart für Text + Systemstandard + Atkinson Hyperlegible + Lora + JetBrains Mono + Datei auswählen… + Eigene Schriftart + Diese Datei konnte nicht als Schriftart gelesen werden Wochenstart Automatisch Zeitformat diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d121f90..4db478a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -274,6 +274,15 @@ Default view Dynamic colour Requires Android 12 or newer + Headings font + Body font + System default + Atkinson Hyperlegible + Lora + JetBrains Mono + Choose file… + Custom font + Couldn\'t read that file as a font Week starts on Automatic Time format diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt new file mode 100644 index 0000000..998ef53 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt @@ -0,0 +1,52 @@ +package de.jeanlucmakiola.calendula.ui.theme + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class FontsTest { + + @Test + fun `fromToken round-trips every bundled font`() { + BundledFont.entries.forEach { font -> + assertThat(BundledFont.fromToken(font.token)).isEqualTo(font) + } + } + + @Test + fun `fromToken is null for system, custom, and unknown tokens`() { + assertThat(BundledFont.fromToken(FONT_SYSTEM_TOKEN)).isNull() + assertThat(BundledFont.fromToken(FONT_CUSTOM_TOKEN)).isNull() + assertThat(BundledFont.fromToken("not-a-font")).isNull() + assertThat(BundledFont.fromToken(null)).isNull() + } + + @Test + fun `system for both roles returns the untouched default typography`() { + assertThat(calendulaTypography(brand = null, plain = null)) + .isSameInstanceAs(CalendulaTypography) + } + + @Test + fun `brand drives display and headline, plain drives title, body, and label`() { + val brand = BundledFont.Lora.family + val plain = BundledFont.JetBrainsMono.family + val typography = calendulaTypography(brand = brand, plain = plain) + + assertThat(typography.displayLarge.fontFamily).isEqualTo(brand) + assertThat(typography.headlineSmall.fontFamily).isEqualTo(brand) + assertThat(typography.titleLarge.fontFamily).isEqualTo(plain) + assertThat(typography.bodyMedium.fontFamily).isEqualTo(plain) + assertThat(typography.labelSmall.fontFamily).isEqualTo(plain) + } + + @Test + fun `a null role keeps that role's default family while the other is applied`() { + val plain = BundledFont.Lora.family + val typography = calendulaTypography(brand = null, plain = plain) + + // Brand styles keep the Material default; plain styles switch. + assertThat(typography.displayLarge.fontFamily) + .isEqualTo(CalendulaTypography.displayLarge.fontFamily) + assertThat(typography.bodyLarge.fontFamily).isEqualTo(plain) + } +} diff --git a/licenses/fonts/AtkinsonHyperlegible-OFL.txt b/licenses/fonts/AtkinsonHyperlegible-OFL.txt new file mode 100644 index 0000000..2befc89 --- /dev/null +++ b/licenses/fonts/AtkinsonHyperlegible-OFL.txt @@ -0,0 +1,92 @@ +Copyright 2020 Braille Institute of America, Inc. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/licenses/fonts/JetBrainsMono-OFL.txt b/licenses/fonts/JetBrainsMono-OFL.txt new file mode 100644 index 0000000..5ceee00 --- /dev/null +++ b/licenses/fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/licenses/fonts/Lora-OFL.txt b/licenses/fonts/Lora-OFL.txt new file mode 100644 index 0000000..d0ce7fe --- /dev/null +++ b/licenses/fonts/Lora-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/licenses/fonts/README.md b/licenses/fonts/README.md new file mode 100644 index 0000000..d413db2 --- /dev/null +++ b/licenses/fonts/README.md @@ -0,0 +1,17 @@ +# Bundled fonts + +Calendula bundles the following fonts as optional user-selectable typefaces +(Settings → Appearance → Fonts). Each is licensed under the SIL Open Font +License 1.1; the full license text is included alongside this file. + +| Font | Files (`app/src/main/res/font/`) | Copyright | License | +|------|----------------------------------|-----------|---------| +| Atkinson Hyperlegible | `atkinson_hyperlegible_regular.ttf`, `atkinson_hyperlegible_bold.ttf` | © 2020 Braille Institute of America, Inc. | [OFL](AtkinsonHyperlegible-OFL.txt) | +| Lora (variable) | `lora.ttf` | © 2011 Cyreal | [OFL](Lora-OFL.txt) | +| JetBrains Mono | `jetbrains_mono_regular.ttf`, `jetbrains_mono_bold.ttf` | © 2020 The JetBrains Mono Project Authors | [OFL](JetBrainsMono-OFL.txt) | + +The device's default typeface (Roboto on most Android builds) remains the +default and is not bundled. + +Fonts a user loads from their own device via "Choose file…" are copied into the +app's private storage and are not redistributed. -- 2.49.1 From efca0c86c4e294a17cc25da388cc45eed799cebb Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 10:27:03 +0200 Subject: [PATCH 28/49] feat(theme): filter custom-font picker to font MIME types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restrict the "Choose file…" document picker to font MIME types (font/ttf, font/otf, font/sfnt, font/collection plus legacy application/* font aliases) instead of */*, so only fonts are selectable. CustomFontStore's Font.Builder validation still guards anything a provider mislabels. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/settings/SettingsScreen.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 9ee4c72..0d73236 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -1402,6 +1402,23 @@ private fun fontLabel(token: String): String = when (token) { ?: stringResource(R.string.settings_font_system) } +/** + * MIME types offered to the document picker so it lists only font files. Covers + * the modern `font/` types plus the legacy `application/` font aliases some + * providers still report. Anything that slips through is still validated by + * [de.jeanlucmakiola.calendula.data.fonts.CustomFontStore] before use. + */ +private val FONT_PICKER_MIME_TYPES = arrayOf( + "font/ttf", + "font/otf", + "font/sfnt", + "font/collection", + "application/x-font-ttf", + "application/x-font-otf", + "application/font-sfnt", + "application/vnd.ms-opentype", +) + /** * Full-screen font chooser for one [FontRole]: the system default, each bundled * font (previewed in its own face), and "Choose file…" which opens the system @@ -1466,7 +1483,7 @@ private fun FontPicker( leadingIcon = if (isCustom) null else Icons.Default.UploadFile, selected = isCustom, position = positionOf(rowCount - 1, rowCount), - onClick = { launcher.launch(arrayOf("*/*")) }, + onClick = { launcher.launch(FONT_PICKER_MIME_TYPES) }, ) } } -- 2.49.1 From e6dd627c85530c4811e8a9cc29171553693bb1da Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 22:36:07 +0200 Subject: [PATCH 29/49] fix(i18n): drop code-pushed German font strings German translations must flow through Weblate; strings committed from code conflict with Weblate's repo state on its next merge. The nine font-picker strings from the custom-fonts feature fall back to English until translated in Weblate (MissingTranslation is informational). The stale-key removal (reminder_custom_with_value) stays: ExtraTranslation is a fatal lint error, so deletions of retired source strings must accompany the code change. Co-Authored-By: Claude Fable 5 --- app/src/main/res/values-de/strings.xml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6d5443b..aa65ccd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -252,15 +252,6 @@ Dunkel Dynamische Farben Erfordert Android 12 oder neuer - Schriftart für Überschriften - Schriftart für Text - Systemstandard - Atkinson Hyperlegible - Lora - JetBrains Mono - Datei auswählen… - Eigene Schriftart - Diese Datei konnte nicht als Schriftart gelesen werden Wochenstart Automatisch Zeitformat -- 2.49.1 From 8e3a17fdc35cd971b766de18bfe72e8e20a2beb5 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 22:40:26 +0200 Subject: [PATCH 30/49] refactor(calendar): share the insert values builder + reminder seeding insertManagedEvent duplicated ~45 lines of insertEvent (ContentValues build and the reminder-seeding loop). Both now go through buildEventInsertValues in EventWriteMapper and a shared seedReminders, differing only in the UID they stamp and the colour/attendee columns insertEvent layers on top. Co-Authored-By: Claude Fable 5 --- .../data/calendar/CalendarDataSource.kt | 112 ++++++------------ .../data/calendar/EventWriteMapper.kt | 36 ++++++ 2 files changed, 69 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt index 2f56ffa..3a8e854 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt @@ -416,47 +416,15 @@ class AndroidCalendarDataSource @Inject constructor( } ?: emptyList() override fun insertManagedEvent(form: EventForm, uid: String, allDayReminderTimeMinutes: Int): Long { - val times = form.toWriteTimes(ZoneId.systemDefault()) - val values = ContentValues().apply { - put( - CalendarContract.Events.CALENDAR_ID, - requireNotNull(form.calendarId) { "EventForm.calendarId is required" }, - ) - // Deterministic UID = the reconciliation key. A re-sync reads it back - // (queryManagedEvents) and diffs on it, so a contact never doubles. - put(CalendarContract.Events.UID_2445, uid) - put(CalendarContract.Events.TITLE, form.title.trim()) - put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0) - put(CalendarContract.Events.DTSTART, times.dtStartMillis) - if (form.rrule == null) { - put(CalendarContract.Events.DTEND, times.dtEndMillis) - } else { - put(CalendarContract.Events.RRULE, form.rrule) - put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay)) - } - put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone) - put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue()) - put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue()) - form.location.trim().takeIf { it.isNotEmpty() } - ?.let { put(CalendarContract.Events.EVENT_LOCATION, it) } - form.description.trim().takeIf { it.isNotEmpty() } - ?.let { put(CalendarContract.Events.DESCRIPTION, it) } - } - val eventId = resolver.insert(CalendarContract.Events.CONTENT_URI, values) + // Deterministic UID = the reconciliation key. A re-sync reads it back + // (queryManagedEvents) and diffs on it, so a contact never doubles. + val values = buildEventInsertValues(form, uid, form.toWriteTimes(ZoneId.systemDefault())) + val eventId = resolver.insert(CalendarContract.Events.CONTENT_URI, values.toContentValues()) ?.let(ContentUris::parseId) ?: throw WriteFailedException("insert managed event into calendar id=${form.calendarId}") // Seeded once, then never re-touched by sync — so a user who moves a // birthday reminder keeps their change. - encodedReminders(form, allDayReminderTimeMinutes).forEach { minutes -> - val reminder = ContentValues().apply { - put(CalendarContract.Reminders.EVENT_ID, eventId) - put(CalendarContract.Reminders.MINUTES, minutes) - put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) - } - if (resolver.insert(CalendarContract.Reminders.CONTENT_URI, reminder) == null) { - Log.w(TAG, "Failed to attach reminder ($minutes min) to managed event $eventId") - } - } + seedReminders(eventId, form, allDayReminderTimeMinutes) return eventId } @@ -779,35 +747,15 @@ class AndroidCalendarDataSource @Inject constructor( .distinct() override fun insertEvent(form: EventForm, allDayReminderTimeMinutes: Int): Long { - val times = form.toWriteTimes(ZoneId.systemDefault()) - val values = ContentValues().apply { - put( - CalendarContract.Events.CALENDAR_ID, - requireNotNull(form.calendarId) { "EventForm.calendarId is required" }, - ) - // A globally-unique UID so a later .ics backup/restore can identify - // the event and not duplicate it on re-import (the provider leaves - // this null for events it didn't sync). Older rows without one fall - // back to a stable synthesised UID at export time (deriveIcsUid). - put(CalendarContract.Events.UID_2445, "${UUID.randomUUID()}@calendula") - put(CalendarContract.Events.TITLE, form.title.trim()) - put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0) - put(CalendarContract.Events.DTSTART, times.dtStartMillis) - // The provider's invariant: recurring rows carry RRULE+DURATION - // (and no DTEND), one-off rows carry DTEND. - if (form.rrule == null) { - put(CalendarContract.Events.DTEND, times.dtEndMillis) - } else { - put(CalendarContract.Events.RRULE, form.rrule) - put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay)) - } - put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone) - put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue()) - put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue()) - form.location.trim().takeIf { it.isNotEmpty() } - ?.let { put(CalendarContract.Events.EVENT_LOCATION, it) } - form.description.trim().takeIf { it.isNotEmpty() } - ?.let { put(CalendarContract.Events.DESCRIPTION, it) } + // A globally-unique UID so a later .ics backup/restore can identify + // the event and not duplicate it on re-import (the provider leaves + // this null for events it didn't sync). Older rows without one fall + // back to a stable synthesised UID at export time (deriveIcsUid). + val values = buildEventInsertValues( + form, + uid = "${UUID.randomUUID()}@calendula", + times = form.toWriteTimes(ZoneId.systemDefault()), + ).toContentValues().apply { // A null colour just leaves both columns unset (the event inherits // its calendar's colour), so only the key/raw cases are written. when { @@ -819,19 +767,7 @@ class AndroidCalendarDataSource @Inject constructor( val uri = resolver.insert(CalendarContract.Events.CONTENT_URI, values) ?: throw WriteFailedException("insert event into calendar id=${form.calendarId}") val eventId = ContentUris.parseId(uri) - // Best effort (spec §8): the event exists at this point — a reminder - // that fails to attach is logged, not surfaced as a failed create. - encodedReminders(form, allDayReminderTimeMinutes) - .forEach { minutes -> - val reminder = ContentValues().apply { - put(CalendarContract.Reminders.EVENT_ID, eventId) - put(CalendarContract.Reminders.MINUTES, minutes) - put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) - } - if (resolver.insert(CalendarContract.Reminders.CONTENT_URI, reminder) == null) { - Log.w(TAG, "Failed to attach reminder ($minutes min) to event $eventId") - } - } + seedReminders(eventId, form, allDayReminderTimeMinutes) // Guests are best-effort like reminders: a row that fails to attach is // logged, not surfaced as a failed create. Calendula never sends an // invitation — it only writes the rows; the backend decides delivery. @@ -839,6 +775,24 @@ class AndroidCalendarDataSource @Inject constructor( return eventId } + /** + * Attach [form]'s reminders to a freshly inserted event. Best effort + * (spec §8): the event exists at this point — a reminder that fails to + * attach is logged, not surfaced as a failed create. + */ + private fun seedReminders(eventId: Long, form: EventForm, allDayReminderTimeMinutes: Int) { + encodedReminders(form, allDayReminderTimeMinutes).forEach { minutes -> + val reminder = ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.MINUTES, minutes) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + } + if (resolver.insert(CalendarContract.Reminders.CONTENT_URI, reminder) == null) { + Log.w(TAG, "Failed to attach reminder ($minutes min) to event $eventId") + } + } + } + override fun updateEvent( eventId: Long, original: EventForm, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt index 2c3928c..dc830aa 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/EventWriteMapper.kt @@ -49,6 +49,42 @@ internal fun EventWriteTimes.toRfc2445Duration(isAllDay: Boolean): String = if ( "P${(dtEndMillis - dtStartMillis) / 1_000L}S" } +/** + * Column values for a brand-new Events row: identity, times (the provider's + * invariant — recurring rows carry RRULE+DURATION and no DTEND, one-off rows + * carry DTEND), availability/access level and trimmed optional text. Shared by + * the user-event and managed-event insert paths, which differ only in the + * [uid] they stamp; colour and attendees are user-event concerns the caller + * layers on top. + */ +internal fun buildEventInsertValues( + form: EventForm, + uid: String, + times: EventWriteTimes, +): Map = buildMap { + put( + CalendarContract.Events.CALENDAR_ID, + requireNotNull(form.calendarId) { "EventForm.calendarId is required" }, + ) + put(CalendarContract.Events.UID_2445, uid) + put(CalendarContract.Events.TITLE, form.title.trim()) + put(CalendarContract.Events.ALL_DAY, if (form.isAllDay) 1 else 0) + put(CalendarContract.Events.DTSTART, times.dtStartMillis) + if (form.rrule == null) { + put(CalendarContract.Events.DTEND, times.dtEndMillis) + } else { + put(CalendarContract.Events.RRULE, form.rrule) + put(CalendarContract.Events.DURATION, times.toRfc2445Duration(form.isAllDay)) + } + put(CalendarContract.Events.EVENT_TIMEZONE, times.timezone) + put(CalendarContract.Events.AVAILABILITY, form.availability.toProviderValue()) + put(CalendarContract.Events.ACCESS_LEVEL, form.accessLevel.toProviderValue()) + form.location.trim().takeIf { it.isNotEmpty() } + ?.let { put(CalendarContract.Events.EVENT_LOCATION, it) } + form.description.trim().takeIf { it.isNotEmpty() } + ?.let { put(CalendarContract.Events.DESCRIPTION, it) } +} + /** * Dirty-checked column values for updating an existing Events row: only what * the user actually changed is written, so untouched fields can't stomp -- 2.49.1 From 442697d57c206ce0fa2b5ef52798bdd3d2e78ab9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 22:40:26 +0200 Subject: [PATCH 31/49] refactor(calendar): move the colour palette to data with named entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AndroidSpecialDatesCalendarSpec hardcoded three ARGB literals that its KDoc claimed were 'picked from the shared palette' — a silent-drift hazard, and referencing ui.common from data would invert layering. The palette now lives in data/calendar as CalendarColorPalette with named hues; the swatch pickers use CalendarColorPalette.all and the spec references Purple/Red/Blue. Co-Authored-By: Claude Fable 5 --- .../data/calendar/CalendarColorPalette.kt | 22 +++++++++++++++++++ .../AndroidSpecialDatesCalendarSpec.kt | 7 +++--- .../calendula/ui/calendars/CalendarsScreen.kt | 6 ++--- .../calendula/ui/common/ColorSwatchRow.kt | 13 ----------- .../calendula/ui/edit/EventEditScreen.kt | 4 ++-- 5 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarColorPalette.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarColorPalette.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarColorPalette.kt new file mode 100644 index 0000000..58b9af1 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarColorPalette.kt @@ -0,0 +1,22 @@ +package de.jeanlucmakiola.calendula.data.calendar + +/** + * Google-Calendar-style palette; ARGB ints for a raw `CALENDAR_COLOR` / + * `EVENT_COLOR`. The named entries exist for callers that need one specific + * hue (the managed special-dates calendars), so they can't drift from the + * swatches offered in the colour picker. + */ +object CalendarColorPalette { + val Red = 0xFFD50000.toInt() + val Orange = 0xFFE67C00.toInt() + val Amber = 0xFFF6BF26.toInt() + val Green = 0xFF33B679.toInt() + val DarkGreen = 0xFF0B8043.toInt() + val Blue = 0xFF039BE5.toInt() + val Indigo = 0xFF3F51B5.toInt() + val Purple = 0xFF8E24AA.toInt() + val Graphite = 0xFF616161.toInt() + + /** The full palette, in swatch-row order. */ + val all: List = listOf(Red, Orange, Amber, Green, DarkGreen, Blue, Indigo, Purple, Graphite) +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/AndroidSpecialDatesCalendarSpec.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/AndroidSpecialDatesCalendarSpec.kt index cacfd54..0aef3e2 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/AndroidSpecialDatesCalendarSpec.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/AndroidSpecialDatesCalendarSpec.kt @@ -3,6 +3,7 @@ package de.jeanlucmakiola.calendula.data.contacts import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import javax.inject.Inject import javax.inject.Singleton @@ -34,8 +35,8 @@ class AndroidSpecialDatesCalendarSpec @Inject constructor( ) override fun color(type: SpecialDateType): Int = when (type) { - SpecialDateType.Birthday -> 0xFF8E24AA.toInt() // purple - SpecialDateType.Anniversary -> 0xFFD50000.toInt() // red - SpecialDateType.Custom -> 0xFF039BE5.toInt() // blue + SpecialDateType.Birthday -> CalendarColorPalette.Purple + SpecialDateType.Anniversary -> CalendarColorPalette.Red + SpecialDateType.Custom -> CalendarColorPalette.Blue } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt index 4900a71..9d087c6 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt @@ -94,7 +94,7 @@ import de.jeanlucmakiola.calendula.R import de.jeanlucmakiola.calendula.data.prefs.BackupStatus import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.domain.CalendarSource -import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE +import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip import de.jeanlucmakiola.calendula.ui.common.DialogAmountField import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown @@ -142,7 +142,7 @@ fun CalendarsScreen( sessionKey = editorSession, isNew = editorId == NEW_CALENDAR_ID, initialName = editing?.displayName.orEmpty(), - initialColor = editing?.color ?: CALENDAR_COLOR_PALETTE.first(), + initialColor = editing?.color ?: CalendarColorPalette.all.first(), initialDescription = editing?.description.orEmpty(), onSave = { name, color, description -> val id = editorId @@ -507,7 +507,7 @@ private fun CalendarEditor( ) Spacer(Modifier.height(12.dp)) ColorSwatchRow( - colors = CALENDAR_COLOR_PALETTE, + colors = CalendarColorPalette.all, selected = color, onSelect = { color = it }, dark = dark, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ColorSwatchRow.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ColorSwatchRow.kt index 6fd11ec..09367db 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ColorSwatchRow.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ColorSwatchRow.kt @@ -67,16 +67,3 @@ fun ColorSwatchRow( } } } - -/** Google-Calendar-style palette; ARGB ints for a raw `CALENDAR_COLOR` / `EVENT_COLOR`. */ -val CALENDAR_COLOR_PALETTE: List = listOf( - 0xFFD50000, // red - 0xFFE67C00, // orange - 0xFFF6BF26, // amber - 0xFF33B679, // green - 0xFF0B8043, // dark green - 0xFF039BE5, // blue - 0xFF3F51B5, // indigo - 0xFF8E24AA, // purple - 0xFF616161, // graphite -).map { it.toInt() } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 2fd6830..a1906f3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -119,7 +119,7 @@ import de.jeanlucmakiola.calendula.domain.toRRule import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter import de.jeanlucmakiola.calendula.ui.common.predictiveBack -import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE +import de.jeanlucmakiola.calendula.data.calendar.CalendarColorPalette import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow import de.jeanlucmakiola.calendula.ui.common.DialogAmountField @@ -1745,7 +1745,7 @@ private fun ColorPickerDialog( ) } else { ColorSwatchRow( - colors = CALENDAR_COLOR_PALETTE, + colors = CalendarColorPalette.all, selected = selected, onSelect = onPickRaw, dark = dark, -- 2.49.1 From b8a420bdab2723fa15c28f64db4ab5ac6592f6b0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 22:40:44 +0200 Subject: [PATCH 32/49] refactor(contacts): one Context.hasContactsPermission() helper The READ_CONTACTS check was written out three times (data source, RootScreen resume trigger, settings screen). A single extension in the contacts package mirrors the existing Context.hasCalendarPermission() precedent; the settings call site follows in the settings cleanup commit. Co-Authored-By: Claude Fable 5 --- .../data/contacts/ContactSpecialDatesDataSource.kt | 9 ++++++--- .../java/de/jeanlucmakiola/calendula/ui/RootScreen.kt | 6 ++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt index c7588d5..13449ed 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt @@ -28,6 +28,11 @@ interface ContactSpecialDatesDataSource { fun readSpecialDates(): List } +/** Whether `READ_CONTACTS` is granted — the gate for every contacts read. */ +fun Context.hasContactsPermission(): Boolean = + ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == + PackageManager.PERMISSION_GRANTED + /** Map a `ContactsContract` event `TYPE` to our calendar bucket. */ internal fun specialDateTypeForRawEventType(type: Int): SpecialDateType = when (type) { Event.TYPE_BIRTHDAY -> SpecialDateType.Birthday @@ -40,9 +45,7 @@ class AndroidContactSpecialDatesDataSource @Inject constructor( @ApplicationContext private val context: Context, ) : ContactSpecialDatesDataSource { - override fun hasPermission(): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == - PackageManager.PERMISSION_GRANTED + override fun hasPermission(): Boolean = context.hasContactsPermission() override fun readSpecialDates(): List { if (!hasPermission()) return emptyList() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt index d3da90d..3e7a2f3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/RootScreen.kt @@ -20,6 +20,7 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler +import de.jeanlucmakiola.calendula.data.contacts.hasContactsPermission import de.jeanlucmakiola.calendula.ui.permission.PermissionScreen import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingScreen import de.jeanlucmakiola.calendula.ui.permission.ReminderOnboardingViewModel @@ -53,10 +54,7 @@ fun RootScreen( // Refresh the contact special-dates mirror on foreground (the // worker is debounced and no-ops when the feature is off). Gated // on the permission so users without the opt-in never enqueue it. - if (ContextCompat.checkSelfPermission( - context, Manifest.permission.READ_CONTACTS - ) == PackageManager.PERMISSION_GRANTED - ) { + if (context.hasContactsPermission()) { SpecialDatesScheduler.runNow(context, foreground = true) } } -- 2.49.1 From 67a07a93462b9c7a76073cf20b846f85f4532838 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 22:40:44 +0200 Subject: [PATCH 33/49] refactor(settings): dedupe override decoding + on-convention template input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - choiceFor moves next to CalendarReminderOverride in SettingsPrefs and replaces the hand-rolled null/empty/else decoder in SettingsViewModel, so the picker and the special-dates rows can't drift apart. - The special-dates template dialog used the codebase's only OutlinedTextField; it now follows the documented dialog convention (InlineTextField over a tonal surface, see DialogControls). - reminderChoiceLabel drops the manual loop (and its wrong comment — map is inline, so composable calls are fine in its lambda). - The blank-template fallback is shared with the sync engine via resolveTitleTemplate instead of being copy-pasted. Co-Authored-By: Claude Fable 5 --- .../calendula/data/prefs/SettingsPrefs.kt | 12 ++++++ .../calendula/ui/settings/SettingsScreen.kt | 43 +++++++++---------- .../ui/settings/SettingsViewModel.kt | 13 +++--- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index 2f7c87c..d2d2def 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -611,6 +611,18 @@ sealed interface CalendarReminderOverride { data class Minutes(val minutes: List) : CalendarReminderOverride } +/** + * The stored override for [calendarId] in an override map, as a + * [CalendarReminderOverride] (absent → [CalendarReminderOverride.Inherit], + * empty → [CalendarReminderOverride.None]) — the inverse of how + * [SettingsPrefs.setCalendarReminderOverride] stores a choice. + */ +fun Map>.choiceFor(calendarId: Long): CalendarReminderOverride = when { + !containsKey(calendarId) -> CalendarReminderOverride.Inherit + getValue(calendarId).isEmpty() -> CalendarReminderOverride.None + else -> CalendarReminderOverride.Minutes(getValue(calendarId)) +} + /** * The lead times to prefill on a new event: the matching per-calendar override * if [calendarId] has one for this event kind, otherwise the global default for diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index b0b7fec..2a7d3f8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -86,8 +86,10 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.data.contacts.hasContactsPermission import de.jeanlucmakiola.calendula.data.crash.CrashReporter import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride +import de.jeanlucmakiola.calendula.data.prefs.choiceFor import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref @@ -104,6 +106,7 @@ import de.jeanlucmakiola.calendula.ui.common.AgendaRangePicker import de.jeanlucmakiola.calendula.ui.common.agendaRangeLabel import de.jeanlucmakiola.calendula.ui.common.CollapsingScaffold import de.jeanlucmakiola.calendula.ui.common.GroupedRow +import de.jeanlucmakiola.calendula.ui.common.InlineTextField import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import de.jeanlucmakiola.calendula.ui.common.labelRes import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip @@ -1090,7 +1093,7 @@ private fun SpecialDatesScreen( } } val requestOrEnable: () -> Unit = { - if (hasContactsPermission(context)) { + if (context.hasContactsPermission()) { viewModel.setSpecialDatesEnabled(true) } else { permissionLauncher.launch(Manifest.permission.READ_CONTACTS) @@ -1290,12 +1293,21 @@ private fun SpecialDatesTemplateDialog( title = { Text(stringResource(R.string.settings_special_dates_template)) }, text = { Column { - androidx.compose.material3.OutlinedTextField( - value = text, - onValueChange = { text = it }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), - ) + // The app's borderless input over a tonal surface (the dialog + // convention — see DialogControls), not Material's outlined field. + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHighest, + shape = RoundedCornerShape(12.dp), + ) { + InlineTextField( + value = text, + onValueChange = { text = it }, + placeholder = stringResource(R.string.settings_special_dates_template), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 12.dp), + ) + } Spacer(Modifier.height(8.dp)) Text( text = stringResource(R.string.settings_special_dates_template_hint), @@ -1324,10 +1336,6 @@ private fun specialDateTypeLabel(type: SpecialDateType): Int = when (type) { SpecialDateType.Custom -> R.string.settings_special_dates_type_custom } -private fun hasContactsPermission(context: Context): Boolean = - ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == - PackageManager.PERMISSION_GRANTED - @Composable private fun specialDatesLastRunLabel(context: Context, lastRun: Long): String = if (lastRun <= 0L) { @@ -1426,22 +1434,11 @@ private fun formatTimeOfDay(context: Context, minutesOfDay: Int): String { return DateFormat.getTimeFormat(context).format(time) } -/** The stored override for [calendarId], as a picker choice (absent → inherit). */ -private fun Map>.choiceFor(calendarId: Long): CalendarReminderOverride = when { - !containsKey(calendarId) -> CalendarReminderOverride.Inherit - this.getValue(calendarId).isEmpty() -> CalendarReminderOverride.None - else -> CalendarReminderOverride.Minutes(this.getValue(calendarId)) -} - /** Label for a global-default choice: empty → "None", else the lead times joined. */ @Composable private fun reminderChoiceLabel(minutes: List): String { if (minutes.isEmpty()) return stringResource(R.string.reminder_none) - // reminderLeadTimeLabel is @Composable, so resolve each part in a loop rather - // than a (non-composable) joinToString transform. - val parts = ArrayList(minutes.size) - for (m in minutes) parts.add(reminderLeadTimeLabel(m)) - return parts.joinToString(", ") + return minutes.map { reminderLeadTimeLabel(it) }.joinToString(", ") } /** Row summary for a calendar: its override, or the inherited global default. */ diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index 8b75a1a..a29f814 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -13,8 +13,10 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesCalendarSpec import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesScheduler import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesSyncEngine +import de.jeanlucmakiola.calendula.data.contacts.resolveTitleTemplate import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride +import de.jeanlucmakiola.calendula.data.prefs.choiceFor import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason @@ -195,8 +197,7 @@ class SettingsViewModel @Inject constructor( // A blank stored template resolves to the localized default so the // editor always shows something sensible. titleTemplates = SpecialDateType.entries.associateWith { type -> - templates[type]?.takeIf { it.isNotBlank() } - ?: specialDatesSpec.defaultTitleTemplate(type) + specialDatesSpec.resolveTitleTemplate(type, templates) }, showYear = showYear, stalledPermission = status.stalled == SpecialDatesStalledReason.PermissionRevoked, @@ -207,13 +208,9 @@ class SettingsViewModel @Inject constructor( prefs.perCalendarAllDayReminderOverride, ) { base, calendars, allDayOverrides -> base.copy( + // An override is always seeded on creation; present-empty = None. reminderChoices = calendars.mapValues { (_, calendarId) -> - // An override is always seeded on creation; present-empty = None. - when (val minutes = allDayOverrides[calendarId]) { - null -> CalendarReminderOverride.Inherit - emptyList() -> CalendarReminderOverride.None - else -> CalendarReminderOverride.Minutes(minutes) - } + allDayOverrides.choiceFor(calendarId) }, ) }.stateIn( -- 2.49.1 From 86d4e115848f0d1367063c114c68f5e651603dfe Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 22:40:44 +0200 Subject: [PATCH 34/49] refactor(contacts): simplify the sync engine's gates and context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The worker no longer pre-checks specialDatesEnabled: the engine already gates on it, and the worker now consumes the previously-unreachable Disabled result instead (without recording a run). - ReminderContext drops the timed defaults it could never use — managed events are always all-day — saving two DataStore reads per sync. - createCalendar no longer persists the calendar id itself; reconcileCalendars is the single place ids are recorded (it always followed up with the same write). - The title-template fallback uses the shared resolveTitleTemplate. Co-Authored-By: Claude Fable 5 --- .../data/contacts/SpecialDatesScheduler.kt | 21 ++++++------ .../data/contacts/SpecialDatesSyncEngine.kt | 32 +++++++++---------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt index 05a1bac..a906f9b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt @@ -83,9 +83,6 @@ class SpecialDatesSyncWorker( override suspend fun doWork(): Result { val deps = EntryPointAccessors.fromApplication(applicationContext, Deps::class.java) val prefs = deps.settingsPrefs() - // Respect the toggle even for already-queued work; never revive a - // disabled feature. - if (!prefs.specialDatesEnabled.first()) return Result.success() val now = System.currentTimeMillis() // A foreground resume can fire often — skip if we synced recently. @@ -95,13 +92,19 @@ class SpecialDatesSyncWorker( } return try { - val result = deps.syncEngine().sync() - val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) { - SpecialDatesStalledReason.PermissionRevoked - } else { - null + when (val result = deps.syncEngine().sync()) { + // The engine re-checks the toggle, so already-queued work never + // revives a disabled feature; don't record a run either. + SpecialDatesSyncResult.Disabled -> return Result.success() + else -> { + val stalled = if (result == SpecialDatesSyncResult.PermissionMissing) { + SpecialDatesStalledReason.PermissionRevoked + } else { + null + } + prefs.recordSpecialDatesRun(now, stalled) + } } - prefs.recordSpecialDatesRun(now, stalled) if (foreground) prefs.setSpecialDatesLastForegroundSync(now) Result.success() } catch (e: Exception) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt index fa3369f..082d3bc 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt @@ -6,7 +6,6 @@ import de.jeanlucmakiola.calendula.data.calendar.ManagedEventRow import de.jeanlucmakiola.calendula.data.calendar.toWriteTimes import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs -import de.jeanlucmakiola.calendula.data.prefs.resolveDefaultReminder import de.jeanlucmakiola.calendula.domain.Availability import de.jeanlucmakiola.calendula.domain.EventForm import de.jeanlucmakiola.calendula.domain.RecurrenceFreq @@ -39,6 +38,16 @@ interface SpecialDatesCalendarSpec { fun defaultTitleTemplate(type: SpecialDateType): String } +/** + * The stored title template for [type], falling back to the localized default + * when blank/absent — the one resolution rule shared by the sync (what gets + * written) and the settings editor (what gets shown). + */ +fun SpecialDatesCalendarSpec.resolveTitleTemplate( + type: SpecialDateType, + stored: Map, +): String = stored[type]?.takeIf { it.isNotBlank() } ?: defaultTitleTemplate(type) + /** * Mirrors contact birthdays/anniversaries/custom dates into local calendars, * one per type. Every reconcile is an idempotent diff keyed on the deterministic @@ -72,8 +81,7 @@ class SpecialDatesSyncEngine @Inject constructor( val showYear = prefs.specialDatesShowYear.first() calendarByType.forEach { (type, calendarId) -> - val template = templates[type]?.takeIf { it.isNotBlank() } - ?: spec.defaultTitleTemplate(type) + val template = spec.resolveTitleTemplate(type, templates) syncType( calendarId = calendarId, type = type, @@ -143,8 +151,8 @@ class SpecialDatesSyncEngine @Inject constructor( } private suspend fun createCalendar(type: SpecialDateType): Long { + // reconcileCalendars persists the id — the single place ids are recorded. val id = calendars.createManagedCalendar(spec.displayName(type), spec.color(type), type) - prefs.setSpecialDatesCalendarId(type, id) // Seed a useful all-day reminder default (on the day + a week before), so // birthdays get lead time out of the box. Per-calendar, user-adjustable — // only set when the user hasn't already configured this calendar. @@ -212,29 +220,21 @@ class SpecialDatesSyncEngine @Inject constructor( ) } + // Managed events are always all-day, so only the all-day defaults apply. private suspend fun readReminderContext(): ReminderContext = ReminderContext( - timedGlobal = prefs.defaultReminderMinutes.first(), allDayGlobal = prefs.defaultAllDayReminderMinutes.first(), - timedOverrides = prefs.perCalendarReminderOverride.first(), allDayOverrides = prefs.perCalendarAllDayReminderOverride.first(), allDayTimeMinutes = prefs.allDayReminderTimeMinutes.first(), ) private data class ReminderContext( - val timedGlobal: List, val allDayGlobal: List, - val timedOverrides: Map>, val allDayOverrides: Map>, val allDayTimeMinutes: Int, ) { - fun resolveAllDay(calendarId: Long): List = resolveDefaultReminder( - timedGlobal = timedGlobal, - allDayGlobal = allDayGlobal, - timedOverrides = timedOverrides, - allDayOverrides = allDayOverrides, - calendarId = calendarId, - isAllDay = true, - ) + /** Same semantics as `resolveDefaultReminder`: present-empty = explicit none. */ + fun resolveAllDay(calendarId: Long): List = + allDayOverrides[calendarId] ?: allDayGlobal } private data class BuiltManagedEvent(val desired: ManagedEventDesired, val form: EventForm) -- 2.49.1 From 037d05b171e052a5823b72b0595bb7afd89724c0 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:01:41 +0200 Subject: [PATCH 35/49] fix(reminders): close both gaps in disabled-calendar suppression (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two holes in the v2.13.0 'disabled calendars no longer notify' fix: - A snoozed reminder bypassed the filter: ReminderActionReceiver's ACTION_SHOW posted unconditionally. The gate now lives once in ReminderNotifier.post — the single choke point both receivers use — built on a shared ReminderAlert.isForDisabledCalendar predicate that never treats calendarId 0L (pre-upgrade snooze intents without EXTRA_CALENDAR_ID) as disabled. - A reminder firing while its calendar was disabled was lost forever: the receiver marks the full due set STATE_FIRED (deliberately, to stop provider re-broadcasts) and nothing re-scans. Suppressed alerts are now stashed in a DataStore-backed SuppressedReminderStore and re-posted when the calendar is re-enabled, while the event hasn't ended yet; expired entries are purged opportunistically. Co-Authored-By: Claude Opus 4.8 --- .../data/reminders/EventReminderReceiver.kt | 30 +++- .../data/reminders/ReminderNotifier.kt | 7 + .../data/reminders/SuppressedReminderStore.kt | 130 ++++++++++++++++++ .../ui/calendars/CalendarsViewModel.kt | 33 ++++- .../data/reminders/PostableAlertsTest.kt | 16 +++ .../reminders/SuppressedReminderStoreTest.kt | 129 +++++++++++++++++ 6 files changed, 337 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStore.kt create mode 100644 app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStoreTest.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt index dc6871f..22fc5cf 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/EventReminderReceiver.kt @@ -17,16 +17,28 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject +/** + * True when [this] alert belongs to a calendar the user disabled in-app, so its + * reminder must be suppressed (mirroring the event filtering in + * CalendarRepositoryImpl). Alerts whose calendar is unknown (id 0L — e.g. a + * pre-upgrade snooze PendingIntent minted before EXTRA_CALENDAR_ID existed) are + * never treated as disabled. This is the one predicate the disabled-calendar + * gate is built from: [postableAlerts] here and the choke point in + * [ReminderNotifier.post] both use it. + */ +internal fun ReminderAlert.isForDisabledCalendar(disabledCalendarIds: Set): Boolean = + calendarId != 0L && calendarId in disabledCalendarIds + /** * The due alerts that should actually surface as notifications: everything - * except alerts whose calendar the user has disabled in-app (mirroring the - * event filtering in CalendarRepositoryImpl). The caller still marks the full - * due set fired, so suppressed alerts are not re-broadcast by the provider. + * except alerts whose calendar the user has disabled in-app. The caller still + * marks the full due set fired, so suppressed alerts are not re-broadcast by the + * provider. */ internal fun postableAlerts( due: List, disabledCalendarIds: Set, -): List = due.filterNot { it.calendarId in disabledCalendarIds } +): List = due.filterNot { it.isForDisabledCalendar(disabledCalendarIds) } /** * Becomes the app that turns the calendar provider's reminder alarms into @@ -45,6 +57,7 @@ class EventReminderReceiver : BroadcastReceiver() { @Inject lateinit var notifier: ReminderNotifier @Inject lateinit var settingsPrefs: SettingsPrefs @Inject lateinit var calendarPrefs: CalendarPrefs + @Inject lateinit var suppressedStore: SuppressedReminderStore override fun onReceive(context: Context, intent: Intent) { if (intent.action != CalendarContract.ACTION_EVENT_REMINDER) return @@ -60,11 +73,16 @@ class EventReminderReceiver : BroadcastReceiver() { val now = System.currentTimeMillis() val due = alertStore.dueAlerts(now) val disabled = calendarPrefs.disabledCalendarIds.first() + val postable = postableAlerts(due, disabled) // Suppress reminders for disabled calendars, but still mark // every due alert fired so the provider stops re-broadcasting - // the suppressed ones. - postableAlerts(due, disabled).forEach { notifier.post(it) } + // the suppressed ones. Stash those suppressed alerts so + // re-enabling their calendar can recover them (they would + // otherwise stay STATE_FIRED forever with no re-scan). + postable.forEach { notifier.post(it) } alertStore.markFired(due.map { it.alertId }, now) + suppressedStore.stash(due - postable.toSet(), now) + suppressedStore.purgeExpired(now) } } finally { pendingResult.finish() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt index 7b92a9d..9bd0dde 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/ReminderNotifier.kt @@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.MainActivity import de.jeanlucmakiola.calendula.R +import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.is24Hour import kotlinx.coroutines.flow.first @@ -33,6 +34,7 @@ import javax.inject.Singleton class ReminderNotifier @Inject constructor( @ApplicationContext private val context: Context, private val settingsPrefs: SettingsPrefs, + private val calendarPrefs: CalendarPrefs, ) { /** False when the user declined `POST_NOTIFICATIONS` or muted the app. */ @@ -44,6 +46,11 @@ class ReminderNotifier @Inject constructor( } suspend fun post(alert: ReminderAlert) { + // The single choke point for the disabled-calendar gate: it covers both + // the provider broadcast (EventReminderReceiver) and a snoozed re-show + // (ReminderActionReceiver), so a calendar disabled after a snooze no + // longer notifies — without either receiver duplicating the check. + if (alert.isForDisabledCalendar(calendarPrefs.disabledCalendarIds.first())) return ensureChannel() val title = alert.title.ifBlank { context.getString(R.string.event_untitled) } val is24Hour = settingsPrefs.timeFormat.first() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStore.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStore.kt new file mode 100644 index 0000000..eb5b6e0 --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStore.kt @@ -0,0 +1,130 @@ +package de.jeanlucmakiola.calendula.data.reminders + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.MutablePreferences +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringSetPreferencesKey +import java.util.Base64 +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Still relevant while the event has not ended: a reminder for an event that is + * already over is pointless to re-surface. Falls back to the begin time when the + * end is unknown (0L). Used both to decide what to re-post and to purge the stash. + */ +internal fun ReminderAlert.isRelevantAt(nowMillis: Long): Boolean = + (endMillis.takeIf { it > 0L } ?: beginMillis) >= nowMillis + +/** + * Local stash of reminder alerts that fired while their calendar was disabled + * in-app. [EventReminderReceiver] marks every due alert `STATE_FIRED` regardless + * (so the provider stops re-broadcasting the suppressed ones), which would + * otherwise lose those reminders forever — there is no re-scan. Stashing lets + * [de.jeanlucmakiola.calendula.ui.calendars.CalendarsViewModel] re-post them if + * the user re-enables the calendar before the event is over. + * + * Persisted in the shared preferences DataStore as a set of self-describing + * strings (one per alert); the stash never reaches a screen, so there is no + * domain model. Entries whose event has already ended are dropped on the next + * stash/recover/purge, so the stash only ever holds a handful of pending alerts. + */ +@Singleton +class SuppressedReminderStore @Inject constructor( + private val store: DataStore, +) { + + /** Add [alerts] to the stash, replacing any existing entry with the same id. */ + suspend fun stash(alerts: List, nowMillis: Long) { + if (alerts.isEmpty()) return + store.edit { prefs -> + val byId = decodeAll(prefs).associateByTo(mutableMapOf()) { it.alertId } + alerts.forEach { byId[it.alertId] = it } + prefs.putStash(byId.values.filter { it.isRelevantAt(nowMillis) }) + } + } + + /** + * Remove and return the still-relevant stashed alerts belonging to any of + * [calendarIds]; drops expired entries for every calendar in passing. + */ + suspend fun recoverFor(calendarIds: Set, nowMillis: Long): List { + val recovered = mutableListOf() + store.edit { prefs -> + val kept = decodeAll(prefs).filter { alert -> + when { + !alert.isRelevantAt(nowMillis) -> false // expired: drop + alert.calendarId in calendarIds -> { recovered += alert; false } + else -> true + } + } + prefs.putStash(kept) + } + return recovered + } + + /** Drop entries whose event has already ended — cheap opportunistic cleanup. */ + suspend fun purgeExpired(nowMillis: Long) { + store.edit { prefs -> + prefs.putStash(decodeAll(prefs).filter { it.isRelevantAt(nowMillis) }) + } + } + + private fun decodeAll(prefs: Preferences): List = + prefs[KEY].orEmpty().mapNotNull { decodeStashEntry(it) } + + private fun MutablePreferences.putStash(alerts: List) { + val encoded = alerts.map { encodeStashEntry(it) }.toSet() + if (encoded.isEmpty()) remove(KEY) else set(KEY, encoded) + } + + private companion object { + val KEY = stringSetPreferencesKey("suppressed_reminders") + } +} + +// One stash entry as a delimited string. The '|' separator is safe because every +// free-text field is Base64-encoded first (that alphabet never contains '|'), and +// a null location is stored as a distinct sentinel that Base64 also never yields. +private const val FIELD_SEP = "|" +private const val NULL_LOCATION = "-" + +internal fun encodeStashEntry(alert: ReminderAlert): String = listOf( + alert.alertId.toString(), + alert.eventId.toString(), + alert.calendarId.toString(), + alert.beginMillis.toString(), + alert.endMillis.toString(), + if (alert.isAllDay) "1" else "0", + alert.title.toBase64(), + alert.location?.toBase64() ?: NULL_LOCATION, +).joinToString(FIELD_SEP) + +/** Reverse of [encodeStashEntry]; returns null for a malformed entry (dropped). */ +internal fun decodeStashEntry(raw: String): ReminderAlert? { + val parts = raw.split(FIELD_SEP) + if (parts.size != 8) return null + return try { + ReminderAlert( + alertId = parts[0].toLong(), + eventId = parts[1].toLong(), + calendarId = parts[2].toLong(), + beginMillis = parts[3].toLong(), + endMillis = parts[4].toLong(), + title = parts[6].fromBase64(), + location = parts[7].takeIf { it != NULL_LOCATION }?.fromBase64(), + isAllDay = parts[5] == "1", + ) + } catch (e: NumberFormatException) { + null + } catch (e: IllegalArgumentException) { // bad Base64 + null + } +} + +private fun String.toBase64(): String = + Base64.getEncoder().encodeToString(toByteArray(Charsets.UTF_8)) + +private fun String.fromBase64(): String = + String(Base64.getDecoder().decode(this), Charsets.UTF_8) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt index d56ee9a..bfadaf6 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsViewModel.kt @@ -14,6 +14,8 @@ import de.jeanlucmakiola.calendula.data.ics.IcsExporter import de.jeanlucmakiola.calendula.data.prefs.BackupStatus import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs +import de.jeanlucmakiola.calendula.data.reminders.ReminderNotifier +import de.jeanlucmakiola.calendula.data.reminders.SuppressedReminderStore import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.ics.IcsWriter import kotlinx.coroutines.CoroutineDispatcher @@ -45,6 +47,8 @@ class CalendarsViewModel @Inject constructor( private val icsExporter: IcsExporter, private val prefs: CalendarPrefs, private val settingsPrefs: SettingsPrefs, + private val suppressedStore: SuppressedReminderStore, + private val notifier: ReminderNotifier, @IoDispatcher private val io: CoroutineDispatcher, ) : ViewModel() { @@ -144,7 +148,10 @@ class CalendarsViewModel @Inject constructor( viewModelScope.launch { val current = prefs.disabledCalendarIds.first() val next = if (disabled) current + id else current - id - if (next != current) prefs.setDisabledCalendarIds(next) + if (next != current) { + prefs.setDisabledCalendarIds(next) + if (!disabled) recoverReminders(setOf(id)) + } } } @@ -157,7 +164,29 @@ class CalendarsViewModel @Inject constructor( viewModelScope.launch { val current = prefs.disabledCalendarIds.first() val next = if (disabled) current + ids else current - ids.toSet() - if (next != current) prefs.setDisabledCalendarIds(next) + if (next != current) { + prefs.setDisabledCalendarIds(next) + if (!disabled) recoverReminders(current intersect ids.toSet()) + } + } + } + + /** + * Re-post the reminders that fired while [reEnabledIds] were disabled and are + * still relevant (event not yet over), then drop them from the stash. Runs + * after the disabled set is written, so the notifier's own disabled gate lets + * them through. Best-effort at re-enable time: it mirrors the receiver gates + * (reminders on + postable), and there is no later re-scan, so alerts left + * unposted because those gates are closed are simply released. + */ + private suspend fun recoverReminders(reEnabledIds: Set) { + if (reEnabledIds.isEmpty()) return + val recovered = suppressedStore.recoverFor(reEnabledIds, System.currentTimeMillis()) + if (recovered.isNotEmpty() && + settingsPrefs.remindersEnabled.first() && + notifier.canPost() + ) { + recovered.forEach { notifier.post(it) } } } diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt index 222281e..5fedc7d 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/PostableAlertsTest.kt @@ -52,4 +52,20 @@ class PostableAlertsTest { assertThat(postable).isEqualTo(due) } + + @Test + fun `alert with unknown calendar id 0 is never treated as disabled`() { + // Pre-upgrade snooze PendingIntents carry no calendar id (defaults to 0L). + val preUpgrade = alert(1, calendarId = 0L) + + assertThat(preUpgrade.isForDisabledCalendar(disabledCalendarIds = setOf(0L))).isFalse() + assertThat(postableAlerts(listOf(preUpgrade), disabledCalendarIds = setOf(0L))) + .containsExactly(preUpgrade) + } + + @Test + fun `isForDisabledCalendar matches only the disabled ids`() { + assertThat(alert(1, calendarId = 200).isForDisabledCalendar(setOf(200))).isTrue() + assertThat(alert(1, calendarId = 100).isForDisabledCalendar(setOf(200))).isFalse() + } } diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStoreTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStoreTest.kt new file mode 100644 index 0000000..74c5eda --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/reminders/SuppressedReminderStoreTest.kt @@ -0,0 +1,129 @@ +package de.jeanlucmakiola.calendula.data.reminders + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path + +class SuppressedReminderStoreTest { + + private fun newDataStore(tempDir: Path): DataStore = + PreferenceDataStoreFactory.create( + produceFile = { tempDir.resolve("test_prefs.preferences_pb").toFile() }, + ) + + private fun alert( + alertId: Long, + calendarId: Long, + endMillis: Long = Long.MAX_VALUE, + title: String = "Event $alertId", + location: String? = null, + ) = ReminderAlert( + alertId = alertId, + eventId = alertId * 10, + calendarId = calendarId, + beginMillis = 0L, + endMillis = endMillis, + title = title, + location = location, + isAllDay = false, + ) + + @Test + fun `encode then decode round-trips every field including delimiters`() { + val original = alert( + alertId = 7, + calendarId = 42, + endMillis = 123_456_789L, + // Free-text with the field separator and other awkward characters. + title = "Lunch | with | Alice", + location = "Café, 3rd floor | room B", + ).copy(beginMillis = 100L, isAllDay = true) + + val decoded = decodeStashEntry(encodeStashEntry(original)) + + assertThat(decoded).isEqualTo(original) + } + + @Test + fun `decode returns null for a malformed entry`() { + assertThat(decodeStashEntry("not-a-valid-entry")).isNull() + } + + @Test + fun `null location round-trips`() { + val original = alert(1, calendarId = 1, location = null) + assertThat(decodeStashEntry(encodeStashEntry(original))).isEqualTo(original) + } + + @Test + fun `recoverFor returns and removes only the re-enabled calendars`() = runTest { + val store = SuppressedReminderStore(newDataStore(tempDir)) + val keep = alert(1, calendarId = 100) + val recoverA = alert(2, calendarId = 200) + val recoverB = alert(3, calendarId = 200) + store.stash(listOf(keep, recoverA, recoverB), nowMillis = 0L) + + val recovered = store.recoverFor(setOf(200L), nowMillis = 0L) + + assertThat(recovered).containsExactly(recoverA, recoverB) + // The still-disabled calendar's alert stays stashed; the recovered ones are gone. + assertThat(store.recoverFor(setOf(100L, 200L), nowMillis = 0L)).containsExactly(keep) + } + + @Test + fun `stash drops alerts whose event already ended`() = runTest { + val store = SuppressedReminderStore(newDataStore(tempDir)) + val past = alert(1, calendarId = 100, endMillis = 500L) + val future = alert(2, calendarId = 100, endMillis = 2_000L) + + store.stash(listOf(past, future), nowMillis = 1_000L) + + assertThat(store.recoverFor(setOf(100L), nowMillis = 1_000L)).containsExactly(future) + } + + @Test + fun `purgeExpired removes only entries past their event end`() = runTest { + val store = SuppressedReminderStore(newDataStore(tempDir)) + // Stash both before "now" so neither is dropped on write, then advance time. + store.stash( + listOf( + alert(1, calendarId = 100, endMillis = 500L), + alert(2, calendarId = 100, endMillis = 2_000L), + ), + nowMillis = 0L, + ) + + store.purgeExpired(nowMillis = 1_000L) + + assertThat(store.recoverFor(setOf(100L), nowMillis = 0L).map { it.alertId }) + .containsExactly(2L) + } + + @Test + fun `stash replaces an existing entry with the same alert id`() = runTest { + val store = SuppressedReminderStore(newDataStore(tempDir)) + store.stash(listOf(alert(1, calendarId = 100, title = "old")), nowMillis = 0L) + store.stash(listOf(alert(1, calendarId = 100, title = "new")), nowMillis = 0L) + + val recovered = store.recoverFor(setOf(100L), nowMillis = 0L) + + assertThat(recovered).hasSize(1) + assertThat(recovered.single().title).isEqualTo("new") + } + + @Test + fun `isRelevantAt is true up to the event end and false after`() { + val a = alert(1, calendarId = 1, endMillis = 1_000L) + assertThat(a.isRelevantAt(999L)).isTrue() + assertThat(a.isRelevantAt(1_000L)).isTrue() + assertThat(a.isRelevantAt(1_001L)).isFalse() + } + + @TempDir + lateinit var tempDir: Path +} -- 2.49.1 From 5c513a1b19c1345e138b5d0fcf3a5ab33095f37c Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:01:41 +0200 Subject: [PATCH 36/49] fix(settings): make the reminder picker's empty state safe and explicit Three defects in the v2.13.0 multi-select reminder picker: - Unchecking the last time on a per-calendar picker silently persisted an explicit no-reminder override; an accidental toggle-undo wiped the calendar's default. Empty-by-unchecking now reverts to 'Use default' (Inherit); deliberate no-reminder is its own exclusive 'None' row (reusing reminder_none) on both pickers, so the empty state is visible and reachable instead of implicit. - A custom (non-preset) lead time's row vanished the moment it was unchecked, stranding the hand-entered value. Custom values seen this session keep their row (unchecked) until the picker closes. - The optimistic selection seeded once from a possibly-not-yet-loaded settings state (initialValue emptyList behind a CalendarProvider- gated combine), so a quick first toggle after process-death restore overwrote the stored default. The local state now re-syncs from the incoming selection until the user first interacts. Co-Authored-By: Claude Opus 4.8 --- .../calendula/ui/common/Picker.kt | 103 +++++++++++++----- .../common/ReminderOverrideForMinutesTest.kt | 35 ++++++ 2 files changed, 108 insertions(+), 30 deletions(-) create mode 100644 app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ReminderOverrideForMinutesTest.kt diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index 847b5a1..c3ef279 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import android.view.WindowManager import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -116,12 +117,16 @@ fun OptionPicker( * Reminder-default picker, full-screen and **multi-select**: each [presets] * lead time (plus any chosen custom value) is a checkbox row that toggles * independently, so a default can carry several reminders ("1 week before" *and* - * "on the day"). A per-calendar picker ([allowInherit]) gains an exclusive "Use - * default reminder" row above the list; choosing it defers to the global default - * and clears the checked times. With nothing checked and inherit off, the choice - * is [CalendarReminderOverride.None] (an explicit no-reminder). A "Custom" row - * expands an inline number field plus a unit selector to add an arbitrary lead - * time to the set. Changes apply live via [onSelect]; the user leaves via back. + * "on the day"). Two exclusive checkmark rows sit above the list: an explicit + * "No reminder" (both pickers, so the empty state stays visible and reachable) + * and, for a per-calendar picker ([allowInherit]), "Use default reminder", which + * defers to the global default. Clearing the last checked time reverts to "Use + * default" on a per-calendar picker (so an accidental toggle-undo can't silently + * wipe the calendar's default) and to explicit [CalendarReminderOverride.None] + * on the global default (where empty legitimately means no reminder). A "Custom" + * row expands an inline number field plus a unit selector to add an arbitrary + * lead time to the set. Changes apply live via [onSelect]; the user leaves via + * back. */ @Composable fun ReminderDefaultPicker( @@ -132,48 +137,54 @@ fun ReminderDefaultPicker( onSelect: (CalendarReminderOverride) -> Unit, onDismiss: () -> Unit, ) { - // Optimistic local state: the chosen override is authoritative while the - // picker is open, so quick successive toggles compose on each other instead - // of racing the round-trip through the settings flow (which would drop a - // toggle made before the previous write echoes back). Seeded once on open; - // the picker leaves composition on dismiss, so reopening re-reads [selected]. + // Optimistic local state: once the user edits, the chosen override is + // authoritative while the picker is open, so quick successive toggles compose + // on each other instead of racing the round-trip through the settings flow + // (which would drop a toggle made before the previous write echoes back). var current by remember { mutableStateOf(selected) } + // Whether the user has toggled anything yet. Until then, [current] keeps + // mirroring [selected]: a picker that first composed against the settings + // flow's initialValue (empty — reachable after process-death restore straight + // onto the Notifications page, before the CalendarProvider-gated combine + // resolves) then adopts the real stored default instead of persisting empty + // on the first tap. After the first edit, local state is authoritative. + var userEdited by remember { mutableStateOf(false) } + LaunchedEffect(selected) { + if (!userEdited) current = selected + } val inherits = current is CalendarReminderOverride.Inherit + val isNone = current is CalendarReminderOverride.None val selectedMinutes = (current as? CalendarReminderOverride.Minutes)?.minutes.orEmpty() - // Presets plus any selected custom (non-preset) values, each as its own row. - val rows = presets + selectedMinutes.filter { it !in presets }.sorted() + // Custom (non-preset) lead times seen this session, so unchecking one keeps + // its row (unchecked) until the picker closes instead of vanishing mid-tap, + // which would strand a hand-entered value with no way to re-check it. + val seenCustom = remember { mutableSetOf() } + seenCustom += selectedMinutes.filter { it !in presets } + // Presets plus every custom value seen this session, each as its own row. + val rows = presets + seenCustom.sorted() var customExpanded by rememberSaveable { mutableStateOf(false) } var amountText by rememberSaveable { mutableStateOf("") } var unit by rememberSaveable { mutableStateOf(ReminderUnit.Minutes) } fun apply(override: CalendarReminderOverride) { + userEdited = true current = override onSelect(override) } - // Emit the override for a new set of minutes: empty collapses to None, which - // (with inherit unchosen) reads as an explicit "no reminder". - fun emit(minutes: List) { - val norm = minutes.distinct().sorted() - apply( - if (norm.isEmpty()) { - CalendarReminderOverride.None - } else { - CalendarReminderOverride.Minutes(norm) - }, - ) - } + fun emit(minutes: List) = apply(reminderOverrideForMinutes(minutes, allowInherit)) fun toggle(minute: Int) = emit(if (minute in selectedMinutes) selectedMinutes - minute else selectedMinutes + minute) FullScreenPicker(title = title, onDismiss = onDismiss) { - // Exclusive "use default" choice (per-calendar only): its own group above - // the multi-select list, since inheriting is mutually exclusive with - // picking specific times. + // Exclusive choices in their own group above the multi-select list, since + // each is mutually exclusive with picking specific times: "use default" + // (per-calendar only) and an explicit "no reminder" (both pickers, so the + // empty state stays visible and deliberately reachable). if (allowInherit) { GroupedRow( title = stringResource(R.string.reminder_use_default), - position = Position.Alone, + position = Position.Top, selected = inherits, trailing = if (inherits) { { SelectedCheck() } @@ -182,8 +193,19 @@ fun ReminderDefaultPicker( }, onClick = { apply(CalendarReminderOverride.Inherit) }, ) - Spacer(Modifier.height(24.dp)) } + GroupedRow( + title = stringResource(R.string.reminder_none), + position = if (allowInherit) Position.Bottom else Position.Alone, + selected = isNone, + trailing = if (isNone) { + { SelectedCheck() } + } else { + null + }, + onClick = { apply(CalendarReminderOverride.None) }, + ) + Spacer(Modifier.height(24.dp)) val rowCount = rows.size + 1 // + the custom row rows.forEachIndexed { index, minute -> val checked = minute in selectedMinutes @@ -222,6 +244,27 @@ fun ReminderDefaultPicker( } } +/** + * The override a lead-time set maps to when emitted from [ReminderDefaultPicker]. + * A non-empty set is [CalendarReminderOverride.Minutes]; an empty set (the last + * time was unchecked) reverts to [CalendarReminderOverride.Inherit] on a + * per-calendar picker ([allowInherit]) so an accidental toggle-undo can't + * silently wipe the calendar's default, and to explicit + * [CalendarReminderOverride.None] on the global default (where empty legitimately + * means no reminder). Pure so it can be unit-tested. + */ +internal fun reminderOverrideForMinutes( + minutes: List, + allowInherit: Boolean, +): CalendarReminderOverride { + val norm = minutes.distinct().sorted() + return when { + norm.isNotEmpty() -> CalendarReminderOverride.Minutes(norm) + allowInherit -> CalendarReminderOverride.Inherit + else -> CalendarReminderOverride.None + } +} + /** * The expanded "Custom" lead-time editor: a tonal card connected to the Custom * row above it (matching the grouped-row system, so the two read as one diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ReminderOverrideForMinutesTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ReminderOverrideForMinutesTest.kt new file mode 100644 index 0000000..a900bd9 --- /dev/null +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ReminderOverrideForMinutesTest.kt @@ -0,0 +1,35 @@ +package de.jeanlucmakiola.calendula.ui.common + +import com.google.common.truth.Truth.assertThat +import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride +import org.junit.jupiter.api.Test + +/** + * The pure emit rule behind [ReminderDefaultPicker]: a non-empty selection is + * [CalendarReminderOverride.Minutes] (normalised), while clearing the last time + * reverts to [CalendarReminderOverride.Inherit] on a per-calendar picker (so an + * accidental toggle-undo can't silently wipe the calendar's default) and to + * explicit [CalendarReminderOverride.None] on the global default. + */ +class ReminderOverrideForMinutesTest { + + @Test + fun `empty selection on a per-calendar picker reverts to inherit`() { + assertThat(reminderOverrideForMinutes(emptyList(), allowInherit = true)) + .isEqualTo(CalendarReminderOverride.Inherit) + } + + @Test + fun `empty selection on the global default is explicit none`() { + assertThat(reminderOverrideForMinutes(emptyList(), allowInherit = false)) + .isEqualTo(CalendarReminderOverride.None) + } + + @Test + fun `a non-empty selection is normalised minutes regardless of scope`() { + assertThat(reminderOverrideForMinutes(listOf(30, 10, 10, 60), allowInherit = true)) + .isEqualTo(CalendarReminderOverride.Minutes(listOf(10, 30, 60))) + assertThat(reminderOverrideForMinutes(listOf(30, 10, 10, 60), allowInherit = false)) + .isEqualTo(CalendarReminderOverride.Minutes(listOf(10, 30, 60))) + } +} -- 2.49.1 From 44d056f31fbec2536a3bb56f823ce4be89fd3fb3 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:01:58 +0200 Subject: [PATCH 37/49] fix(settings): commit drag reorders unconditionally on release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The order mutation and onReorder call sat after the 160 ms settle animation inside the same cancellable coroutine; grabbing another row within that window cancelled the job between animation and commit, silently reverting a finished reorder. The commit now runs synchronously in onDragEnd — the settle is purely visual (the live offset is re-based onto the new slot in the same frame, so nothing jumps) and safe to interrupt. Also derive the hovered target slot via derivedStateOf so a drag only recomposes the column when a slot boundary is crossed, not on every pixel (the held row's own translation is already draw-phase). Co-Authored-By: Claude Sonnet 5 --- .../calendula/ui/common/ReorderableColumn.kt | 57 +++++++++++++------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt index d6a1f5e..f6ce3bd 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/ReorderableColumn.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -43,8 +44,10 @@ private val RowGap: Dp = 2.dp * can't nest a scrolling list). Rows are a fixed [ReorderableRowHeight] with a * uniform gap, so a row's target slot is simply how many whole pitches it has * been dragged. The held row follows the finger while the others slide out of - * its way (animated); on release the held row settles into its slot, then the - * order is committed with a single [onReorder] call. + * its way (animated); on release the order is committed immediately with a + * single [onReorder] call, then the held row eases into its new slot as a + * purely visual settle — safe to interrupt with another drag, since the + * commit itself never waits on it. * * [rowContent] receives the [Position] for the row's place in the order (to reuse * [GroupedRow]'s card shaping — pass `gapBelow = false` there, this owns spacing) @@ -66,16 +69,29 @@ fun ReorderableColumn( var order by remember(items) { mutableStateOf(items) } var draggedKey by remember { mutableStateOf(null) } // Live translation of the held row from its slot (px); also drives the - // release settle, animated back onto a slot before the order is committed. + // release settle — re-based onto the row's new slot once the order commits + // (see onDragEnd), then eased down to zero. var dragOffset by remember { mutableFloatStateOf(0f) } // The running release/cancel settle, cancelled if a new drag pre-empts it. + // Purely visual — the order commit (see onDragEnd) never depends on it. var settleJob by remember { mutableStateOf(null) } val draggedIndex = draggedKey?.let { key -> order.indexOfFirst { keyOf(it) == key }.takeIf { it >= 0 } } - // Whole slots dragged → the slot the held row currently hovers over. - val targetIndex = draggedIndex?.let { - (it + (dragOffset / pitchPx).roundToInt()).coerceIn(0, order.lastIndex) - } + // Whole slots dragged → the slot the held row currently hovers over. Derived + // so recomposition only fires when the *target slot* actually changes, not on + // every dragged pixel: dragOffset moves every frame during a drag, but the + // held row's own translation is already applied in the draw phase via + // graphicsLayer below, so only the neighbours' shift (which depends on this) + // needs to recompose, and only when a slot boundary is actually crossed. + // (Read as a plain val, not `by`, below: a delegated property has a custom + // getter and Kotlin won't smart-cast it in the `when` over `targetIndex` in + // the loop, so the derived value is captured into a real local instead.) + val targetIndex = remember(items, pitchPx) { + derivedStateOf { + val idx = draggedKey?.let { key -> order.indexOfFirst { keyOf(it) == key }.takeIf { it >= 0 } } + idx?.let { (it + (dragOffset / pitchPx).roundToInt()).coerceIn(0, order.lastIndex) } + } + }.value Column(modifier, verticalArrangement = Arrangement.spacedBy(RowGap)) { order.forEachIndexed { index, item -> @@ -112,16 +128,25 @@ fun ReorderableColumn( val from = order.indexOfFirst { keyOf(it) == key } if (from < 0) return@detectDragGestures val to = (from + (dragOffset / pitchPx).roundToInt()).coerceIn(0, order.lastIndex) + if (to != from) { + // Commit synchronously and unconditionally, before the settle + // animation below runs — the commit must not depend on that + // coroutine reaching its end, or a new drag starting within the + // ~160ms settle window would cancel it and silently revert an + // already-finished reorder. + order = order.toMutableList().apply { add(to, removeAt(from)) } + onReorder(order) + // The row now lays out at slot `to` instead of `from`; re-base + // the live offset onto that new slot (same visual position, + // expressed relative to the new one) so the settle below eases + // it the rest of the way instead of jumping. + dragOffset -= (to - from) * pitchPx + } settleJob = scope.launch { - // Settle the held row onto its target slot, then commit — - // resetting offset and slot in the same frame, so nothing jumps. - Animatable(dragOffset).animateTo((to - from) * pitchPx, tween(160)) { - dragOffset = value - } - if (to != from) { - order = order.toMutableList().apply { add(to, removeAt(from)) } - onReorder(order) - } + // Purely visual from here: ease the held row onto its slot, then + // release the drag state. Safe to cancel — the order was already + // committed above. + Animatable(dragOffset).animateTo(0f, tween(160)) { dragOffset = value } draggedKey = null dragOffset = 0f } -- 2.49.1 From 3863e7485724bdb742e235f6189b9efc4aa37de4 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:01:58 +0200 Subject: [PATCH 38/49] fix(settings): quick-switch write races, override parsing, stale custom fonts Four fixes across the settings/prefs layer: - Quick-switch toggles and reorders were read-modify-write against the async-echoed UI snapshot, so two rapid interactions reverted each other. Writes now go through SettingsPrefs.updateQuickSwitch, an atomic transform over the currently-stored value, via intent-level ViewModel ops; the MIN_ENABLED floor is re-checked inside the transform since the screen's guard reads the stale snapshot. - parseReminderOverrides treated any unparseable stored value as an explicit empty override (no reminder). Only the deliberate 'none' sentinel means that now; garbage drops the entry so the calendar inherits the global default. Partially-valid values salvage their valid minutes. - Replacing an already-active custom font never refreshed typography: the unchanged 'custom' token made AppFontSettings value-equal, so the StateFlow never re-emitted. A per-role import stamp now breaks equality on re-import (missing key = 0, backward compatible). - The FontPicker custom preview resolved the font file unmemoized on every recomposition (disk stat + fresh FontFamily defeating the typeface cache); it's now remembered, keyed on the import stamp. Co-Authored-By: Claude Opus 4.8 --- .../jeanlucmakiola/calendula/MainActivity.kt | 6 ++- .../calendula/data/prefs/SettingsPrefs.kt | 46 ++++++++++++++++- .../calendula/ui/settings/SettingsScreen.kt | 18 ++++--- .../ui/settings/SettingsViewModel.kt | 32 ++++++++++-- .../calendula/ui/theme/Fonts.kt | 5 ++ .../calendula/data/prefs/SettingsPrefsTest.kt | 50 +++++++++++++++++++ .../calendula/ui/theme/FontsTest.kt | 9 ++++ 7 files changed, 153 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index 962e46f..f3fe318 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -103,8 +103,10 @@ class MainActivity : AppCompatActivity() { settings.timeFormat.is24Hour(android.text.format.DateFormat.is24HourFormat(context)) } // The user's custom-font choice, resolved to a Material typography - // (issue #19). Recomputed only when a token changes; "system for both" - // returns the default scale untouched. + // (issue #19). Recomputed only when a token — or the custom-font + // re-import stamp AppFontSettings carries, so replacing the file + // behind an active "custom" token still refreshes — changes; + // "system for both" returns the default scale untouched. val fonts by settingsViewModel.fontState.collectAsStateWithLifecycle() val typography = remember(fonts, context) { calendulaTypography( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt index d73d49f..657d505 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import de.jeanlucmakiola.calendula.domain.EventFormField +import de.jeanlucmakiola.calendula.domain.FontRole import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.parseAgendaRange import de.jeanlucmakiola.calendula.ui.agenda.storageValue @@ -128,6 +129,26 @@ class SettingsPrefs @Inject constructor( store.edit { it[PLAIN_FONT_KEY] = token } } + /** + * A per-role bump counter for the user-loaded custom font. Re-importing + * overwrites the same file under the same "custom" token, so the token alone + * can't signal the change; this stamp — carried into AppFontSettings — breaks + * value equality so the resolved FontFamily (and its previews) refresh. A + * missing key is 0 (existing installs); the first import bumps it to 1. + */ + val brandFontStamp: Flow = store.data.map { prefs -> prefs[BRAND_FONT_STAMP_KEY] ?: 0 } + + val plainFontStamp: Flow = store.data.map { prefs -> prefs[PLAIN_FONT_STAMP_KEY] ?: 0 } + + /** Bump [role]'s custom-font stamp after a re-import so equality-keyed caches refresh. */ + suspend fun bumpCustomFontStamp(role: FontRole) { + val key = when (role) { + FontRole.BRAND -> BRAND_FONT_STAMP_KEY + FontRole.PLAIN -> PLAIN_FONT_STAMP_KEY + } + store.edit { it[key] = (it[key] ?: 0) + 1 } + } + suspend fun setWeekStart(pref: WeekStartPref) { store.edit { it[WEEK_START_KEY] = pref.storageValue() } } @@ -241,6 +262,19 @@ class SettingsPrefs @Inject constructor( store.edit { it[QUICK_SWITCH_VIEWS_KEY] = serializeQuickSwitch(config) } } + /** + * Atomic read-modify-write of the quick-switch config: [transform] sees the + * value currently stored, parsed inside the edit block, not the async-echoed + * UI snapshot. Two rapid intents (a toggle then a drag) therefore compose + * instead of each re-serialising a stale copy of the other field over it. + */ + suspend fun updateQuickSwitch(transform: (QuickSwitchConfig) -> QuickSwitchConfig) { + store.edit { prefs -> + val current = parseQuickSwitch(prefs[QUICK_SWITCH_VIEWS_KEY]) + prefs[QUICK_SWITCH_VIEWS_KEY] = serializeQuickSwitch(transform(current)) + } + } + /** * Navigation-drawer view order (#24). Comma-joined enum names; missing views * are appended in default order and unknown names dropped. Absent key means @@ -525,6 +559,8 @@ class SettingsPrefs @Inject constructor( internal val DYNAMIC_COLOR_KEY = booleanPreferencesKey("dynamic_color") internal val BRAND_FONT_KEY = stringPreferencesKey("brand_font") internal val PLAIN_FONT_KEY = stringPreferencesKey("plain_font") + internal val BRAND_FONT_STAMP_KEY = intPreferencesKey("brand_font_stamp") + internal val PLAIN_FONT_STAMP_KEY = intPreferencesKey("plain_font_stamp") internal val WEEK_START_KEY = stringPreferencesKey("week_start") internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range") internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range") @@ -675,7 +711,15 @@ private fun parseReminderOverrides(stored: String?): Map> { return stored.split(ENTRY_SEP).mapNotNull { entry -> val parts = entry.split(KEY_VALUE_SEP).takeIf { it.size == 2 } ?: return@mapNotNull null val id = parts[0].toLongOrNull() ?: return@mapNotNull null - id to parts[1].toReminderList() + // Only the deliberate "none" sentinel means an explicit no-reminder + // override (empty list); a non-sentinel value that parses to no valid + // minutes is garbage and drops the entry, so the calendar inherits the + // global default rather than silently reading as "no reminder". + when (val value = parts[1]) { + NONE -> id to emptyList() + else -> value.toReminderList().takeIf { it.isNotEmpty() }?.let { id to it } + ?: return@mapNotNull null + } }.toMap() } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index 0d73236..8d5a4d7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -671,6 +671,7 @@ private fun AppearanceScreen( title = stringResource(R.string.settings_font_headings), role = FontRole.BRAND, selected = fonts.brand, + stamp = fonts.brandStamp, onSelect = { viewModel.setFont(FontRole.BRAND, it) }, onImport = { viewModel.importCustomFont(FontRole.BRAND, it) }, onDismiss = { showBrandFont = false }, @@ -681,6 +682,7 @@ private fun AppearanceScreen( title = stringResource(R.string.settings_font_body), role = FontRole.PLAIN, selected = fonts.plain, + stamp = fonts.plainStamp, onSelect = { viewModel.setFont(FontRole.PLAIN, it) }, onImport = { viewModel.importCustomFont(FontRole.PLAIN, it) }, onDismiss = { showPlainFont = false }, @@ -773,7 +775,7 @@ private fun ViewsScreen( ReorderableColumn( items = config.order, keyOf = { it }, - onReorder = { viewModel.setQuickSwitchConfig(config.copy(order = it)) }, + onReorder = { viewModel.setQuickSwitchOrder(it) }, ) { view, position, dragHandle, isDragging -> val checked = view in config.enabled ViewRow( @@ -787,10 +789,7 @@ private fun ViewsScreen( checked = checked, // Keep the last two on: with fewer, the pill can't switch. enabled = !checked || canDisable, - onCheckedChange = { on -> - val enabled = if (on) config.enabled + view else config.enabled - view - viewModel.setQuickSwitchConfig(config.copy(enabled = enabled)) - }, + onCheckedChange = { on -> viewModel.setQuickSwitchViewEnabled(view, on) }, ) }, ) @@ -1431,6 +1430,7 @@ private fun FontPicker( title: String, role: FontRole, selected: String, + stamp: Int, onSelect: (String) -> Unit, onImport: (Uri) -> Unit, onDismiss: () -> Unit, @@ -1448,6 +1448,12 @@ private fun FontPicker( // System default + the bundled fonts + the "Choose file…" row. val rowCount = BundledFont.entries.size + 2 val isCustom = selected == FONT_CUSTOM_TOKEN + // Resolving the custom face stats the disk and builds a fresh FontFamily, so + // memoise it; re-keyed on [stamp] (bumped on re-import) so a replaced file + // refreshes the preview while plain recompositions reuse the cached family. + val customPreview = remember(role, isCustom, stamp) { + if (isCustom) resolveFontFamily(FONT_CUSTOM_TOKEN, role, context) else null + } FullScreenPicker(title = title, onDismiss = onDismiss) { FontOptionRow( @@ -1479,7 +1485,7 @@ private fun FontPicker( stringResource(R.string.settings_font_choose_file) }, // A loaded font previews in its own face; otherwise show an upload cue. - preview = if (isCustom) resolveFontFamily(FONT_CUSTOM_TOKEN, role, context) else null, + preview = customPreview, leadingIcon = if (isCustom) null else Icons.Default.UploadFile, selected = isCustom, position = positionOf(rowCount - 1, rowCount), diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt index c510e19..a3c7fc3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsViewModel.kt @@ -159,8 +159,10 @@ class SettingsViewModel @Inject constructor( val fontState: StateFlow = combine( prefs.brandFont, prefs.plainFont, - ) { brand, plain -> - AppFontSettings(brand = brand, plain = plain) + prefs.brandFontStamp, + prefs.plainFontStamp, + ) { brand, plain, brandStamp, plainStamp -> + AppFontSettings(brand = brand, plain = plain, brandStamp = brandStamp, plainStamp = plainStamp) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), @@ -239,6 +241,10 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { val ok = withContext(io) { CustomFontStore.import(appContext, role, uri) } if (ok) { + // Bump the stamp so replacing an already-active custom font (same + // file, same "custom" token) still breaks font-settings equality + // and refreshes the resolved typeface. + prefs.bumpCustomFontStamp(role) setFont(role, FONT_CUSTOM_TOKEN) } else { _fontImportFailed.emit(Unit) @@ -319,8 +325,26 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { prefs.setFormFieldDefault(field, enabled) } } - fun setQuickSwitchConfig(config: QuickSwitchConfig) { - viewModelScope.launch { prefs.setQuickSwitchConfig(config) } + /** + * Enable or disable [view] in the quick-switch cycle. Goes through an atomic + * read-modify-write so a concurrent reorder can't clobber this toggle (and + * vice versa) by re-serialising a stale snapshot. The MIN_ENABLED floor is + * re-checked inside the transform: the screen's own guard reads the + * async-echoed snapshot, so two quick disable-taps could both look allowed + * yet compose to a below-minimum cycle. + */ + fun setQuickSwitchViewEnabled(view: CalendarView, enabled: Boolean) { + viewModelScope.launch { + prefs.updateQuickSwitch { config -> + val next = if (enabled) config.enabled + view else config.enabled - view + if (next.size < QuickSwitchConfig.MIN_ENABLED) config else config.copy(enabled = next) + } + } + } + + /** Reorder the quick-switch views (atomic read-modify-write; keeps the enabled set). */ + fun setQuickSwitchOrder(order: List) { + viewModelScope.launch { prefs.updateQuickSwitch { it.copy(order = order) } } } fun setDrawerViewOrder(order: List) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt index 8b2b135..1dacb99 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Fonts.kt @@ -71,6 +71,11 @@ private fun weightAxis(weight: Int) = FontVariation.Settings(FontVariation.weigh data class AppFontSettings( val brand: String = FONT_SYSTEM_TOKEN, val plain: String = FONT_SYSTEM_TOKEN, + // Per-role custom-font re-import stamp: bumped when the "custom" file is + // replaced, so a same-token re-import still breaks equality and refreshes the + // resolved family. 0 for existing installs / no import yet (issue #19). + val brandStamp: Int = 0, + val plainStamp: Int = 0, ) /** diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt index 6de61e9..a8cd3fe 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt @@ -5,6 +5,7 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.Preferences import com.google.common.truth.Truth.assertThat import de.jeanlucmakiola.calendula.domain.EventFormField +import de.jeanlucmakiola.calendula.domain.FontRole import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS @@ -198,6 +199,22 @@ class SettingsPrefsTest { ) } + @Test + fun `updateQuickSwitch transforms the stored value, so intents compose`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + // A toggle then a reorder, each expressed against the *stored* config — + // the reorder must not resurrect the just-disabled view (the stale-UI- + // snapshot race this API exists to prevent). + prefs.updateQuickSwitch { it.copy(enabled = it.enabled - CalendarView.Day) } + val newOrder = listOf(CalendarView.Agenda, CalendarView.Month, CalendarView.Week, CalendarView.Day) + prefs.updateQuickSwitch { it.copy(order = newOrder) } + val config = prefs.quickSwitchConfig.first() + assertThat(config.order).containsExactlyElementsIn(newOrder).inOrder() + assertThat(config.enabled).containsExactly( + CalendarView.Agenda, CalendarView.Month, CalendarView.Week, + ) + } + @Test fun `drawer order defaults to the implemented order and round-trips`(@TempDir tempDir: Path) = runTest { val prefs = SettingsPrefs(newDataStore(tempDir)) @@ -319,6 +336,26 @@ class SettingsPrefsTest { } } + @Test + fun `garbage per-calendar override values drop the entry, the none sentinel stays explicit`( + @TempDir tempDir: Path, + ) = runTest { + val store = newDataStore(tempDir) + val prefs = SettingsPrefs(store) + // Only "none" deliberately means an explicit no-reminder override; a + // value with no valid minutes is garbage and must drop its entry so the + // calendar inherits the global default instead of silently losing + // reminders. Partially-valid values keep their salvageable minutes. + store.updateData { p -> + val m = p.toMutablePreferences() + m[SettingsPrefs.CALENDAR_REMINDER_OVERRIDE_KEY] = "5=none;7=abc;8=;9=30,abc;11=15" + m + } + assertThat(prefs.perCalendarReminderOverride.first()).containsExactly( + 5L, emptyList(), 9L, listOf(30), 11L, listOf(15), + ) + } + @Test fun `all-day default round-trips, including none`(@TempDir tempDir: Path) = runTest { val prefs = SettingsPrefs(newDataStore(tempDir)) @@ -402,6 +439,19 @@ class SettingsPrefsTest { assertThat(prefs.snoozeMinutes.first()).isEqualTo(1) } + @Test + fun `custom-font stamps default to zero and bump per role independently`(@TempDir tempDir: Path) = runTest { + val prefs = SettingsPrefs(newDataStore(tempDir)) + // Existing installs have no stamp key — that must read as 0. + assertThat(prefs.brandFontStamp.first()).isEqualTo(0) + assertThat(prefs.plainFontStamp.first()).isEqualTo(0) + prefs.bumpCustomFontStamp(FontRole.BRAND) + prefs.bumpCustomFontStamp(FontRole.BRAND) + prefs.bumpCustomFontStamp(FontRole.PLAIN) + assertThat(prefs.brandFontStamp.first()).isEqualTo(2) + assertThat(prefs.plainFontStamp.first()).isEqualTo(1) + } + @Test fun `explicit week-start prefs resolve regardless of locale`() { assertThat(WeekStartPref.Day(DayOfWeek.MONDAY).resolveFirstDay(Locale.US)) diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt index 998ef53..ab84941 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/theme/FontsTest.kt @@ -20,6 +20,15 @@ class FontsTest { assertThat(BundledFont.fromToken(null)).isNull() } + @Test + fun `a bumped custom-font stamp breaks font-settings equality`() { + // Re-importing keeps the same "custom" token, so only the stamp can make + // the settings re-emit and invalidate remember() keys downstream. + val before = AppFontSettings(brand = FONT_CUSTOM_TOKEN, brandStamp = 1) + assertThat(before.copy(brandStamp = 2)).isNotEqualTo(before) + assertThat(before.copy(plainStamp = 1)).isNotEqualTo(before) + } + @Test fun `system for both roles returns the untouched default typography`() { assertThat(calendulaTypography(brand = null, plain = null)) -- 2.49.1 From 9f7e93d5f83cc0910b024cbf02c0f8193d781d1b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:03:33 +0200 Subject: [PATCH 39/49] fix(contacts): seal managed calendars off from user edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Managed special-dates calendars were only recognised by an id cached in preferences, and were offered as ordinary write targets — so a user could create an event in 'Birthdays' (which the next sync then deleted), and after a backup restore wiped the prefs the editor lock silently disappeared. CalendarSource now carries isManaged, read from the durable CAL_SYNC2 marker the data layer already stamps, so identity survives a restore. With it: - the new-event calendar picker excludes managed calendars (no user events land in a calendar the sync owns); - the editor lock keys off the marker, not a stored id; - a managed event's save is forced to the whole series instead of parking in the scope dialog (a 'this/following' split creates rows the sync reverts); - the detail sheet hides Delete for managed events (the sync would just resurrect them — a contact date is removed at its source). Co-Authored-By: Claude Fable 5 --- .../calendula/data/calendar/CalendarMapper.kt | 7 ++++ .../calendula/data/calendar/Projections.kt | 9 +++++ .../jeanlucmakiola/calendula/domain/Models.kt | 8 +++++ .../calendula/ui/detail/EventDetailScreen.kt | 20 ++++++----- .../calendula/ui/detail/EventDetailUiState.kt | 6 ++++ .../ui/detail/EventDetailViewModel.kt | 1 + .../calendula/ui/edit/EventEditViewModel.kt | 36 ++++++++++++++----- 7 files changed, 70 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarMapper.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarMapper.kt index dda3126..c4d214a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarMapper.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarMapper.kt @@ -24,5 +24,12 @@ internal fun ColumnReader.toCalendarSource(): CalendarSource { } else { null }, + // A special-dates mirror calendar, recognised by its durable CAL_SYNC2 + // marker (only meaningful on the local calendars the app owns). This is + // the source of truth for the editor lock — independent of any stored + // preference that a backup restore could have wiped. + isManaged = isLocal && + getString(CalendarProjection.IDX_MANAGED_MARKER) + ?.startsWith(CalendarProjection.MANAGED_MARKER_PREFIX) == true, ) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt index 5a362bf..2066ac7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/Projections.kt @@ -15,9 +15,17 @@ internal object CalendarProjection { // own we stash one in CAL_SYNC1 (synced rows put their sync token here, // so the mapper only reads it for local calendars). DESCRIPTION_COLUMN, + // The special-dates marker (CAL_SYNC2) — the durable identity the app + // uses to recognise its own managed calendars, independent of any + // stored preference id (which a backup restore / data wipe can lose). + MANAGED_MARKER_COLUMN, ) const val DESCRIPTION_COLUMN: String = CalendarContract.Calendars.CAL_SYNC1 + const val MANAGED_MARKER_COLUMN: String = CalendarContract.Calendars.CAL_SYNC2 + + /** Namespace prefix of every managed-calendar [MANAGED_MARKER_COLUMN] value. */ + const val MANAGED_MARKER_PREFIX = "calendula.specialdates" const val IDX_ID = 0 const val IDX_DISPLAY_NAME = 1 @@ -27,6 +35,7 @@ internal object CalendarProjection { const val IDX_VISIBLE = 5 const val IDX_ACCESS_LEVEL = 6 const val IDX_DESCRIPTION = 7 + const val IDX_MANAGED_MARKER = 8 } internal object InstanceProjection { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/domain/Models.kt b/app/src/main/java/de/jeanlucmakiola/calendula/domain/Models.kt index 5d5b89f..aaf4fb5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/domain/Models.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/Models.kt @@ -26,6 +26,14 @@ data class CalendarSource( * owns for its own calendars). Always null for synced calendars. */ val description: String? = null, + /** + * A special-dates mirror calendar the app manages (birthdays/anniversaries + * from contacts). Its events' title/date/recurrence are owned by the sync, + * so it's hidden from the new-event calendar picker and its events lock those + * fields in the editor. Recognised by a durable provider marker, so it holds + * even after a backup restore clears the app's stored ids. + */ + val isManaged: Boolean = false, ) data class EventInstance( diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt index c187ac3..eefce33 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt @@ -250,14 +250,18 @@ fun EventDetailScreen( contentDescription = stringResource(R.string.event_detail_edit), ) } - IconButton( - onClick = onDeleteClick, - enabled = deleteState != DeleteUiState.Deleting, - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.event_detail_delete), - ) + // Managed special-dates events have no delete — the sync + // would resurrect them; the date is removed at the contact. + if (!s.isManaged) { + IconButton( + onClick = onDeleteClick, + enabled = deleteState != DeleteUiState.Deleting, + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.event_detail_delete), + ) + } } } }, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailUiState.kt index 60d405b..6f6dd97 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailUiState.kt @@ -15,6 +15,12 @@ sealed interface EventDetailUiState { val calendarName: String?, /** Whether the owning calendar allows modifying events (shows edit/delete). */ val canModify: Boolean = false, + /** + * The event belongs to a managed special-dates calendar: its editable + * fields (reminders, notes) can still be edited, but delete is hidden — + * the sync would just resurrect it. Contact dates are removed at the source. + */ + val isManaged: Boolean = false, ) : EventDetailUiState } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt index 40f7ce8..8374c21 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailViewModel.kt @@ -154,6 +154,7 @@ class EventDetailViewModel @Inject constructor( detail = corrected, calendarName = calendar?.displayName, canModify = calendar?.canModifyContents == true, + isManaged = calendar?.isManaged == true, ) } catch (e: CancellationException) { throw e diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt index 673501a..fe1ec73 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditViewModel.kt @@ -122,18 +122,24 @@ class EventEditViewModel @Inject constructor( val autofocusTitle: Boolean, ) + /** Every calendar the provider exposes; the source for both lists below. */ + private val allCalendars: Flow> = + repository.calendars().catch { emit(emptyList()) } + /** * Writable calendars — the only valid event targets. Disabled calendars are * excluded, so you can't create into a calendar you've removed from the app; * a last-used preselect landing on a now-disabled calendar falls back to the * first remaining writable one (handled by [resolvedCalendarId] and [state]). + * Managed special-dates calendars are excluded too: their events are owned by + * the contact sync, which would delete any user event created there. */ private val writableCalendars: Flow> = combine( - repository.calendars(), + allCalendars, prefs.disabledCalendarIds, ) { calendars, disabled -> - calendars.filter { it.canModifyContents && it.id !in disabled } - }.catch { emit(emptyList()) } + calendars.filter { it.canModifyContents && it.id !in disabled && !it.isManaged } + } /** The target calendar id, resolved exactly as the form shows it. */ private val resolvedCalendarId: Flow = combine( @@ -165,24 +171,33 @@ class EventEditViewModel @Inject constructor( ::ExternalInputs, ).flowOn(io), colorPalette, - settingsPrefs.managedCalendarIds, - ) { local, external, palette, managedIds -> + allCalendars, + ) { local, external, palette, allCalendars -> val form = local.form ?: return@combine null val resolvedId = form.calendarId ?: external.lastUsed?.takeIf { id -> external.writable.any { it.id == id } } ?: external.writable.firstOrNull()?.id val resolved = form.copy(calendarId = resolvedId) + val resolvedCalendar = allCalendars.firstOrNull { it.id == resolvedId } + // Editing an event in a managed calendar locks its synced fields. Keyed + // off the calendar's durable marker, not a stored id, so it holds after a + // backup restore too. + val isManaged = local.editTarget != null && resolvedCalendar?.isManaged == true + // The picker offers writable calendars only; when editing a managed event + // its own (excluded) calendar is added back so the row still names it. + val pickerCalendars = + if (isManaged && resolvedCalendar != null) external.writable + resolvedCalendar + else external.writable val visibleFields = external.defaultFields + local.revealed EventEditUiState( form = resolved, - calendars = external.writable, + calendars = pickerCalendars, problems = if (local.showProblems) resolved.problems() else emptySet(), saveState = local.saveState, visibleFields = visibleFields, hiddenFields = (EventFormField.entries.toSet() - visibleFields).sorted(), isEditing = local.editTarget != null, - // Editing an event in a managed calendar locks its synced fields. - isManaged = local.editTarget != null && resolvedId != null && resolvedId in managedIds, + isManaged = isManaged, autofocusTitle = external.autofocusTitle, // A modified-occurrence exception can't carry its own rule, so // the scope dialog drops "only this event" after a rule change. @@ -420,7 +435,10 @@ class EventEditViewModel @Inject constructor( _saveState.value = SaveUiState.Saved return } - if (target != null && target.original.rrule != null) { + // Managed events are a yearly series whose editable fields (reminders, + // notes, location) live on the series row — never offer the scope dialog, + // which would split the series into an exception the sync then reverts. + if (target != null && target.original.rrule != null && !current.isManaged) { _saveState.value = SaveUiState.AwaitingScope return } -- 2.49.1 From 8881559e67b3cf5f36c921e07ec622b14c5207b9 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:03:51 +0200 Subject: [PATCH 40/49] fix(contacts): scope managed-event queries + fix reminder fire hour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two provider-level bugs in the managed-event data path: - queryManagedEvents and applyManagedCalendarReminders matched every event in the calendar (UID_2445 IS NOT NULL / no filter). A stray user event there (e.g. an .ics import) was treated as 'existing but not desired' and deleted, or had its own reminders wiped and all-day-re-encoded. Both now match only our own mirror events (the 'contact-' UID prefix). - All-day reminder offsets were sampled at the event's DTSTART, which for a year-less birthday is the 1972 leap anchor — a year whose timezone offset (pre-DST) differs from today's, skewing every modern occurrence by up to an hour. The offset is now sampled at the upcoming occurrence (nextYearlyOccurrence), leaving only the inherent ±1h DST drift. Co-Authored-By: Claude Fable 5 --- .../data/calendar/AllDayReminderEncoding.kt | 21 +++++ .../data/calendar/CalendarDataSource.kt | 89 +++++++++++++------ .../calendar/AllDayReminderEncodingTest.kt | 37 ++++++++ 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncoding.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncoding.kt index 72afc6a..446489c 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncoding.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncoding.kt @@ -50,6 +50,27 @@ internal fun toProviderAllDayMinutes( return ((utcMidnight - fire) / MILLIS_PER_MINUTE).toInt() } +/** + * The next date on or after [today] carrying [month]/[day] — the date to sample + * a yearly all-day reminder's UTC offset at. A managed birthday's DTSTART is an + * ancient anchor (1972 for year-less dates), a year whose timezone rules + * (pre-DST offsets) differ from today's; sampling the offset there skews every + * modern occurrence by hours. Sampling at the upcoming occurrence makes the + * stored offset correct for it and its neighbours (only ±1h DST drift remains, + * the inherent limit). Feb-29 skips forward to the next leap year. + */ +internal fun nextYearlyOccurrence(month: Int, day: Int, today: LocalDate): LocalDate { + var year = today.year + // A Feb-29 date is valid only every ~4 years; 8 tries always reaches one. + repeat(8) { + val candidate = runCatching { LocalDate.of(year, month, day) }.getOrNull() + if (candidate != null && !candidate.isBefore(today)) return candidate + year++ + } + // Unreachable for real month/day inputs; fall back to the raw anchor. + return LocalDate.of(today.year, month, day) +} + /** * Recover the semantic whole-day lead time from a raw all-day reminder * [rawMinutes]. Keys off the **local date** of the encoded fire instant, so it diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt index 3a8e854..0c01ef2 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt @@ -24,12 +24,14 @@ import de.jeanlucmakiola.calendula.domain.EventForm import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.EventStatus import de.jeanlucmakiola.calendula.domain.Reminder +import de.jeanlucmakiola.calendula.domain.contacts.MANAGED_UID_PREFIX import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.domain.ics.IcsEvent import de.jeanlucmakiola.calendula.domain.ics.ParsedIcsEvent import de.jeanlucmakiola.calendula.domain.rruleTruncatedAt import kotlinx.datetime.toJavaLocalDate import java.time.Instant +import java.time.LocalDate import java.time.ZoneId import java.time.ZoneOffset import java.util.UUID @@ -135,11 +137,12 @@ interface CalendarDataSource { fun updateManagedFields(eventId: Long, values: Map) /** - * Replace the reminders on **every** event in [calendarId] with [minutes] - * (all-day lead times), each encoded from its own date so it fires at - * [allDayReminderTimeMinutes]. Used by the special-dates section so a reminder - * change applies to existing events too, not just future ones — the managed - * calendars treat reminders as a calendar-level setting. + * Replace the reminders on **every managed** event in [calendarId] with + * [minutes] (all-day lead times), each encoded so it fires at + * [allDayReminderTimeMinutes] on its upcoming occurrence. Used by the + * special-dates section so a reminder change applies to existing events too, + * not just future ones — the managed calendars treat reminders as a + * calendar-level setting. Skips any non-managed (user) event in the calendar. */ fun applyManagedCalendarReminders( calendarId: Long, @@ -393,10 +396,13 @@ class AndroidCalendarDataSource @Inject constructor( CalendarContract.Events.DTSTART, CalendarContract.Events.RRULE, ), + // Only our own mirror events (UID prefix), so a user event that somehow + // landed in this calendar (e.g. an .ics import) is never seen as + // "existing but not desired" and deleted by the diff. "${CalendarContract.Events.CALENDAR_ID} = ? AND " + "${CalendarContract.Events.DELETED} = 0 AND " + - "${CalendarContract.Events.UID_2445} IS NOT NULL", - arrayOf(calendarId.toString()), + "${CalendarContract.Events.UID_2445} LIKE ?", + arrayOf(calendarId.toString(), "$MANAGED_UID_PREFIX%"), null, )?.use { c -> buildList { @@ -418,13 +424,19 @@ class AndroidCalendarDataSource @Inject constructor( override fun insertManagedEvent(form: EventForm, uid: String, allDayReminderTimeMinutes: Int): Long { // Deterministic UID = the reconciliation key. A re-sync reads it back // (queryManagedEvents) and diffs on it, so a contact never doubles. - val values = buildEventInsertValues(form, uid, form.toWriteTimes(ZoneId.systemDefault())) + val times = form.toWriteTimes(ZoneId.systemDefault()) + val values = buildEventInsertValues(form, uid, times) val eventId = resolver.insert(CalendarContract.Events.CONTENT_URI, values.toContentValues()) ?.let(ContentUris::parseId) ?: throw WriteFailedException("insert managed event into calendar id=${form.calendarId}") // Seeded once, then never re-touched by sync — so a user who moves a - // birthday reminder keeps their change. - seedReminders(eventId, form, allDayReminderTimeMinutes) + // birthday reminder keeps their change. Encoded against the upcoming + // occurrence, not the ancient DTSTART anchor (wrong timezone offset). + val anchor = Instant.ofEpochMilli(times.dtStartMillis).atZone(ZoneOffset.UTC).toLocalDate() + insertReminderRows( + eventId, + managedAllDayReminderMinutes(anchor, form.reminders, allDayReminderTimeMinutes), + ) return eventId } @@ -443,23 +455,26 @@ class AndroidCalendarDataSource @Inject constructor( allDayReminderTimeMinutes: Int, minutes: List, ) { - val zone = ZoneId.systemDefault() resolver.query( CalendarContract.Events.CONTENT_URI, arrayOf(CalendarContract.Events._ID, CalendarContract.Events.DTSTART), - "${CalendarContract.Events.CALENDAR_ID} = ? AND ${CalendarContract.Events.DELETED} = 0", - arrayOf(calendarId.toString()), + // Only our own mirror events, so a stray user event in this calendar + // isn't all-day-re-encoded or stripped of its own reminders. + "${CalendarContract.Events.CALENDAR_ID} = ? AND " + + "${CalendarContract.Events.DELETED} = 0 AND " + + "${CalendarContract.Events.UID_2445} LIKE ?", + arrayOf(calendarId.toString(), "$MANAGED_UID_PREFIX%"), null, )?.use { c -> while (c.moveToNext()) { val eventId = c.getLong(0) - // Managed events are all-day: DTSTART is a UTC midnight. - val startDate = Instant.ofEpochMilli(c.getLong(1)) + // Managed events are all-day: DTSTART is a UTC midnight anchor. + val anchor = Instant.ofEpochMilli(c.getLong(1)) .atZone(ZoneOffset.UTC).toLocalDate() - val encoded = minutes - .map { toProviderAllDayMinutes(it, startDate, zone, allDayReminderTimeMinutes) } - .distinct() - reconcileReminders(eventId, encoded) + reconcileReminders( + eventId, + managedAllDayReminderMinutes(anchor, minutes, allDayReminderTimeMinutes), + ) } } } @@ -780,8 +795,12 @@ class AndroidCalendarDataSource @Inject constructor( * (spec §8): the event exists at this point — a reminder that fails to * attach is logged, not surfaced as a failed create. */ - private fun seedReminders(eventId: Long, form: EventForm, allDayReminderTimeMinutes: Int) { - encodedReminders(form, allDayReminderTimeMinutes).forEach { minutes -> + private fun seedReminders(eventId: Long, form: EventForm, allDayReminderTimeMinutes: Int) = + insertReminderRows(eventId, encodedReminders(form, allDayReminderTimeMinutes)) + + /** Insert one `METHOD_ALERT` reminder row per raw provider offset. */ + private fun insertReminderRows(eventId: Long, encodedMinutes: List) { + encodedMinutes.forEach { minutes -> val reminder = ContentValues().apply { put(CalendarContract.Reminders.EVENT_ID, eventId) put(CalendarContract.Reminders.MINUTES, minutes) @@ -793,6 +812,25 @@ class AndroidCalendarDataSource @Inject constructor( } } + /** + * A managed all-day event's reminders, encoded so they fire at + * [allDayReminderTimeMinutes] on its **upcoming** occurrence. The event's + * DTSTART is an ancient recurrence anchor (see [nextYearlyOccurrence]); using + * it directly would sample a stale timezone offset, so the offset is sampled + * at the next occurrence of the anchor's month/day instead. + */ + private fun managedAllDayReminderMinutes( + anchor: LocalDate, + reminders: List, + allDayReminderTimeMinutes: Int, + ): List { + val zone = ZoneId.systemDefault() + val onDate = nextYearlyOccurrence(anchor.monthValue, anchor.dayOfMonth, LocalDate.now(zone)) + return reminders + .map { toProviderAllDayMinutes(it, onDate, zone, allDayReminderTimeMinutes) } + .distinct() + } + override fun updateEvent( eventId: Long, original: EventForm, @@ -1217,11 +1255,12 @@ class AndroidCalendarDataSource @Inject constructor( const val LOCAL_ACCOUNT_NAME = "Calendula" /** - * Column and namespace for the special-dates marker. CAL_SYNC1 already - * holds the local-calendar description, so the marker uses CAL_SYNC2. + * Column and namespace for the special-dates marker (shared with the + * calendar projection, which reads it into [CalendarSource.isManaged]). + * CAL_SYNC1 already holds the local-calendar description, so it uses CAL_SYNC2. */ - val MANAGED_MARKER_COLUMN: String = CalendarContract.Calendars.CAL_SYNC2 - const val MANAGED_MARKER_PREFIX = "calendula.specialdates" + val MANAGED_MARKER_COLUMN: String = CalendarProjection.MANAGED_MARKER_COLUMN + const val MANAGED_MARKER_PREFIX = CalendarProjection.MANAGED_MARKER_PREFIX /** * How far ahead/behind a search looks for a recurring event's nearest diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncodingTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncodingTest.kt index 51083a5..7ebb69b 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncodingTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/AllDayReminderEncodingTest.kt @@ -83,6 +83,43 @@ class AllDayReminderEncodingTest { assertThat(toProviderAllDayMinutes(-1, summer, berlin, nineAm)).isEqualTo(-1) } + @Test + fun `next yearly occurrence wraps to next year once this year's date has passed`() { + val today = LocalDate.of(2026, 8, 1) + assertThat(nextYearlyOccurrence(5, 14, today)).isEqualTo(LocalDate.of(2027, 5, 14)) + assertThat(nextYearlyOccurrence(12, 25, today)).isEqualTo(LocalDate.of(2026, 12, 25)) + } + + @Test + fun `next yearly occurrence returns today when the date is today`() { + val today = LocalDate.of(2026, 3, 10) + assertThat(nextYearlyOccurrence(3, 10, today)).isEqualTo(today) + } + + @Test + fun `next yearly occurrence skips non-leap years for a Feb-29 date`() { + assertThat(nextYearlyOccurrence(2, 29, LocalDate.of(2026, 6, 1))) + .isEqualTo(LocalDate.of(2028, 2, 29)) + } + + @Test + fun `sampling the offset at the next occurrence, not a 1972 anchor, fixes the fire hour`() { + // A year-less birthday's DTSTART anchor is 1972 (Berlin had no DST then, + // UTC+1). Sampling the offset there skews a modern CEST occurrence by an + // hour; sampling at the upcoming occurrence fires at the intended time. + val anchor1972 = LocalDate.of(1972, 7, 15) + val nextOccurrence = nextYearlyOccurrence(7, 15, LocalDate.of(2026, 6, 1)) // 2026-07-15, CEST + val rawFromAnchor = toProviderAllDayMinutes(0, anchor1972, berlin, nineAm) + val rawFromNext = toProviderAllDayMinutes(0, nextOccurrence, berlin, nineAm) + + val fireFromAnchor = actualFire(rawFromAnchor, nextOccurrence) + .let(java.time.Instant::ofEpochMilli).atZone(berlin).toLocalTime() + val fireFromNext = actualFire(rawFromNext, nextOccurrence) + .let(java.time.Instant::ofEpochMilli).atZone(berlin).toLocalTime() + assertThat(fireFromAnchor).isEqualTo(LocalTime.of(10, 0)) // skewed +1h + assertThat(fireFromNext).isEqualTo(LocalTime.of(9, 0)) // correct + } + @Test fun `a winter-anchored offset drifts one hour on a summer occurrence`() { // Known limitation: one fixed MINUTES per series can't track DST. An -- 2.49.1 From 7fe59b36c310290f039a5e34aa0f40b65270b2cf Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:04:06 +0200 Subject: [PATCH 41/49] fix(contacts): keep distinct custom dates instead of collapsing them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mirror keyed each event on (contact, type) only, and the read deduped on that same pair from an unordered query. So a contact with two custom dates ('Wedding', 'Graduation') mirrored just one — and, the winner being order-dependent, the single event could ping-pong between the two dates across syncs. A Custom date's UID now carries a discriminator (its label, else its month-day), the read dedups on the full UID, and the query is ordered by Data._ID so a genuine duplicate resolves the same way every time. Co-Authored-By: Claude Fable 5 --- .../contacts/ContactSpecialDatesDataSource.kt | 16 +++++--- .../domain/contacts/SpecialDateFormatting.kt | 38 ++++++++++++++++--- .../contacts/SpecialDateFormattingTest.kt | 31 +++++++++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt index 13449ed..f4edca0 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/ContactSpecialDatesDataSource.kt @@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType +import de.jeanlucmakiola.calendula.domain.contacts.managedUid import de.jeanlucmakiola.calendula.domain.contacts.parseContactEventDate import javax.inject.Inject import javax.inject.Singleton @@ -50,9 +51,12 @@ class AndroidContactSpecialDatesDataSource @Inject constructor( override fun readSpecialDates(): List { if (!hasPermission()) return emptyList() val resolver = context.contentResolver - // A contact can carry the same date more than once (multiple raw - // contacts under one aggregate); keep the first per (contact, type). - val seen = HashSet>() + // A contact can carry the same date more than once (multiple raw contacts + // under one aggregate); dedup on the mirror's reconciliation key so exact + // duplicates collapse while genuinely distinct dates (two custom events on + // one contact) are all kept. Ordered by Data._ID so which of two truly + // conflicting rows wins is stable across syncs (no event ping-pong). + val seen = HashSet() val result = ArrayList() runCatching { resolver.query( @@ -60,7 +64,7 @@ class AndroidContactSpecialDatesDataSource @Inject constructor( PROJECTION, "${ContactsContract.Data.MIMETYPE} = ?", arrayOf(Event.CONTENT_ITEM_TYPE), - null, + "${ContactsContract.Data._ID} ASC", )?.use { c -> val idxDate = c.getColumnIndexOrThrow(Event.START_DATE) val idxType = c.getColumnIndexOrThrow(Event.TYPE) @@ -71,8 +75,7 @@ class AndroidContactSpecialDatesDataSource @Inject constructor( val lookup = c.getString(idxLookup)?.takeIf { it.isNotEmpty() } ?: continue val parts = parseContactEventDate(c.getString(idxDate)) ?: continue val type = specialDateTypeForRawEventType(c.getInt(idxType)) - if (!seen.add(lookup to type)) continue - result += ContactSpecialDate( + val date = ContactSpecialDate( lookupKey = lookup, displayName = c.getString(idxName)?.trim().orEmpty(), type = type, @@ -81,6 +84,7 @@ class AndroidContactSpecialDatesDataSource @Inject constructor( year = parts.year, label = c.getString(idxLabel)?.takeIf { it.isNotBlank() }, ) + if (seen.add(date.managedUid())) result += date } } }.onFailure { Log.w(TAG, "Reading contact special-dates failed", it) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt b/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt index f356abb..393ccf8 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormatting.kt @@ -3,12 +3,40 @@ package de.jeanlucmakiola.calendula.domain.contacts import kotlinx.datetime.LocalDate /** - * The deterministic `Events.UID_2445` that ties a mirrored event to its source - * contact date. Stable across syncs (the reconciliation key), and namespaced by - * type so the same contact's birthday and anniversary never collide. + * Prefix of every managed-event `UID_2445`. Distinguishes mirror events from + * user-created ones (which carry a random `@calendula` UID), so the sync + * only ever reconciles — and never deletes — events it actually owns. */ -fun managedEventUid(type: SpecialDateType, lookupKey: String): String = - "contact-${type.name.lowercase()}:$lookupKey@calendula" +const val MANAGED_UID_PREFIX = "contact-" + +/** + * The deterministic `Events.UID_2445` that ties a mirrored event to its source + * contact date. Stable across syncs (the reconciliation key), namespaced by + * type so the same contact's birthday and anniversary never collide, and — when + * a [discriminator] is given — by it too, so two Custom dates on one contact + * (e.g. "Wedding" and "Graduation") get distinct events instead of clobbering + * each other. + */ +fun managedEventUid(type: SpecialDateType, lookupKey: String, discriminator: String? = null): String { + val disc = discriminator?.takeIf { it.isNotBlank() }?.let { ":$it" }.orEmpty() + return "$MANAGED_UID_PREFIX${type.name.lowercase()}:$lookupKey$disc@calendula" +} + +/** + * The reconciliation key for this date's mirrored event. Birthdays/anniversaries + * are one-per-contact, so they key on the contact alone; a [SpecialDateType.Custom] + * date adds a discriminator (its label, else its month-day) so distinct custom + * dates on one contact don't collapse into a single event. + */ +fun ContactSpecialDate.managedUid(): String = + managedEventUid(type, lookupKey, customDiscriminator()) + +private fun ContactSpecialDate.customDiscriminator(): String? = + if (type == SpecialDateType.Custom) { + label?.trim()?.lowercase()?.ifBlank { null } ?: "%02d-%02d".format(month, day) + } else { + null + } /** * The all-day date the recurring `FREQ=YEARLY` series is anchored at: the real diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt index 9961f7a..47bcb83 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/domain/contacts/SpecialDateFormattingTest.kt @@ -23,6 +23,37 @@ class SpecialDateFormattingTest { .isEqualTo("contact-anniversary:abc@calendula") } + private fun custom(label: String?, month: Int, day: Int) = ContactSpecialDate( + lookupKey = "k", + displayName = "Jane", + type = SpecialDateType.Custom, + month = month, + day = day, + year = null, + label = label, + ) + + @Test + fun `birthday and anniversary uids ignore the discriminator`() { + assertThat(birthday(5, 14, 1990).managedUid()).isEqualTo("contact-birthday:k@calendula") + } + + @Test + fun `two custom dates on one contact get distinct uids by label`() { + assertThat(custom("Wedding", 6, 10).managedUid()) + .isEqualTo("contact-custom:k:wedding@calendula") + assertThat(custom("Graduation", 9, 1).managedUid()) + .isNotEqualTo(custom("Wedding", 6, 10).managedUid()) + } + + @Test + fun `label-less custom dates fall back to month-day so distinct dates survive`() { + assertThat(custom(null, 6, 10).managedUid()) + .isEqualTo("contact-custom:k:06-10@calendula") + assertThat(custom(null, 9, 1).managedUid()) + .isNotEqualTo(custom(null, 6, 10).managedUid()) + } + @Test fun `anchor date uses the known year`() { assertThat(birthday(5, 14, 1990).anchorDate()).isEqualTo(LocalDate(1990, 5, 14)) -- 2.49.1 From 84e6402031fdcfa2edf1125df1ea96d5df10806a Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:04:26 +0200 Subject: [PATCH 42/49] 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 --- .../data/contacts/SpecialDatesScheduler.kt | 19 +++++++++++++++--- .../data/contacts/SpecialDatesSyncEngine.kt | 20 ++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt index a906f9b..ba5cff5 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesScheduler.kt @@ -30,6 +30,7 @@ object SpecialDatesScheduler { private const val WORK_NAME = "special-dates-sync" private const val WORK_NAME_NOW = "special-dates-sync-now" + private const val WORK_NAME_FOREGROUND = "special-dates-sync-foreground" private const val KEY_FOREGROUND = "foreground" /** Enqueue (or cancel) the daily periodic reconcile to match [enabled]. */ @@ -38,6 +39,7 @@ object SpecialDatesScheduler { if (!enabled) { workManager.cancelUniqueWork(WORK_NAME) workManager.cancelUniqueWork(WORK_NAME_NOW) + workManager.cancelUniqueWork(WORK_NAME_FOREGROUND) return } val request = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) @@ -48,15 +50,19 @@ object SpecialDatesScheduler { } /** - * Run one reconcile immediately. [foreground] runs are debounced (the app can - * resume often); enable/"Sync now" runs ([foreground] false) always sync. + * Run one reconcile immediately. [foreground] runs (the app resuming) are + * debounced inside the worker and use their own work name, so a frequent + * foreground resync can never REPLACE — and swallow — a pending enable / + * "Sync now" run, which always syncs. The engine serializes the two if they + * overlap. */ fun runNow(context: Context, foreground: Boolean = false) { val request = OneTimeWorkRequestBuilder() .setInputData(Data.Builder().putBoolean(KEY_FOREGROUND, foreground).build()) .build() + val name = if (foreground) WORK_NAME_FOREGROUND else WORK_NAME_NOW WorkManager.getInstance(context) - .enqueueUniqueWork(WORK_NAME_NOW, ExistingWorkPolicy.REPLACE, request) + .enqueueUniqueWork(name, ExistingWorkPolicy.REPLACE, request) } internal const val INPUT_FOREGROUND = KEY_FOREGROUND @@ -107,6 +113,13 @@ class SpecialDatesSyncWorker( } if (foreground) prefs.setSpecialDatesLastForegroundSync(now) Result.success() + } catch (e: SecurityException) { + // A revoked calendar/contacts permission won't fix itself on retry — + // park the feature in a stalled state (surfaced in settings) instead + // of retrying with backoff forever. + Log.w(TAG, "Special-dates sync lacks a required permission", e) + prefs.recordSpecialDatesRun(now, SpecialDatesStalledReason.PermissionRevoked) + Result.success() } catch (e: Exception) { Log.w(TAG, "Special-dates sync failed", e) Result.retry() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt index 082d3bc..74ba06a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/contacts/SpecialDatesSyncEngine.kt @@ -13,10 +13,12 @@ import de.jeanlucmakiola.calendula.domain.SimpleRecurrence import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType import de.jeanlucmakiola.calendula.domain.contacts.anchorDate -import de.jeanlucmakiola.calendula.domain.contacts.managedEventUid +import de.jeanlucmakiola.calendula.domain.contacts.managedUid import de.jeanlucmakiola.calendula.domain.contacts.renderSpecialDateTitle import de.jeanlucmakiola.calendula.domain.toRRule import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime import java.time.ZoneId @@ -64,11 +66,19 @@ class SpecialDatesSyncEngine @Inject constructor( private val spec: SpecialDatesCalendarSpec, ) { + // Serializes calendar-lifecycle work: two overlapping syncs (the daily job + // racing a "Sync now"/foreground run) would each see no managed calendar and + // both create one; a teardown racing an in-flight sync would delete calendars + // the sync then recreates. Holding this for the whole of sync()/teardown() + // makes those check-then-act sequences atomic, and — because sync() re-reads + // the enabled flag inside the lock — a teardown always wins the race. + private val lifecycleMutex = Mutex() + /** * Reconcile every enabled type against the device's contacts. Returns why it * stopped early (disabled / permission gone) or [SpecialDatesSyncResult.Success]. */ - suspend fun sync(): SpecialDatesSyncResult { + suspend fun sync(): SpecialDatesSyncResult = lifecycleMutex.withLock { if (!prefs.specialDatesEnabled.first()) return SpecialDatesSyncResult.Disabled if (!contacts.hasPermission()) return SpecialDatesSyncResult.PermissionMissing @@ -91,7 +101,7 @@ class SpecialDatesSyncEngine @Inject constructor( reminderCtx = reminderCtx, ) } - return SpecialDatesSyncResult.Success + SpecialDatesSyncResult.Success } /** @@ -117,7 +127,7 @@ class SpecialDatesSyncEngine @Inject constructor( } /** Delete every managed calendar and forget its id — used when the feature is turned off. */ - suspend fun teardown() { + suspend fun teardown() = lifecycleMutex.withLock { calendars.findManagedCalendars().forEach { calendars.deleteCalendar(it.id) } SpecialDateType.entries.forEach { prefs.setSpecialDatesCalendarId(it, null) } } @@ -211,7 +221,7 @@ class SpecialDatesSyncEngine @Inject constructor( val dtStartMillis = form.toWriteTimes(ZoneId.systemDefault()).dtStartMillis return BuiltManagedEvent( desired = ManagedEventDesired( - uid = managedEventUid(type, sd.lookupKey), + uid = sd.managedUid(), title = title, dtStartMillis = dtStartMillis, rrule = YEARLY_RRULE, -- 2.49.1 From 0718afd5a62637c51b4f75d73d629fe6fe60fa00 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Thu, 2 Jul 2026 23:13:27 +0200 Subject: [PATCH 43/49] fix(edit): make the new-event calendar picker a scrollable grouped list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../calendula/ui/edit/EventEditScreen.kt | 82 +++++++++++++------ 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index 166c0fd..b23d04b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.automirrored.filled.Notes import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.CalendarMonth +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Contacts import androidx.compose.material.icons.filled.EventAvailable @@ -120,12 +121,15 @@ import de.jeanlucmakiola.calendula.ui.common.calendarCollapseExit import de.jeanlucmakiola.calendula.ui.common.calendarExpandEnter import de.jeanlucmakiola.calendula.ui.common.predictiveBack import de.jeanlucmakiola.calendula.ui.common.CALENDAR_COLOR_PALETTE +import de.jeanlucmakiola.calendula.ui.common.CalendarColorChip import de.jeanlucmakiola.calendula.ui.common.CalendarDatePickerDialog import de.jeanlucmakiola.calendula.ui.common.ColorSwatchRow import de.jeanlucmakiola.calendula.ui.common.DialogAmountField import de.jeanlucmakiola.calendula.ui.common.DialogUnitDropdown import de.jeanlucmakiola.calendula.ui.common.eventFormFieldIcon import de.jeanlucmakiola.calendula.ui.common.eventFormFieldLabel +import de.jeanlucmakiola.calendula.ui.common.FullScreenPicker +import de.jeanlucmakiola.calendula.ui.common.GroupedRow import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY import de.jeanlucmakiola.calendula.ui.common.InlineTextField import de.jeanlucmakiola.calendula.ui.common.OptionCard @@ -943,7 +947,7 @@ private fun EventEditContent( } if (showCalendarPicker) { - CalendarPickerDialog( + CalendarPicker( calendars = state.calendars, selectedId = form.calendarId, onSelect = { @@ -1931,34 +1935,64 @@ private fun ScheduleRow( } } +/** + * Full-screen calendar picker: a scrollable list of every writable calendar, + * grouped under its owning account like the visibility filter and calendar + * manager. Replaces the former fixed-height [AlertDialog], which clipped the + * list past ~9 rows so accounts with many calendars had unreachable entries + * (#29). Selecting a row calls [onSelect]; the caller closes the picker. + */ @Composable -private fun CalendarPickerDialog( +private fun CalendarPicker( calendars: List, selectedId: Long?, onSelect: (Long) -> Unit, onDismiss: () -> Unit, ) { - val dark = isSystemInDarkTheme() - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_detail_calendar)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - calendars.forEach { calendar -> - OptionCard( - label = calendar.displayName, - onClick = { onSelect(calendar.id) }, - icon = Icons.Default.CalendarMonth, - // The calendar's own colour carries its identity. - iconTint = pastelize(calendar.color, dark), - supportingText = calendar.accountName.takeIf { it.isNotBlank() }, - selected = calendar.id == selectedId, - ) - } + // Group by owning account (name, else type, else the calendar's own name), + // preserving the provider's order within and across groups — the same + // grouping [groupByAccount] applies for the drawer filter. + val groups = remember(calendars) { + calendars + .groupBy { + it.accountName.takeIf(String::isNotBlank) + ?: it.accountType.takeIf(String::isNotBlank) + ?: it.displayName } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - ) + .toList() + } + FullScreenPicker( + title = stringResource(R.string.event_detail_calendar), + onDismiss = onDismiss, + ) { + groups.forEach { (account, cals) -> + Text( + text = account, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 4.dp), + ) + cals.forEachIndexed { index, calendar -> + val isSelected = calendar.id == selectedId + GroupedRow( + title = calendar.displayName, + position = positionOf(index, cals.size), + selected = isSelected, + leading = { CalendarColorChip(calendar.color) }, + trailing = if (isSelected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + } + } else { + null + }, + onClick = { onSelect(calendar.id) }, + ) + } + } + } } -- 2.49.1 From 0a89ff9d0e8ccde91a9ac2a7376087e8296b0d07 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 15:22:18 +0200 Subject: [PATCH 44/49] fix(edit): pin the picker title instead of a large collapsing header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../calendula/ui/common/GroupedList.kt | 63 +++++++++++++------ .../calendula/ui/common/Picker.kt | 12 +++- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt index c183aab..de2a542 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/GroupedList.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -57,10 +58,16 @@ fun positionOf(index: Int, count: Int): Position = when { } /** - * The app's standard full-screen list scaffold: a collapsing [LargeTopAppBar] - * whose title shrinks into the bar (next to the back button) as the content - * scrolls. Content is a scrollable column that feeds the toolbar via nested - * scroll. Used by Settings and the calendar manager so they share one shell. + * The app's standard full-screen list scaffold. By default a collapsing + * [LargeTopAppBar] whose title shrinks into the bar (next to the back button) as + * the content scrolls — used by Settings and the calendar manager, where the + * large header sets the page. Content is a scrollable column that feeds the + * toolbar via nested scroll. + * + * Set [largeTopBar] to false for a pinned, single-line [TopAppBar] instead: the + * title sits in the bar from the start, with no expanded header to scroll past. + * Preferred for selection pickers, where the tall header is just wasted space + * above the options. */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,12 +75,16 @@ fun CollapsingScaffold( title: String, onBack: () -> Unit, modifier: Modifier = Modifier, + largeTopBar: Boolean = true, snackbarHost: @Composable () -> Unit = {}, actions: @Composable RowScope.() -> Unit = {}, content: @Composable ColumnScope.() -> Unit, ) { - val scrollBehavior = + val scrollBehavior = if (largeTopBar) { TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) + } else { + TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + } Scaffold( modifier = modifier .predictiveBack(onBack = onBack) @@ -81,22 +92,34 @@ fun CollapsingScaffold( .background(MaterialTheme.colorScheme.surface) .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - LargeTopAppBar( - title = { Text(title) }, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.settings_back), - ) - } - }, - actions = actions, - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - scrolledContainerColor = MaterialTheme.colorScheme.surface, - ), + val navigationIcon = @Composable { + IconButton(onClick = onBack) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.settings_back), + ) + } + } + val colors = TopAppBarDefaults.topAppBarColors( + scrolledContainerColor = MaterialTheme.colorScheme.surface, ) + if (largeTopBar) { + LargeTopAppBar( + title = { Text(title) }, + navigationIcon = navigationIcon, + actions = actions, + scrollBehavior = scrollBehavior, + colors = colors, + ) + } else { + TopAppBar( + title = { Text(title) }, + navigationIcon = navigationIcon, + actions = actions, + scrollBehavior = scrollBehavior, + colors = colors, + ) + } }, snackbarHost = snackbarHost, ) { innerPadding -> diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index ce6abae..66b24df 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -43,8 +43,9 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange /** * Shared full-screen scaffold for selection pickers: a full-bleed [Dialog] that - * reuses the app's [CollapsingScaffold] (collapsing title + back button), so a - * picker is visually identical to a Settings sub-page and uses the full width. + * reuses the app's [CollapsingScaffold] (back button + full width), but with a + * pinned single-line title rather than the large collapsing header — a picker is + * a short list, so the tall header would only be empty space to scroll past. * [content] places the connected grouped rows; selecting one calls [onDismiss]. */ @Composable @@ -69,7 +70,12 @@ fun FullScreenPicker( (view.parent as? DialogWindowProvider)?.window ?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) } - CollapsingScaffold(title = title, onBack = onDismiss, content = content) + CollapsingScaffold( + title = title, + onBack = onDismiss, + largeTopBar = false, + content = content, + ) } } -- 2.49.1 From f6d40de83210ce1d7b9a20e4d19b91158f9ae971 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 16:05:22 +0200 Subject: [PATCH 45/49] =?UTF-8?q?chore(i18n):=20strip=20hand-edited=20Germ?= =?UTF-8?q?an=20strings=20=E2=80=94=20German=20returns=20via=20Weblate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/src/main/res/values-de/strings.xml | 36 +------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1d05129..aa65ccd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -12,7 +12,7 @@ Kalender konnte nicht gelesen werden. Alle Termine, schön im Blick - Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App zu Beginn nicht — und nichts verlässt je dein Gerät. + Calendula braucht Zugriff auf deinen Kalender, um deine Termine zu zeigen und zu verwalten. Mehr verlangt die App nie. Kalender-Zugriff erlauben Kalender-Zugriff abgelehnt Ohne Kalender-Zugriff kann Calendula keine Termine anzeigen. Du kannst den Zugriff in den System-Einstellungen wieder erlauben. @@ -387,38 +387,4 @@ Der Issue-Tracker konnte nicht geöffnet werden. Der Bericht ist in deiner Zwischenablage. 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 _(Der Bericht war zu lang für diesen Link — füge ihn aus deiner Zwischenablage hier ein.)_ - - - Geburtstage - Jahrestage - Besondere Tage - {name}s Geburtstag ({year}) - {name}s Jahrestag ({year}) - {name} - Verwaltet von „%1$s“ — Titel, Datum und Wiederholung werden aus deinen Kontakten synchron gehalten. Erinnerungen, Ort und Notizen kannst du selbst bearbeiten. - Geburtstage & Jahrestage aus Kontakten - Besondere Kontakttage - Kontakttage anzeigen - Spiegelt Geburtstage und weitere Daten deiner Kontakte in lokale Kalender. Liest Kontakte nur auf diesem Gerät — nichts wird hochgeladen, und deine Kontakte werden nie verändert. - Geburtstage - Jahrestage - Weitere Tage - Titelformat - Verwende {name} für den Kontakt und {year} für das Jahr (Geburtsjahr bzw. Startjahr eines Jahrestags; ausgeblendet, wenn unbekannt). - Erinnerungen - Jahr anzeigen - {year} in Titeln anzeigen, wenn es bekannt ist - Jetzt synchronisieren - Noch nicht synchronisiert - Zuletzt synchronisiert %1$s - Farbe und Sichtbarkeit jedes Kalenders legst du in den Kalender-Einstellungen fest. - In Besondere Kontakttage festgelegt - Pausiert - Calendula kann deine Kontakte nicht mehr lesen, daher werden diese Kalender nicht aktualisiert. - Zugriff erlauben - Kontakttage deaktivieren? - Dadurch werden die Kontakttage-Kalender und ihre Termine gelöscht. Hinzugefügte Erinnerungen oder Notizen gehen verloren. - Dadurch werden der Kalender „%1$s“ und seine Termine gelöscht. Hinzugefügte Erinnerungen oder Notizen gehen verloren. - Deaktivieren - Speichern -- 2.49.1 From be13f97802c3c2767c094d140b6dd6caa624a777 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 16:05:22 +0200 Subject: [PATCH 46/49] 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 --- app/src/main/res/xml/locales_config.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index f68afab..8171e19 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -10,4 +10,6 @@ + + -- 2.49.1 From a2b1e7792b4edc22e8771a98e373a1646b8ca33b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 16:10:31 +0200 Subject: [PATCH 47/49] 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 --- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9afe143..c1d649f 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -303,7 +303,6 @@ Ninguno Usar recordatorio por defecto Cantidad - Personalizado (%1$s) Guardar Recordatorios por calendario Sobreescribe el por recordatorio defecto por calendario — De forma separada para eventos con duración fija o que duran todo el dia. Un calendario puede mantener el por defecto o tener su propia configuracion. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 406960a..e05bf1b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -285,7 +285,6 @@ Nessuno Usa il promemoria standard Valore - Personalizza (%1$s) Imposta Promemoria per calendario Sovrascrivi i reminder standard con reminder specifici per calendario, separati per eventi giornalieri ed eventi ad ore. Ogni calendario può usare i promemoria di default, ignorarli o avere i suoi personalizzati. -- 2.49.1 From f168021f51f48ff91eef2ca14ee04b77798d4a8e Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 16:25:39 +0200 Subject: [PATCH 48/49] release: bump version to 2.13.0 (21300) Co-Authored-By: Claude Fable 5 --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72977e7..6ff2520 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,8 +28,8 @@ android { // which builds this version and then creates the matching vX.Y.Z tag + // release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 + // PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md. - versionCode = 21200 - versionName = "2.12.0" + versionCode = 21300 + versionName = "2.13.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } -- 2.49.1 From 79ad70e0c017f254d77eba49eb2cd39a8a0aaaae Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Jul 2026 16:25:39 +0200 Subject: [PATCH 49/49] release: cut v2.13.0 changelog and fastlane notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 32 ++++++++- .../android/en-US/changelogs/21300.txt | 70 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/21300.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index daa0184..eff4ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,19 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [2.13.0] — 2026-07-03 ### Added +- Calendula now speaks Spanish and Italian. Both arrived as community + translations through [Calendula's Weblate](https://weblate.dev.jeanlucmakiola.de/projects/calendula/) + — a **huge thank you** to + [kikerw](https://weblate.dev.jeanlucmakiola.de/user/kikerw/) for the Spanish + translation and to + [corrent](https://weblate.dev.jeanlucmakiola.de/user/corrent/) for the Italian + one! Pick your language under Settings → Language or in Android's per-app + language settings. Strings added in this very release may still show in + English until the translations catch up. Want Calendula in your language? + Translating happens entirely in the browser — every contribution is welcome. - See your contacts' birthdays and anniversaries in your calendar. A new, **optional** feature (Settings → Contact special dates) mirrors your contacts' birthdays, anniversaries and other dates into local "Birthdays", @@ -33,6 +43,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 date number — now opens that day, and the "today" button snaps the grid back to the current month in place. Thanks to @rgz46vic and @ptab for the suggestions ([#18], [#20]). +- Make Calendula's text your own. Settings → Appearance gains **Headings font** + and **Body font** pickers: keep the system default, choose a bundled face + (Atkinson Hyperlegible, Lora, JetBrains Mono — each previewed in its own + face), or load any `.ttf`/`.otf` file from your device, independently per + role. Font size and colour stay with Android's accessibility scaling and the + app theme, as discussed on the issue. Thanks to @abrossimow for the + suggestion ([#19]). +- Choose what the view-switch button cycles through. Settings → Appearance now + lets you pick which views the top-right quick-switch button rotates between + and drag them into your preferred order; views you switch off stay reachable + from the navigation menu, which can be reordered the same way. Thanks to + @abrossimow for the suggestion ([#24]). ### Fixed - Month widget arrows and "today" button work again. On release builds the @@ -49,6 +71,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 nothing applied. The edited occurrence is now stored correctly, so the change lands on just that one event and leaves the rest of the series untouched ([#16]). +- Every calendar can be picked when creating an event. The event editor's + calendar picker was a fixed-height dialog, so with many calendars anything + past the first nine or so was simply unreachable. It is now a full-screen, + scrollable list grouped by account, with each calendar's colour shown. + Thanks to @dschuermann for the report ([#29]). ## [2.12.0] — 2026-06-28 @@ -779,4 +806,7 @@ automatically, with zero telemetry and no internet permission. [#16]: https://codeberg.org/jlmakiola/calendula/issues/16 [#17]: https://codeberg.org/jlmakiola/calendula/issues/17 [#18]: https://codeberg.org/jlmakiola/calendula/issues/18 +[#19]: https://codeberg.org/jlmakiola/calendula/issues/19 [#20]: https://codeberg.org/jlmakiola/calendula/issues/20 +[#24]: https://codeberg.org/jlmakiola/calendula/issues/24 +[#29]: https://codeberg.org/jlmakiola/calendula/issues/29 diff --git a/fastlane/metadata/android/en-US/changelogs/21300.txt b/fastlane/metadata/android/en-US/changelogs/21300.txt new file mode 100644 index 0000000..0d7e7ac --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/21300.txt @@ -0,0 +1,70 @@ +### Added +- Calendula now speaks Spanish and Italian. Both arrived as community + translations through [Calendula's Weblate](https://weblate.dev.jeanlucmakiola.de/projects/calendula/) + — a **huge thank you** to + [kikerw](https://weblate.dev.jeanlucmakiola.de/user/kikerw/) for the Spanish + translation and to + [corrent](https://weblate.dev.jeanlucmakiola.de/user/corrent/) for the Italian + one! Pick your language under Settings → Language or in Android's per-app + language settings. Strings added in this very release may still show in + English until the translations catch up. Want Calendula in your language? + Translating happens entirely in the browser — every contribution is welcome. +- See your contacts' birthdays and anniversaries in your calendar. A new, + **optional** feature (Settings → Contact special dates) mirrors your contacts' + birthdays, anniversaries and other dates into local "Birthdays", + "Anniversaries" and "Other dates" calendars that stay in sync as your contacts + change. Each is a normal local calendar, so you set its colour, visibility and + reminders the usual way — new birthdays even start with a reminder a week before + *and* on the day. The title format is yours to customise (`{name}`, `{year}`). + This is the first feature to use the contacts permission: it is requested only + when you turn the feature on, everything stays on your device (Calendula has no + internet access), and your contacts are only ever read, never changed. Thanks + to @moonj for the suggestion ([#15]). +- Set more than one default reminder per calendar. A calendar's default + reminders — and the global defaults under Settings → Notifications — can now + hold several lead times instead of just one, so new events can start with, say, + a reminder a week before *and* one on the day. The reminder pickers are now + multi-select; per-calendar overrides can still inherit the global default or + turn reminders off entirely. Thanks to @moonj for the suggestion ([#14]). +- Widget headers now open the app. Tapping the month/year title on the month + widget opens the app on the month view, and tapping the "Upcoming" title on + the agenda widget opens it on your default view — so there's a one-tap way + back into the app that lands where you'd expect, instead of only through a day + or event. On the month widget, tapping anywhere on a day — not just the small + date number — now opens that day, and the "today" button snaps the grid back + to the current month in place. Thanks to @rgz46vic and @ptab for the + suggestions ([#18], [#20]). +- Make Calendula's text your own. Settings → Appearance gains **Headings font** + and **Body font** pickers: keep the system default, choose a bundled face + (Atkinson Hyperlegible, Lora, JetBrains Mono — each previewed in its own + face), or load any `.ttf`/`.otf` file from your device, independently per + role. Font size and colour stay with Android's accessibility scaling and the + app theme, as discussed on the issue. Thanks to @abrossimow for the + suggestion ([#19]). +- Choose what the view-switch button cycles through. Settings → Appearance now + lets you pick which views the top-right quick-switch button rotates between + and drag them into your preferred order; views you switch off stay reachable + from the navigation menu, which can be reordered the same way. Thanks to + @abrossimow for the suggestion ([#24]). + +### Fixed +- Month widget arrows and "today" button work again. On release builds the + prev/next-month arrows and the jump-to-today control on the month widget did + nothing when tapped — code shrinking had stripped the tap handlers behind + them. They respond again. Thanks to @rgz46vic for the report ([#18]). +- Disabled calendars no longer notify. Reminders for events in a calendar you + have disabled (Settings → Calendars) are now suppressed instead of still + popping up — matching how a disabled calendar's events already stay hidden + everywhere else in the app ([#17]). +- Editing a single occurrence of a recurring event works again. Choosing **Only + this event** and saving a change to one event in a repeating series silently + did nothing — the change was rejected and the edit form simply reappeared with + nothing applied. The edited occurrence is now stored correctly, so the change + lands on just that one event and leaves the rest of the series untouched + ([#16]). +- Every calendar can be picked when creating an event. The event editor's + calendar picker was a fixed-height dialog, so with many calendars anything + past the first nine or so was simply unreachable. It is now a full-screen, + scrollable list grouped by account, with each calendar's colour shown. + Thanks to @dschuermann for the report ([#29]). + -- 2.49.1