diff --git a/.github/workflows/flutter-linux.yml b/.github/workflows/flutter-linux.yml
index c3ba9c8c..73740ede 100644
--- a/.github/workflows/flutter-linux.yml
+++ b/.github/workflows/flutter-linux.yml
@@ -31,7 +31,8 @@ jobs:
libstdc++-12-dev \
libsecret-1-dev \
libjsoncpp-dev \
- liblzma-dev
+ liblzma-dev \
+ xvfb
- name: Set up Flutter
uses: subosito/flutter-action@v2
@@ -86,6 +87,19 @@ jobs:
- name: Test
run: '"$BUSYMAX_FLUTTER_EXECUTABLE" test'
+ - name: Test native clock patterns and optional GTK time picker
+ run: |
+ mkdir -p build/linux/native-tests
+ g++ -std=c++17 -Wall -Wextra -Werror \
+ windows/runner/tests/short_time_pattern_test.cpp \
+ -o build/linux/native-tests/short_time_pattern_test
+ build/linux/native-tests/short_time_pattern_test
+ g++ -std=c++17 -Wall -Wextra -Werror \
+ linux/runner/tests/time_picker_test.cc \
+ -o build/linux/native-tests/time_picker_test \
+ $(pkg-config --cflags --libs gtk+-3.0)
+ xvfb-run -a build/linux/native-tests/time_picker_test
+
- name: Validate release provider configuration
if: github.event_name == 'push'
env:
diff --git a/README.md b/README.md
index c05535db..2bc727cd 100644
--- a/README.md
+++ b/README.md
@@ -1,207 +1,117 @@
# BusyMax
-BusyMax is a Linux and Windows desktop calendar and task manager built with
-Flutter.
+BusyMax is a beta calendar and task manager for Linux and Windows. It brings
+events, tasks, reminders, and day, week, month, year, and agenda views into one
+desktop application.
-It brings calendar events and tasks into a native-feeling desktop interface:
-Yaru with the existing GTK integrations on Linux, and Fluent UI with the native
-title bar on Windows 11. BusyMax connects directly to Google, Microsoft, Apple
-iCloud Calendar, and Nextcloud. Apple Reminders is not supported.
+Linux uses a Yaru/GTK desktop composition with XDG integrations. Windows uses
+a separate Fluent composition with a native Windows title bar, tray,
+notifications, and packaged startup integration. Provider capabilities differ;
+see the [provider support matrix](docs/provider_support_matrix.md) before
+relying on a particular task field or operation.
-[](https://snapcraft.io/busymax)
-
-[](https://snapcraft.io/busymax)
+[](https://snapcraft.io/busymax)
-
+
-
- Month view with calendars, tasks, and event details.
-
+
Month view in the Linux desktop application.
## Highlights
-- First-class Linux Yaru and Windows 11 Fluent desktop applications.
-- Calendar views for day, week, month, year, and agenda planning.
-- Task creation with lists, start/due dates, reminders, repeat options,
- subtasks, status, progress, priority, categories, location, and URL.
-- Nextcloud task-list creation/rename/delete, recursive task duplicate/delete,
- raw iCalendar export, clear-completed, and cross-list subtree moves.
-- Event editing with calendar selection, time controls, repeat rules, and
- reminders.
-- Ordinary event and supported-task location fields, with a saved destination
- opened on demand in an external maps application or browser.
-- Tray shortcut for opening the main Agenda view.
-- Direct integrations with Google Calendar, Google Tasks, Microsoft Calendar,
- Microsoft To Do, Apple iCloud Calendar, Nextcloud Calendar, and Nextcloud
- Tasks.
-- Offline-first local cache, conditional DAV writes, recurrence exceptions,
- alarms, and explicit conflict handling for CalDAV accounts.
-
-## Screenshots
+- Calendar and task planning in day, week, month, year, and agenda views.
+- Direct connections to Google, Microsoft, Apple iCloud Calendar, and
+ Nextcloud, plus read-only WebCal subscriptions.
+- Event and task editing that follows each provider's supported fields and the
+ permissions granted for a collection.
+- Local caching for offline viewing. Supported changes can be queued for some
+ providers; offline behavior is not identical across every integration.
+- Desktop reminders, tray actions, import/export, themes, keyboard navigation,
+ and localized interfaces.
+
+
+More Linux screenshots
-
-
-
- Week view with color-coded calendars and scheduled tasks.
-
-
-
-
- Day view for focused daily planning.
-
+
Week view
+
Day view
+
+
+
Agenda view
+
Year view
+
+
+
Task editor
+
Event editor
-
-
-
- Agenda view with upcoming events, tasks, and details.
-
-
-
-
- Task creation with lists, due dates, reminders, and repeat options.
-
+
Account providers
-
-More screenshots
+
-
+## Installation and accounts
-
-
-
+The public packaged release is the Linux beta in the
+[Snap Store](https://snapcraft.io/busymax). The repository does not document a
+public production Windows download or Store listing. Windows maintainers can
+build and install a test-signed package by following the
+[Windows packaging guide](docs/windows_packaging.md); unsigned CI packages are
+test artifacts, not ordinary user downloads.
-
-
-
+Packaged users do not need Flutter, Visual Studio, Linux development libraries,
+or their own OAuth registration. In a configured build:
-
-
-
+- Google and Microsoft accounts connect through browser-based sign-in.
+- Apple iCloud Calendar uses an Apple app-specific password; Apple Reminders is
+ not supported. See [Connect Apple iCloud Calendar](docs/apple_icloud_setup.md).
+- Nextcloud authorizes BusyMax in the default browser and requires a compatible
+ HTTPS server. See [Connect Nextcloud Calendar and Tasks](docs/nextcloud_setup.md).
+- WebCal adds a read-only calendar subscription from a confirmed URL.
-
+Developers who need to configure their own desktop OAuth applications should
+use the [Google OAuth registration guide](docs/google_setup.md) or
+[Microsoft OAuth registration guide](docs/microsoft_setup.md). Provider
+differences and limitations are summarized in the
+[provider support matrix](docs/provider_support_matrix.md).
-## Prerequisites
-
-- [Flutter SDK](https://docs.flutter.dev/install)
-- Flutter is pinned to 3.47.2 for both platforms (bundled Dart 3.13.2).
-- GTK 3 and libhandy development packages (`libgtk-3-dev` and
- `libhandy-1-dev` on Ubuntu/Debian)
-- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`; see
- [Google setup](docs/google_setup.md)
-- `MICROSOFT_OAUTH_CLIENT_ID`, see [Microsoft Setup](docs/microsoft_setup.md)
-
-Apple and Nextcloud do not require compile-time client credentials:
-
-- [Apple iCloud Calendar setup](docs/apple_icloud_setup.md) requires two-factor
- authentication and an Apple app-specific password.
-- [Nextcloud setup](docs/nextcloud_setup.md) requires an HTTPS server and
- completes authorization in the default browser.
-
-Windows targets Windows 11 24H2 or newer on x64 only. See
-[Windows development](docs/windows_development.md) for Visual Studio and SDK
-requirements, exact development commands, and the source-side validation
-workflow.
-
-## Run locally on Linux
-
-Register the development launcher once so GNOME can associate BusyMax's native
-Wayland windows with its desktop icon. The helper is idempotent and defaults to
-the Flutter debug bundle:
-
-```bash
-tool/install_linux_dev_desktop.sh
-```
-
-Then run BusyMax normally:
-
-```bash
-flutter run -d linux -t lib/main_linux.dart \
- --dart-define=GOOGLE_OAUTH_CLIENT_ID= \
- --dart-define=GOOGLE_OAUTH_CLIENT_SECRET= \
- --dart-define=MICROSOFT_OAUTH_CLIENT_ID=
-```
-
-Locations are stored as ordinary calendar/task data. **Show on map** hands a
-saved address or point to the registered maps handler on Linux, with a Google
-Maps browser-search fallback, and to the default browser on Windows. A complete
-HTTP(S) location is opened directly as **Open link**. BusyMax has no embedded
-map, address autocomplete, mapping account, or mapping API-key requirement.
-Provider-native coordinates remain in Microsoft structured locations and
-iCalendar `GEO`. When importing or copying a point into Google Calendar, which
-has only free-form location text in this workflow, BusyMax retains a local
-supplemental point for the exact saved event or provider series. A later
-recurring occurrence can reuse the series point only while its saved location
-still matches; an occurrence-specific point takes precedence. Supplemental
-series data follows a Google **This and following** split only when the new
-series keeps that location; the earlier series retains its own association.
-points are not uploaded through Google Calendar and therefore are not
-automatically available on another BusyMax installation.
-
-Use `tool/install_linux_dev_desktop.sh --uninstall` to remove the development
-launcher. Remove it before testing an installed Snap so the user-level launcher
-does not take precedence; packaged Snaps register their own launcher.
-
-## Feedback submissions
-
-The native **Send feedback** form in the About dialog sends JSON to
-`POST https://busystack.org/api/feedback`. Every submission contains a new
-submission UUID, the application identifier `busymax`, the application version
-and build number, the runtime platform identifier, category, subject, message,
-and optional reply email. A successful response has this form:
-
-```json
-{ "id": "server-reference-id" }
-```
-
-The optional technical-details checkbox is off by default. When the user
-explicitly enables it, BusyMax adds only the operating-system version and
-application locale. BusyMax does not attach logs, account or calendar data,
-file names, screenshots, environment variables, tokens, or other diagnostics.
-
-For local website development, override the endpoint through the existing
-compile-time configuration mechanism:
-
-```bash
-flutter run -d linux \
- --dart-define=BUSYSTACK_FEEDBACK_ENDPOINT=http://127.0.0.1:8090/api/feedback
-```
-
-The local Snap helper accepts the same value with
-`--dart-define BUSYSTACK_FEEDBACK_ENDPOINT=http://127.0.0.1:8090/api/feedback`.
-No API, CAPTCHA, or other private server credential is used by the desktop
-application.
-
-## Provider support
-
-The [provider capability matrix](docs/provider_support_matrix.md) lists the
-features and limitations of each integration. Maintainers can use the
-[live-provider test guide](docs/live_provider_testing.md) for opt-in Nextcloud
-and iCloud integration tests.
-
-## Build and publish the Snap
-
-See [Snap Build and Beta Release](docs/beta_snap_release.md) for OAuth build
-configuration, canonical Snapcraft packaging, local installation, artifact
-verification, Store review, and beta release instructions.
-
-## Windows architecture and packaging
-
-Windows uses the separate `lib/main_windows.dart` entrypoint and a Fluent UI
-composition. Linux continues to use `lib/main_linux.dart`, Yaru, GTK, the XDG
-tray, and the existing Snap package. The Windows Store MSIX workflow is local
-and CI-only; it never deploys an artifact.
+## Documentation
-- [Windows architecture](docs/windows_architecture.md)
-- [Windows UI](docs/windows_ui.md)
-- [Windows packaging](docs/windows_packaging.md)
-- [Windows release checklist](docs/windows_release_checklist.md)
+Account, provider, and data information:
+
+- [Provider support matrix](docs/provider_support_matrix.md)
+- [Apple iCloud Calendar setup](docs/apple_icloud_setup.md)
+- [Nextcloud Calendar and Tasks setup](docs/nextcloud_setup.md)
- [Privacy and data map](docs/privacy_data_map.md)
+
+Development and maintenance:
+
+- [Development setup for Linux and Windows](docs/development.md)
+- [Google developer OAuth registration](docs/google_setup.md) and
+ [Microsoft developer OAuth registration](docs/microsoft_setup.md)
+- [iCalendar and DAV data model](docs/icalendar_data_model.md)
+- [Live-provider testing](docs/live_provider_testing.md)
+- [Windows architecture](docs/windows_architecture.md)
+- [Snap beta release](docs/beta_snap_release.md)
+- [Windows packaging](docs/windows_packaging.md) and
+ [release checklist](docs/windows_release_checklist.md)
+- [Maintenance tools](tool/README.md) and
+ [vendored dependencies](third_party/README.md)
+
+## Support and feedback
+
+Report defects and request features in the
+[BusyMax issue tracker](https://github.com/busystack/busymax/issues). The
+in-app **Send feedback** action submits only the fields described in the
+[privacy and data map](docs/privacy_data_map.md); optional technical details are
+off by default.
+
+## License
+
+BusyMax is licensed under the [Apache License 2.0](LICENSE). See
+[NOTICE](NOTICE) for attribution and trademark information.
diff --git a/docs/apple_icloud_setup.md b/docs/apple_icloud_setup.md
index 11daab74..48b2b399 100644
--- a/docs/apple_icloud_setup.md
+++ b/docs/apple_icloud_setup.md
@@ -1,67 +1,60 @@
# Apple iCloud Calendar setup
-BusyMax connects directly to Apple iCloud Calendar over CalDAV. This profile
-synchronizes calendar collections and `VEVENT` resources only. It does not
-connect to Apple Reminders.
+BusyMax connects to Apple iCloud Calendar from Linux and Windows. It
+synchronizes calendar collections and events; Apple Reminders is not supported.
## Before connecting
You need:
- an Apple Account with two-factor authentication enabled;
-- the email address used by that Apple Account; and
+- the email address for that Apple Account; and
- a dedicated app-specific password for BusyMax.
-Do not enter your primary Apple Account password in BusyMax. Apple documents
-app-specific passwords as the fallback for third-party apps that cannot use
-Apple's account-authorization contract. BusyMax's Linux client does not use an
-undocumented Apple authorization flow.
+Do not enter your primary Apple Account password in BusyMax.
-## Create the password and connect
+## Connect
1. Sign in at [account.apple.com](https://account.apple.com/).
-2. Open **Sign-In and Security**, then **App-Specific Passwords**.
-3. Generate a password with a recognizable label such as `BusyMax Linux`.
+2. Open **Sign-In and Security > App-Specific Passwords**.
+3. Generate a password with a recognizable label such as `BusyMax`.
4. In BusyMax, open **Add account** and choose **Apple iCloud Calendar**.
-5. Enter the Apple Account email and the complete generated password. BusyMax
- trims accidental whitespace but otherwise treats the password as opaque.
+5. Enter the Apple Account email and the complete app-specific password.
6. Select **Connect**. BusyMax validates the credential and discovers the
- account's calendars before saving it.
+ account's calendars before saving the account.
-BusyMax starts discovery at `https://caldav.icloud.com/`, follows only
-validated Apple iCloud CalDAV destinations, and requires normal platform TLS
-certificate validation. There is no invalid-certificate, HTTP, or custom
-iCloud-server option.
+BusyMax connects only to Apple iCloud over HTTPS with normal platform
+certificate validation. This profile cannot connect to arbitrary CalDAV
+servers.
## Calendars and editing
-BusyMax shows discovered event calendars and derives whether each one is
-writable from DAV privileges. Shared or subscribed read-only calendars remain
-visible but their edit controls are disabled. Event content is cached locally
-for offline viewing; offline edits to writable calendars are queued and later
-sent with conditional ETag checks.
+BusyMax shows discovered event calendars and follows their reported
+permissions. Shared or subscribed read-only calendars remain visible without
+editing controls. Event data is cached for offline viewing; supported edits to
+writable calendars can be queued and synchronized later with conflict checks.
-Calendar collection creation, deletion, rename, color, and ordering are not
-supported for iCloud. Invitations and scheduling changes are also not
-supported.
+Creating, deleting, renaming, recoloring, or reordering iCloud calendar
+collections is not supported. BusyMax also does not support Apple Reminders,
+calendar invitations, or scheduling changes for this provider. See the
+[provider support matrix](provider_support_matrix.md).
## Reconnect, revoke, or remove
-- If Apple rejects the credential, existing cached data and pending work stay
- local. Generate a replacement app-specific password, then use **Reconnect**.
-- To revoke access remotely, remove the BusyMax password at
- [account.apple.com](https://account.apple.com/) under **App-Specific
- Passwords**. BusyMax cannot revoke an Apple password through CalDAV.
-- Removing the account from BusyMax removes its local credential, cached DAV
- objects, projections, cursors, conflicts, and pending operations. It does
- not revoke the remote Apple password; revoke it manually as well.
+- If Apple rejects the credential, use **Reconnect** with a replacement
+ app-specific password. Cached content and pending work remain available while
+ reconnection is required.
+- To revoke access remotely, delete the BusyMax app-specific password under
+ **Sign-In and Security > App-Specific Passwords** at
+ [account.apple.com](https://account.apple.com/). BusyMax cannot revoke it
+ remotely through Calendar.
+- Removing the account in BusyMax removes its local credential and local
+ account data. It does **not** revoke the app-specific password at Apple, so
+ revoke that password separately.
- Apple states that changing or resetting the primary Apple Account password
- automatically revokes all app-specific passwords.
-
-Apple's current instructions are [Sign in to apps with your Apple Account
-using app-specific passwords](https://support.apple.com/en-gb/102654) and
-[Access your iCloud Mail, Calendar and Contacts in third-party
-apps](https://support.apple.com/en-ie/121539).
+ revokes all app-specific passwords.
-This setup is specific to Apple iCloud and cannot be used for arbitrary CalDAV
-servers.
+Apple's current guidance is
+[Sign in to apps with your Apple Account using app-specific passwords](https://support.apple.com/en-gb/102654)
+and
+[Access your iCloud data in third-party apps](https://support.apple.com/en-ie/121539).
diff --git a/docs/beta_snap_release.md b/docs/beta_snap_release.md
index c0a1dde4..62f0a30a 100644
--- a/docs/beta_snap_release.md
+++ b/docs/beta_snap_release.md
@@ -1,34 +1,28 @@
-# BusyMax Snap Build and Beta Release
+# Snap beta release
-`snap/snapcraft.yaml` packages an existing Flutter Linux bundle. It does not
-run `flutter build` or read `.snap-local/busymax-dart-defines.json`, so the
-OAuth-enabled Flutter build must run first.
+The canonical Snap build packages an existing Flutter Linux release bundle with
+`snap/snapcraft.yaml`. Snapcraft does not run `flutter build` or read the
+local Dart-defines file, so build the configured Flutter bundle first.
## Prepare
-Required: Linux amd64, the Flutter Linux toolchain, snapd, Snapcraft with LXD,
-the `libhandy-1-dev` build package, `unsquashfs` from `squashfs-tools`, and
-BusyMax Store access for publishing.
+Use Linux amd64 with the [BusyMax development toolchain](development.md),
+snapd, Snapcraft with LXD, `libhandy-1-dev`, and `unsquashfs` from
+`squashfs-tools`. Publishing also requires authorization for the BusyMax Snap
+Store listing.
-Check that the package versions match:
+Confirm that source versions agree:
```bash
grep -nE '^version:|_amd64.snap`. Use the exact path it prints.
-If the app says **This provider is not configured**, the bundle was built
-without valid defines; reconnecting cannot fix it, so rebuild the package.
+Snapcraft prints the path to `busymax__amd64.snap`. Use that exact
+artifact for every inspection, installation, checksum, and upload. If BusyMax
+reports that a provider is not configured, the compiled bundle is missing its
+Dart defines and must be rebuilt.
-The `Flutter Linux` GitHub workflow performs the release build and strict Snap
-packaging on pushes and pull requests targeting `main`. Pull requests build and
-install an unconfigured Snap for packaging validation, but do not upload it.
-Pushes to `main` require the two client IDs and Google Desktop client secret in
-GitHub Actions configuration, verify that those values reached the compiled
-binary, and upload the installable `busymax-snap` artifact. Missing provider
-configuration fails the workflow before the release build.
+The Flutter Linux workflow performs source validation, builds
+`lib/main_linux.dart`, packages a strict Snap, and installs it for package
+validation. Pull requests use an unconfigured package and do not upload it.
+Pushes to `main` require the Google and Microsoft release settings, verify
+that they reached the binary, and retain the `busymax-snap` artifact for seven
+days.
-For a local scaffold smoke build instead, first quit every running BusyMax
-instance, including its tray process and any development build:
+### Local scaffold helper
+
+For quick local scaffold testing, quit every BusyMax window and tray process,
+then run:
```bash
./tool/build_install_snap_local.sh \
--dart-define-from-file .snap-local/busymax-dart-defines.json
```
-The helper requires `/snap/busymax/current` or `--scaffold DIR`, repacks and
-installs the local payload, and is not the canonical Store build above. Leave
-`--root` at its safe default. It does not remove or purge app data. Its
-`Defines: 1` output only confirms that one file argument was passed, not that
-the required values are present. Use `--skip-tests` only for a repeat build of
-the same commit after its tests passed; `--no-run` still installs the package.
+The helper repacks `/snap/busymax/current`, or the directory passed with
+`--scaffold`, around a newly built bundle. It is not the canonical Snapcraft
+build and must not be published. Leave `--root` at its safe default. The helper
+does not purge application data. Use `--skip-tests` only for a repeat build of
+the same validated revision. **`--no-run` still installs the package; it only
+suppresses launch.**
-## Verify Locally
+## Inspect and install the exact artifact
-Set the exact artifact path:
+Set the actual artifact path, then record its metadata and checksum:
```bash
SNAP_FILE=./busymax_RELEASE_VERSION_amd64.snap
-```
-
-Check its metadata and save its checksum:
-
-```bash
unsquashfs -cat "$SNAP_FILE" meta/snap.yaml |
sed -n '/^name:/p;/^version:/p;/^grade:/p;/^confinement:/p'
sha256sum "$SNAP_FILE"
```
-Check the top-level launchers:
+Confirm that the package is strict and contains one top-level launcher:
```bash
unsquashfs -ll "$SNAP_FILE" |
sed -nE 's#^.*squashfs-root/meta/gui/([^/]+\.desktop)$#\1#p'
```
-The output must contain exactly `busymax.desktop`.
-`share/applications/io.busystack.busymax.desktop` is an expected internal file,
-not a second top-level launcher.
+The launcher output must contain only `busymax.desktop`.
+`share/applications/io.busystack.busymax.desktop` is the expected internal
+desktop file.
-Close BusyMax and its tray process, then install and launch the local package:
+Quit other BusyMax processes before installing:
```bash
sudo snap install --dangerous "$SNAP_FILE"
@@ -131,55 +109,38 @@ snap connections busymax
snap run busymax
```
-`--dangerous` bypasses Store signature checks, not strict confinement.
-
-Before upload, verify:
-
-- Ordinary location text can be entered and saved without network requests or a
- mapping key. No autocomplete list or embedded map is present.
-- **Show on map** opens the saved address or coordinates through the strict
- Snap's registered external handler. Verify both an installed maps handler and
- the Google Maps browser fallback, plus the failure message when neither route
- can launch.
-- **Open link** appears only for a complete saved HTTP(S) location and preserves
- its path, query, and fragment.
-- Location editing, external opening, editor save/cancel, theme changes, and the
- minimum window size remain usable at 100%, 125%, 150%, and 200% scaling.
-- Desktop search shows one BusyMax launcher; the tray Agenda action opens the
- Agenda view in the main window.
-- Google and Microsoft sign-in complete successfully.
-- Apple iCloud setup accepts only an Apple Account email and app-specific
- password, discovers calendars over verified TLS, and reconnects after a
- controlled app-password revocation.
-- Nextcloud Login Flow v2 opens the system default browser, completes after the
- user returns to BusyMax, preserves an installation path, and uses the
- server-returned canonical credentials.
-- Tasks and events can be created, edited, completed, and deleted; a task
- created in Agenda appears immediately without manual refresh.
-- Accounts, settings, and data survive restart.
-- The XDG Secret portal-backed encrypted credential file works while strictly
- confined: connect, quit, restart the desktop session if practical, reopen,
- sync, reconnect, then remove the account and confirm its local credential is
- gone.
-- Revoked Apple/Nextcloud credentials pause synchronization while cached data
- and pending work remain visible.
-- Read-only/shared DAV collections remain visible but do not expose mutation
- controls; a server ACL change is enforced after refresh.
-- Network, DNS, platform TLS rejection, recurrence/alarm projection, and
- notifications work under confinement.
-- Notifications and tray actions, including opening Agenda in the main window
- and Quit, work.
-- Upgrade a copy of data from the last released package to candidate schema 14.
- Also run the production-like schema-5 fixture through the complete supported
- migration chain to schema 14, preserving provider credentials, cursors,
- pending operations, DAV projections, supplemental locations, and account
- removal/local cleanup.
-- `snap/snapcraft.yaml`, metainfo, and screenshots describe exactly the tested
- providers. Apple wording says iCloud Calendar, not Apple Reminders.
-
-## Upload To Beta
-
-Authenticate if needed, then upload once with the beta release target:
+`--dangerous` bypasses Store signature checking, not strict confinement. The
+installation command does not request a purge, but preserve a backup of release
+test data before any package operation.
+
+## Release verification
+
+Consolidate results in the release record. At minimum verify:
+
+- the installed revision, strict confinement, single desktop launcher, tray
+ actions, notifications, startup, and restart;
+- Google and Microsoft browser sign-in and Apple/Nextcloud connection,
+ reconnect, revocation, read-only permissions, and synchronization;
+- event and provider-supported task create/edit/complete/delete, recurrence,
+ alarms, imports/exports, WebCal, conflict behavior, and offline recovery;
+- external location/link opening as described in the
+ [privacy and data map](privacy_data_map.md), without an embedded map or
+ mapping credential;
+- the strict-Snap Secret portal credential store across quit and desktop
+ restart, followed by account removal;
+- network, DNS, and platform TLS failures;
+- UI, themes, scaling, localization, calendar views, and tray Agenda targeting;
+ and
+- upgrade of a copy of data from the last release plus the supported migration
+ fixture through the complete current migration chain, preserving credentials,
+ provider state, cached content, pending work, conflicts, and supplemental
+ location ownership.
+
+Do not use production personal data for release testing.
+
+## Publish an authorized beta
+
+Authenticate and upload the verified artifact once:
```bash
snapcraft login
@@ -189,33 +150,32 @@ snapcraft revisions busymax --arch amd64
snapcraft status busymax --arch amd64
```
-Save the numeric Store revision printed for the verified checksum. It is an
-immutable upload identifier, separate from the app version. Do not re-upload
-the artifact because review or release is pending.
+Record the numeric Store revision printed for the verified checksum. It is an
+immutable upload identifier, not the application version. Do not re-upload
+while review or release is pending.
-The `busymax-dbus` session D-Bus slot may trigger manual review. If an older
-revision blocks the new one, reject it only when it is obsolete; otherwise
-wait or contact the
-[Store reviewers](https://forum.snapcraft.io/c/store-requests/19). A
-`resource-not-ready` or inconsistent-state error means nothing was released.
-Check the [publisher dashboard](https://dashboard.snapcraft.io/) and retry only
-after review clears.
+The `busymax-dbus` session D-Bus slot can require manual review. If an older
+obsolete revision blocks the candidate, reject only that obsolete revision;
+otherwise wait or contact the
+[Store reviewers](https://forum.snapcraft.io/c/store-requests/19).
+`resource-not-ready` or inconsistent-state errors do not prove a release.
+Check the publisher dashboard before retrying.
-If manual review completes but the revision was not automatically released,
-release the exact reviewed revision:
+If review completes without automatic release, release the exact reviewed
+revision:
```bash
snapcraft release busymax STORE_REVISION beta
snapcraft status busymax --arch amd64
```
-The recipe currently has `grade: devel`, so only `beta` and `edge` are allowed.
-Candidate or stable requires `grade: stable`, a rebuild, a new upload, and the
-same verification.
+The recipe uses `grade: devel`, so it can release only to beta or edge. A
+candidate or stable release requires changing the grade, rebuilding, and
+repeating artifact verification.
-## Verify The Store Revision
+## Verify the Store revision
-Prefer a separate test machine. For a fresh install:
+Prefer a separate test machine:
```bash
sudo snap install busymax --beta
@@ -223,19 +183,19 @@ snap info busymax
snap run busymax
```
-For an existing Store-tracking install:
+For an existing beta-tracking install:
```bash
sudo snap refresh busymax --channel=beta
snap info busymax
```
-Repeat the local smoke checks against the Store-delivered revision.
-
-Record the downloaded revision, channel, checksum, test machine, and smoke-test
-result in the release record. Do not include account identities, credentials,
-DAV resource paths, or calendar and task content.
+Repeat the installed-package checks against the Store-delivered revision.
+Record its revision, channel, checksum, test machine, and results without
+account identities, credentials, DAV paths, or calendar/task content.
-Official references: [build environments](https://documentation.ubuntu.com/snapcraft/stable/reference/build-environment-options/),
+Official references:
+[Snapcraft build environments](https://documentation.ubuntu.com/snapcraft/stable/reference/build-environment-options/),
[upload](https://documentation.ubuntu.com/snapcraft/stable/reference/commands/upload/),
-and [revision management](https://documentation.ubuntu.com/snapcraft/stable/how-to/publishing/manage-revisions-and-releases/).
+and
+[revision management](https://documentation.ubuntu.com/snapcraft/stable/how-to/publishing/manage-revisions-and-releases/).
diff --git a/docs/development.md b/docs/development.md
new file mode 100644
index 00000000..f1fae93e
--- /dev/null
+++ b/docs/development.md
@@ -0,0 +1,171 @@
+# Development
+
+This guide is the shared setup and validation entry point for BusyMax on Linux
+and Windows. Release packaging has separate procedures linked below.
+
+## Shared setup
+
+BusyMax development and CI use Flutter **3.47.2** and the Dart **3.13.2** SDK
+bundled with that Flutter installation. Use the bundled `dart` executable; do
+not mix Flutter with a different global Dart SDK. The `pubspec.yaml` constraint
+`sdk: ^3.12.0` is the Dart language/package compatibility constraint. It is not
+the repository's build-toolchain version.
+
+The workflows verify the selected SDK with `tool/verify_flutter_sdk.dart`.
+After installing Flutter 3.47.2, prepare the checkout in this order:
+
+```bash
+flutter pub get --enforce-lockfile
+flutter gen-l10n
+dart run build_runner build --delete-conflicting-outputs --force-jit
+```
+
+Dependency resolution must precede localization generation, which must precede
+Drift generation. Generated localization and database files are committed; a
+generation run should leave them unchanged.
+
+Google and Microsoft sign-in require provider-specific build configuration.
+You can build, inspect, and test the application without registering both
+providers. Add only the configuration for the sign-in flows you need:
+
+- `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`: see
+ [Google OAuth registration](google_setup.md).
+- `MICROSOFT_OAUTH_CLIENT_ID`: see
+ [Microsoft OAuth registration](microsoft_setup.md).
+
+Apple iCloud Calendar and Nextcloud do not use compile-time OAuth client
+credentials.
+
+## Linux development
+
+The Linux CI build runs on Ubuntu 24.04 and installs the following packages:
+
+```bash
+sudo apt-get install -y \
+ clang \
+ cmake \
+ ninja-build \
+ pkg-config \
+ libgtk-3-dev \
+ libhandy-1-dev \
+ libstdc++-12-dev \
+ libsecret-1-dev \
+ libjsoncpp-dev \
+ liblzma-dev
+```
+
+The CMake project directly requires GTK 3, GLib/GIO, and libhandy; the
+additional CI libraries cover the registered Flutter plugins and native build
+environment.
+
+Run the Linux composition explicitly:
+
+```bash
+flutter config --enable-linux-desktop
+flutter run -d linux -t lib/main_linux.dart
+```
+
+Append the relevant `--dart-define` arguments when testing Google or Microsoft
+sign-in.
+
+### Development desktop registration
+
+Register the checkout for the current user so GNOME can associate native
+Wayland windows with the BusyMax launcher and icon:
+
+```bash
+tool/install_linux_dev_desktop.sh
+```
+
+The helper writes a marked desktop entry and icon below the user's XDG data
+directory. It defaults to the Flutter debug bundle and accepts
+`--executable FILE`. Remove its files with:
+
+```bash
+tool/install_linux_dev_desktop.sh --uninstall
+```
+
+Remove this user-level launcher before testing an installed Snap. Desktop
+search can prefer it over the packaged launcher.
+
+## Windows development
+
+BusyMax targets Windows 11 24H2 (`10.0.26100.0`) or newer on x64. Windows 10,
+x86, ARM64, MSI, and EXE installers are outside the supported target.
+
+Install:
+
+- Flutter 3.47.2 with Windows desktop enabled;
+- Visual Studio 2022 with **Desktop development with C++**, the x64 MSVC
+ toolchain, and CMake tools;
+- Windows SDK `10.0.26100.0` or newer; and
+- Pester 5 when running the packaging contract tests (CI uses 5.7.1).
+
+From a PowerShell prompt at the repository root, verify the environment:
+
+```powershell
+flutter config --enable-windows-desktop
+.\tool\windows\check_prerequisites.ps1
+```
+
+Run the Windows composition explicitly. This example configures both browser
+sign-in providers and includes the Google desktop client secret required by the
+Google setup guide:
+
+```powershell
+flutter run -d windows -t lib/main_windows.dart `
+ --dart-define=BUSYMAX_WINDOWS_AUMID=BusyStack.BusyMax.Development `
+ --dart-define=GOOGLE_OAUTH_CLIENT_ID= `
+ --dart-define=GOOGLE_OAUTH_CLIENT_SECRET= `
+ --dart-define=MICROSOFT_OAUTH_CLIENT_ID=
+```
+
+Omit the defines for providers you are not testing. An unpackaged development
+build reports `StartupTask` as unavailable and does not use a registry or
+Startup-folder substitute. Windows notification display, cancellation, and
+action callbacks require installed package identity, so exercise them with the
+test-signed MSIX described in [Windows packaging](windows_packaging.md).
+
+## Validation
+
+After the shared preparation sequence, run the normal offline checks:
+
+```bash
+dart format --output=none --set-exit-if-changed .
+flutter analyze
+flutter test
+dart run tool/check_platform_boundaries.dart
+```
+
+The platform-boundary checker prevents shared or Windows code from reaching
+Linux-only UI and services, and prevents shared business or Linux code from
+depending on Fluent UI.
+
+Normal `flutter test` skips credential-gated provider tests. Those tests mutate
+remote data and have separate setup and safety requirements in
+[Live-provider testing](live_provider_testing.md).
+
+The Linux and Windows workflows run for pull requests targeting `main` and
+pushes to `main`; Windows also supports a manual dispatch. A newer run for the
+same workflow and ref cancels the superseded run. Windows test reports are
+retained for every run, while the unsigned CI MSIX and package evidence are
+retained for seven days only after a successful `main` push or manual run.
+The workflows do not deploy Windows packages.
+
+For release artifacts, follow [Snap beta release](beta_snap_release.md) or
+[Windows packaging](windows_packaging.md) and the
+[Windows release checklist](windows_release_checklist.md).
+
+## Optional feedback endpoint
+
+Local website development can override the feedback destination with the
+compile-time `BUSYSTACK_FEEDBACK_ENDPOINT` setting, for example:
+
+```bash
+flutter run -d linux -t lib/main_linux.dart \
+ --dart-define=BUSYSTACK_FEEDBACK_ENDPOINT=http://127.0.0.1:8090/api/feedback
+```
+
+The configuration is defined in
+[`BuildConfig`](../lib/src/config/build_config.dart); submission behavior lives
+in the [feedback client](../lib/src/features/feedback/data/feedback_api_client.dart).
diff --git a/docs/google_setup.md b/docs/google_setup.md
index 0333a341..428deffe 100644
--- a/docs/google_setup.md
+++ b/docs/google_setup.md
@@ -1,43 +1,51 @@
-# Google OAuth setup
-
-BusyMax requires a Google desktop OAuth client. Its client ID and client secret
-are supplied as `GOOGLE_OAUTH_CLIENT_ID` and
-`GOOGLE_OAUTH_CLIENT_SECRET` at build time.
-
-## Create a Google Cloud project
-
-1. Open the [Google Cloud Console](https://console.cloud.google.com/).
-2. Create or select a project.
-3. Enable the Google Tasks API and Google Calendar API.
-
-## Configure the consent screen
-
-1. Open [Google Auth Platform](https://console.cloud.google.com/auth/).
-2. Complete the initial setup with the application name, support email,
- audience, and contact email.
-3. Under **Audience**, add development accounts as test users while the app is
- in testing mode.
-4. Under **Data access**, add these scopes:
-
- ```text
- openid
- https://www.googleapis.com/auth/userinfo.email
- https://www.googleapis.com/auth/userinfo.profile
- https://www.googleapis.com/auth/tasks
- https://www.googleapis.com/auth/calendar
- ```
-
-The identity scopes provide the stable account identity and display label. The
-Tasks and Calendar scopes allow BusyMax to synchronize and edit the
-corresponding data.
+# Google OAuth registration
+
+This guide is for developers and release maintainers who configure a BusyMax
+build. People installing an already configured package do not need to create a
+Google Cloud project.
+
+BusyMax reads the Google desktop OAuth client ID and client secret from the
+compile-time settings `GOOGLE_OAUTH_CLIENT_ID` and
+`GOOGLE_OAUTH_CLIENT_SECRET`.
+
+## Configure Google APIs and consent
+
+1. Create or select a project in the
+ [Google Cloud console](https://console.cloud.google.com/).
+2. Enable the Google Tasks API and Google Calendar API.
+3. Open [Google Auth Platform](https://console.cloud.google.com/auth/), complete
+ the Branding, Audience, and Contact Information setup, and keep development
+ accounts under **Audience > Test users** while the app is in testing.
+4. Under **Data Access**, add the Google Tasks read/write scope
+ `https://www.googleapis.com/auth/tasks` and the Google Calendar read/write
+ scope `https://www.googleapis.com/auth/calendar`.
+
+BusyMax's runtime authorization request also contains the OpenID Connect scope
+strings `openid`, `email`, and `profile`. Google may show the last two in
+the console as `https://www.googleapis.com/auth/userinfo.email` and
+`https://www.googleapis.com/auth/userinfo.profile`. These identity scopes
+provide the account identity and display label; the Tasks and Calendar scopes
+authorize provider data access.
+
+Google documents the current console areas in
+[Get started with Google Auth Platform](https://support.google.com/cloud/answer/15544987)
+and the Tasks permission in
+[Choose Google Tasks API scopes](https://developers.google.com/workspace/tasks/auth).
## Create the desktop client
-1. Open **Clients** and select **Create client**.
+1. Open **Google Auth Platform > Clients** and select **Create client**.
2. Choose **Desktop app** as the application type.
-3. Give the client a recognizable name.
-4. Store the client ID and client secret securely and provide them to the build
- as `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`.
-
-Use only credentials created for this desktop application. Do not commit them
-to the repository.
+3. Give the client a recognizable name and create it.
+4. Supply the resulting client ID and client secret to the BusyMax build.
+
+BusyMax opens the system browser and listens on a temporary
+`http://127.0.0.1:/` loopback callback. Do not configure a web
+application client or a hosted redirect endpoint for this flow.
+
+Desktop OAuth configuration is embedded in the application package and can be
+extracted; it is not a protected server-side secret. Use credentials dedicated
+to this desktop application, restrict access to the build configuration, and
+never commit credentials or generated credential files. Google's
+[OAuth client documentation](https://support.google.com/cloud/answer/15549257)
+describes desktop applications as public clients.
diff --git a/docs/icalendar_data_model.md b/docs/icalendar_data_model.md
index 402c650d..bb1be224 100644
--- a/docs/icalendar_data_model.md
+++ b/docs/icalendar_data_model.md
@@ -1,105 +1,137 @@
-# iCalendar data model
+# iCalendar and DAV data model
-BusyMax keeps the server's complete iCalendar resource as the synchronization
-authority. It does not reconstruct DAV resources from normalized event or task
-rows.
+This page describes the preservation and mutation rules maintainers must keep
+when changing BusyMax's Apple iCloud Calendar or Nextcloud integrations.
-Two layers operate on each resource:
+## Complete resources and projections
+
+The server's complete iCalendar resource is the synchronization authority.
+BusyMax does not rebuild DAV objects from normalized event or task rows.
+
+Each resource has two layers:
- The document layer retains property order, unknown and duplicate properties,
parameters, sibling components, alarms, recurrence exceptions, and
- `VTIMEZONE` data. Edits patch only the affected fields.
-- The semantic layer projects supported `VEVENT` and `VTODO` fields into the
- local database for display, search, reminders, and recurrence expansion.
+ `VTIMEZONE` definitions. Edits patch only affected fields.
+- The semantic layer projects supported `VEVENT` and `VTODO` values into
+ Drift for display, search, reminders, and recurrence expansion.
Mutated resources use CRLF line endings and fold content lines at 75 UTF-8
-octets. Untouched data remains intact, including provider-specific extensions
-that BusyMax does not interpret.
+octets. Unknown provider extensions, unsupported recurrence forms, detached
+instances, custom timezones, and alarm details remain intact when BusyMax edits
+a supported field.
-## Editable Nextcloud `VTODO` data
+The `xml` package parses namespace-aware WebDAV responses. BusyMax enforces
+response, depth, element, and text limits and rejects DTD and entity
+declarations before parsing.
-The Nextcloud task editor is based on the official
-[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1).
-BusyMax can project and patch these properties without regenerating the rest of
-the resource:
+## Editable and preserved task data
-| Data | iCalendar representation |
+The Nextcloud task editor projects and patches the following supported data:
+
+| Area | iCalendar representation |
|---|---|
| Identity and text | `UID`, `SUMMARY`, `DESCRIPTION`, `CATEGORIES` |
-| Scheduling | `DTSTART`, `DUE`, date-only, floating, UTC, or `TZID` values |
+| Schedule | `DTSTART`, `DUE`, date-only, floating, UTC, and `TZID` values |
| Progress | `STATUS`, `PERCENT-COMPLETE`, `COMPLETED` |
| Details | `PRIORITY`, `LOCATION`, `URL`, `CLASS` |
| Hierarchy and order | parent `RELATED-TO`, `X-APPLE-SORT-ORDER` |
| Recurrence | `RRULE`, `RDATE`, `EXDATE`, `RECURRENCE-ID` |
-| Reminders | every `VALARM` child component |
-| Nextcloud UI state | `X-PINNED`, `X-OC-HIDESUBTASKS`, `X-OC-HIDECOMPLETEDSUBTASKS` |
+| Reminders | `VALARM` child components |
+| Nextcloud state | `X-PINNED`, `X-OC-HIDESUBTASKS`, `X-OC-HIDECOMPLETEDSUBTASKS` |
| Bookkeeping | `CREATED`, `LAST-MODIFIED`, `DTSTAMP` |
-Status, percentage, and completion date are changed together using the same
-state transitions as Nextcloud Tasks. In particular, reopening a 100-percent
-complete task changes it to 99 percent when an in-progress value is needed.
-Completing a parent completes its open descendants; reopening a descendant
-reopens a closed ancestor. Parent deletion is queued child-first.
-
-The recurrence editor covers the subset exposed by Nextcloud Tasks 0.18.1:
-`DAILY`, `WEEKLY`, `MONTHLY`, and `YEARLY`; `INTERVAL`; the supported `BYDAY`,
-`BYMONTH`, `BYMONTHDAY`, and `BYSETPOS` combinations; and either `COUNT` or
-`UNTIL`. Rules outside that subset, multiple `RRULE` properties, and detached
-instances remain synchronized and preserved but are not rewritten by the
-editor. Completing a recurring master creates the completed exception and
-advances the master when the rule has another occurrence.
-
-Every alarm remains in source order. The editor adds absolute reminders and
-before-start or before-due relative reminders. It applies the same editability
-rules as Nextcloud Tasks 0.18.1: absolute UTC triggers and supported
-start-relative triggers can be changed; due-relative, timed positive, and
-other unsupported trigger forms remain visible, removable, and intact. Alarm
-actions and additional properties are preserved. Removing a start or due value
-with related alarms requires the user to either remove those alarms or convert
-them to absolute triggers.
-
-## DAV task operations
-
-Task-object creates, updates, moves, and deletes are represented as durable
-pending operations. Updates use exact ETag preconditions. A cross-list move
-uses WebDAV `MOVE` with the original member name, `Depth: infinity`, and
-`Overwrite: F`; descendants are moved child-first so parent relationships stay
-valid. Recursive duplication assigns a new UID to the complete recurrence set
-and then duplicates descendants under their corresponding new parents. Clear
-completed selects closed root tasks and recursively deletes their descendants,
-including cancelled root trees as Nextcloud Tasks does.
-
-Creating a Nextcloud task list uses extended `MKCOL` with a `VTODO`-only
-supported component set, the Nextcloud default blue `#0082C9`, and the enabled
-property. Rename uses `PROPPATCH`; delete and unshare use `DELETE`. The list
-slug rules and collision suffixes follow Nextcloud Tasks 0.18.1.
-
-## Dependency decision
-
-No general-purpose iCalendar or CalDAV package is authoritative for this data.
-The evaluated packages normalized or regenerated resources in ways that could
-discard unsupported content or could not satisfy BusyMax's sync-token,
-conditional-write, redirect, and parser-limit requirements.
-
-The `xml` package is used only for namespace-aware WebDAV XML parsing. BusyMax
-adds response-size, depth, element-count, and text-size limits and rejects DTD
-and entity declarations before parsing.
-
-## Protocol references
-
-- [RFC 4791](https://www.rfc-editor.org/rfc/rfc4791): CalDAV collections,
- reports, object resources, and write preconditions
-- [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545): iCalendar syntax and
- semantics
-- [RFC 4918](https://www.rfc-editor.org/rfc/rfc4918): WebDAV properties,
- conditional writes, `PROPPATCH`, `MOVE`, and `DELETE`
+Status, percentage, and completion time change as one state transition.
+Completing a parent completes open descendants; reopening a descendant reopens
+a closed ancestor. Completing a recurring master records its completed
+instance and advances the master when another occurrence exists.
+
+The editor supports the daily, weekly, monthly, and yearly recurrence subset
+represented by its controls, including supported intervals, selectors, counts,
+and end dates. Rules outside that subset, multiple `RRULE` properties, and
+detached instances remain synchronized but read-only.
+
+All alarms remain in source order. BusyMax can add absolute reminders and
+supported start- or due-relative reminders. Unsupported trigger forms and alarm
+actions remain visible or preserved without normalization. Removing a start or
+due value with a dependent alarm requires removing that alarm or converting it
+to an absolute trigger.
+
+The implementation reference is
+[Nextcloud Tasks 0.18.1](https://github.com/nextcloud/tasks/tree/v0.18.1).
+That version defines the current compatibility target; it is not a claim that a
+live server with that Tasks version has been tested.
+
+## Durable mutations and conflicts
+
+Creates, updates, moves, and deletes are durable pending operations. Updates
+and deletes use exact ETag preconditions. On a concurrent server change,
+BusyMax merges only provably disjoint field edits; otherwise it records an
+explicit conflict for the user.
+
+Task hierarchy operations preserve parent identity and dependency order.
+Parent deletion is queued child-first. Cross-list Nextcloud moves retain the
+member name and move descendants child-first. Duplication assigns new UIDs to
+the recurrence set and maps descendants to the corresponding new parents.
+Clear-completed applies to closed root task trees.
+
+Collection creation and administration are separate from object mutations and
+require live permission checks. Nextcloud event collections are created for
+`VEVENT`; task lists use a `VTODO` component set. Mixed collections remain a
+single remote resource.
+
+## Nextcloud extensions
+
+BusyMax discovers collection types, privileges, scheduling principals, and
+supported reports rather than enabling behavior from the server brand alone.
+Cached WebCal subscriptions remain Nextcloud-owned sources and use the
+advertised `nc-calendar-webcal-cache` extension; BusyMax does not send account
+credentials to the upstream subscription URL.
+
+Collection settings, sharing, and publishing follow advertised properties and
+principal permissions. Content writing, property writing, sharing
+administration, and owner deletion are distinct. A partial property update is
+reported per property; an uncertain commit is reconciled before the user is
+asked to retry.
+
+Calendar scheduling uses discovered calendar-user addresses and outbox
+privileges. Organizer and attendee changes use the ordinary conditional event
+write and the server's implicit scheduling; BusyMax does not also send an SMTP
+or duplicate scheduling message. Free/busy failures produce unknown
+availability. Inbox acknowledgement removes the scheduling message, not the
+calendar event. `Schedule-Tag` support and VTODO invitations are not assumed.
+
+Native imports group resources by component type and UID and preserve
+recurrence sets, timezones, alarms, participant parameters, and unknown
+properties. Scheduling methods require normalization acknowledgement, and
+Nextcloud imports use `X-NC-Scheduling: false` so stored imports do not send
+invitations. Exports use the effective local resource, including pending edits.
+Collection export is a cached snapshot and writes separate resources so
+conflicting timezone definitions are not merged.
+
+Trash discovery covers deleted events, tasks, and collections when Nextcloud
+advertises the extension. Restore uses the deleted-resource href and restore
+target returned by the server and rechecks permissions. Ordinary deletion and
+confirmed permanent deletion remain separate operations.
+
+Nextcloud Server **34.0.3** is an implementation reference, not a tested server
+version. Useful versioned sources include the
+[calendar-home implementation](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/CalendarHome.php),
+[scheduling plugin](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/Schedule/Plugin.php),
+[WebCal caching plugin](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php),
+and
+[trash restore target](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php).
+Record the versions actually installed during
+[live-provider testing](live_provider_testing.md).
+
+## Standards
+
+- [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545): iCalendar
+- [RFC 5546](https://www.rfc-editor.org/rfc/rfc5546): iTIP scheduling
+- [RFC 4791](https://www.rfc-editor.org/rfc/rfc4791): CalDAV
+- [RFC 4918](https://www.rfc-editor.org/rfc/rfc4918): WebDAV
- [RFC 5689](https://www.rfc-editor.org/rfc/rfc5689): extended `MKCOL`
-- [RFC 6578](https://www.rfc-editor.org/rfc/rfc6578): WebDAV collection
- synchronization
+- [RFC 6578](https://www.rfc-editor.org/rfc/rfc6578): collection synchronization
+- [RFC 6638](https://www.rfc-editor.org/rfc/rfc6638): CalDAV scheduling
- [RFC 6764](https://www.rfc-editor.org/rfc/rfc6764): service discovery
-- [RFC 7809](https://www.rfc-editor.org/rfc/rfc7809): time zones by reference
-
-The DAV and iCalendar test suites cover lossless field patches, task-state and
-hierarchy transitions, recurring completion, alarms, custom time zones,
-recursive duplicate/delete, cross-list `MOVE`, collection mutations, offline
-mutation replay, and server conflict handling.
+- [RFC 7809](https://www.rfc-editor.org/rfc/rfc7809): timezones by reference
diff --git a/docs/live_provider_testing.md b/docs/live_provider_testing.md
index bfb183b5..6a0f10e3 100644
--- a/docs/live_provider_testing.md
+++ b/docs/live_provider_testing.md
@@ -1,27 +1,30 @@
-# Live provider tests
+# Live-provider testing
Live integration tests are opt-in and are skipped by a normal `flutter test`
run. They create and delete remote calendars and objects, change sharing
-permissions, and may revoke app passwords. Run them only against disposable QA
-accounts and isolated test servers.
+permissions, and can revoke app passwords. Use only disposable QA accounts,
+isolated test servers, and QA-only invitation recipients.
-Pass credentials through the test process environment. Do not commit them or
-include them in logs, screenshots, or bug reports.
+Pass credentials through the test process environment. Never commit them or
+include credentials, DAV paths, Login Flow URLs or tokens, raw iCalendar, or
+user content in logs, screenshots, and bug reports. A fixture may use loopback
+HTTP or a test CA only where stated below; production account connections still
+require HTTPS and normal platform certificate validation.
-For every Nextcloud run, record the installed Calendar and Tasks app versions
-from the QA installation (not the latest available releases):
+## Record installed Nextcloud versions
+
+For each Nextcloud run, record the versions actually installed on the QA
+system. Do not copy implementation-reference versions from other documentation.
```text
BUSYMAX_NEXTCLOUD_QA_CALENDAR_VERSION=
BUSYMAX_NEXTCLOUD_QA_TASKS_VERSION=
```
-The main DAV, sharing and Login Flow tests read the actual Server version from
-the installation's public `status.php` and report version-only evidence. They
-do not request administrative app-management access. Initial reference targets
-are Server 34.0.3 and Tasks 0.18.1; do not substitute these numbers for actual
-installed versions. Supply an installation root (including a subdirectory when
-applicable), not a calendar-object URL, in live-test environment variables.
+The DAV, sharing, and Login Flow tests read the Server version from the
+installation's public `status.php`. They do not request administrative app
+management. Supply the installation root, including a subdirectory when used,
+rather than a calendar-object URL.
## Nextcloud DAV
@@ -40,18 +43,18 @@ Run:
flutter test test/dav/nextcloud_live_integration_test.dart
```
-The URL may use HTTP only for an isolated loopback fixture. Production
-Nextcloud profiles require HTTPS.
+The URL may use HTTP only for an isolated loopback fixture. Set
+`BUSYMAX_NEXTCLOUD_LIVE_LARGE=1` to include the 128-member collection case.
-Set `BUSYMAX_NEXTCLOUD_LIVE_LARGE=1` to include the 128-member collection test.
-The restart test uses two separate runs:
+The restart scenario requires two runs against the same persistent server
+storage. First set:
```text
BUSYMAX_NEXTCLOUD_LIVE_RESTART_ID=
BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE=prepare
```
-Restart the server without replacing its persistent storage, then rerun with
+Restart the server without replacing storage, then rerun with
`BUSYMAX_NEXTCLOUD_LIVE_RESTART_STAGE=verify`.
## Nextcloud Login Flow v2
@@ -63,7 +66,7 @@ BUSYMAX_NEXTCLOUD_LOGIN_LIVE=1
BUSYMAX_NEXTCLOUD_LOGIN_LIVE_URL=
BUSYMAX_NEXTCLOUD_LOGIN_LIVE_USERNAME=
BUSYMAX_NEXTCLOUD_LOGIN_LIVE_APP_PASSWORD=
-BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT=
+BUSYMAX_NEXTCLOUD_LOGIN_LIVE_TLS_CERT=
BUSYMAX_NEXTCLOUD_LOGIN_LIVE_BROWSER=
```
@@ -75,7 +78,7 @@ flutter test test/dav/nextcloud_login_flow_live_test.dart
Run once at the server root and once through a path-prefixed installation such
as `/nextcloud`. The test revokes the app password returned by Login Flow, so
-use credentials created for this purpose.
+use credentials created for this test.
## Nextcloud sharing
@@ -101,36 +104,8 @@ Run:
flutter test test/dav/nextcloud_sharing_live_test.dart
```
-This test creates a calendar, changes user and group shares, verifies effective
-permissions including reader write-properties, and removes the collection.
-
-## Extended Nextcloud release matrix
-
-The deterministic suites under `test/dav/nextcloud/` and existing mutation,
-discovery and sync suites exercise the shared implementation without accounts.
-Normal runs must remain offline from providers. Opt-in live test success alone
-does not cover the complete desktop release matrix. In disposable QA collections
-also verify and record:
-
-| Area | Live/desktop checks |
-| --- | --- |
-| Inventory | Mixed, subscribed-only/cache-enabled, delegated read/write, received shares and writable federation; fail one home and retain cached sources/pending edits |
-| Management | Calendar/list creation, metadata per-property results, mixed deletion warnings, self-unshare, publication URL, successful write followed by failed refresh |
-| Scheduling | Organizer invite/update/remove guests/cancel; attendee RSVP occurrence/series; offline replay; count actual received messages; unknown free/busy; inbox acknowledgement without event deletion |
-| Tasks 0.18.1 | Repeating completion with exceptions and alarms; status/percentage/completed consistency; duplication, subtree movement/parents, X-properties and shared classification restrictions |
-| Native data | Raw event/task import/export with zones, recurrence, unknown fields and pending edits; explicit copies and parent identities; silent import sends no invitation |
-| Trash | Event, VTODO and whole calendar/list restoration; permissions changing after confirmation; collisions, retention expiry; separately confirmed permanent deletion |
-| Sync | Exact ETags, disjoint/conflicting edits, invalid sync tokens, truncated reports, restart and unknown-outcome reconciliation |
-| Desktop | Both actual entrypoints on native hosts; keyboard, browser launch, small windows, themes and 100/125/150/200% scaling |
-
-Never use unrelated calendars, recipients or user data. Scheduling tests need
-QA-only recipient accounts and approval to deliver test invitations; they must
-not reuse arbitrary attendees from an imported calendar. Ordinary deletion must
-not set the trash-bypass header. Inspect the QA inbox and Nextcloud Calendar/Tasks
-applications as well as BusyMax's stored state.
-
-Record unexecuted cases as **not run**, not passed or supported solely from an
-enabled UI control. See [the implementation record](nextcloud_interoperability.md).
+The test creates a calendar, changes user and group shares, verifies effective
+permissions including reader property access, and removes the collection.
## Apple iCloud Calendar
@@ -145,20 +120,60 @@ BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_WRITABLE=1
BUSYMAX_ICLOUD_LIVE_EXPECT_SHARED_READ_ONLY=1
```
-The two shared-calendar flags are optional and should be set only when those
+The shared-calendar flags are optional and should be set only when those
fixtures exist. Run:
```bash
flutter test test/dav/apple_icloud_live_integration_test.dart
```
-The test covers discovery, collection permissions, conditional event writes,
-date and recurrence forms, alarms, conflict behavior, credential replacement,
-and local account removal. It does not replace a manual check in Apple Calendar
-or iCloud.com.
+The test covers discovery, collection permissions, conditional writes, date
+and recurrence forms, alarms, conflicts, credential replacement, and local
+account removal. Confirm important results independently in Apple Calendar or
+iCloud.com.
+
+## Coverage that still needs real systems
+
+Deterministic suites under `test/dav/` cover parsing and preservation,
+discovery, exact ETags, mutation queues, conflicts, delegated discovery
+failures, scheduling intent, import/export, and trash/sharing safety with fake
+transports. They do not prove a particular server version, browser
+authorization, invitation delivery, federation, retention, Nextcloud
+Calendar/Tasks rendering, Apple rendering, or native desktop integration.
+
+Before a release claim, exercise and record:
+
+- authentication, reconnect, and account removal at the Nextcloud root and a
+ subdirectory installation;
+- owned, read-only, writable, group-shared, delegated, federated, mixed, and
+ cached-subscription collections, including permission changes while work is
+ queued;
+- collection creation/settings, mixed-collection deletion warnings,
+ self-unshare, publishing, failed refresh after a committed change, and
+ uncertain-outcome reconciliation;
+- organizer invitations, updates, guest removal and cancellation; attendee
+ occurrence/series replies; free/busy failures; inbox acknowledgement; and
+ the actual count of delivered QA messages;
+- repeating task completion, exceptions, alarms, status/progress consistency,
+ duplication, hierarchy, subtree moves, and shared-classification rules in
+ the installed Nextcloud Tasks app;
+- silent import, effective-resource export, timezones, recurrence, unknown
+ properties, copied UIDs/parent identities, and pending local edits;
+- event, task, and collection trash restoration, collisions, retention expiry,
+ and separately confirmed permanent deletion;
+- restart, invalid sync tokens, truncated reports, concurrent edits, and
+ unknown outcomes; and
+- both native entry points on their actual hosts, including browser launch,
+ keyboard access, themes, small windows, and 100%, 125%, 150%, and 200% scale.
+
+Never send test invitations to arbitrary attendees copied from imported data.
+Ordinary deletion must not use a trash-bypass action. Inspect the QA provider
+applications as well as BusyMax's local state.
## Recording results
-Record the source revision, provider/server versions, operating environment,
-test command, and result. Redact credentials, DAV resource paths, Login Flow
-URLs and tokens, raw iCalendar, and user content before sharing any output.
+Keep one release record containing the source revision, provider and installed
+server/app versions, operating system, native entry point or package identity,
+commands, result for each case, and relevant redacted evidence. Mark
+unexecuted cases **not run**; do not infer a pass from an enabled control or a
+deterministic test.
diff --git a/docs/microsoft_setup.md b/docs/microsoft_setup.md
index 49268dab..47b63919 100644
--- a/docs/microsoft_setup.md
+++ b/docs/microsoft_setup.md
@@ -1,27 +1,35 @@
-# Microsoft OAuth setup
+# Microsoft OAuth registration
-BusyMax requires a Microsoft public-client application ID, supplied as
-`MICROSOFT_OAUTH_CLIENT_ID` at build time.
+This guide is for developers and release maintainers who configure a BusyMax
+build. People installing an already configured package do not need their own
+Microsoft Entra application.
+
+BusyMax reads the public-client application ID from the compile-time setting
+`MICROSOFT_OAUTH_CLIENT_ID`.
## Create the application registration
-1. Open [Microsoft Entra](https://entra.microsoft.com/).
-2. Go to **App registrations** and select **New registration**.
-3. Enter an application name.
-4. Select the account type that supports both organizational and personal
- Microsoft accounts.
-5. Add a **Public client/native mobile and desktop** redirect URI:
+1. In the [Microsoft Entra admin center](https://entra.microsoft.com/), open
+ **App registrations** and select **New registration**.
+2. Enter an application name.
+3. Select the supported account type that includes both organizational
+ directories and personal Microsoft accounts.
+4. Register the application and copy its **Application (client) ID**.
+5. Under **Authentication**, add the **Mobile and desktop applications**
+ platform and the redirect URI:
```text
http://localhost
```
-6. Register the application and copy its **Application (client) ID**. Supply
- that value as `MICROSOFT_OAUTH_CLIENT_ID`.
+BusyMax opens the system browser and listens on an ephemeral localhost port.
+Microsoft's
+[desktop registration guidance](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-desktop-app-sign-in)
+uses this redirect for a system-browser desktop flow.
-## Add delegated Microsoft Graph permissions
+## Add delegated permissions
-Under **API permissions**, add these delegated Microsoft Graph permissions:
+Under **API permissions**, add these **delegated** Microsoft Graph permissions:
```text
User.Read
@@ -29,5 +37,16 @@ Tasks.ReadWrite
Calendars.ReadWrite
```
-Do not create or embed a client secret. BusyMax uses the public-client OAuth
-flow.
+These are provider-console permission names. At runtime BusyMax requests their
+fully qualified Graph scope strings together with the identity scopes
+`openid`, `profile`, and `email`, plus `offline_access` so it can request
+refresh tokens. See the official
+[Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference)
+and
+[OpenID Connect scope reference](https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc).
+
+Supply the application ID as `MICROSOFT_OAUTH_CLIENT_ID`. It is embedded in
+the desktop package, so treat it as public application configuration and do not
+commit private build configuration. Do not create or embed a client secret:
+BusyMax uses an authorization-code flow with PKCE as a public client, not a
+confidential-client flow.
diff --git a/docs/nextcloud_interoperability.md b/docs/nextcloud_interoperability.md
deleted file mode 100644
index 2f53d9d4..00000000
--- a/docs/nextcloud_interoperability.md
+++ /dev/null
@@ -1,103 +0,0 @@
-# Nextcloud implementation and verification record
-
-## Reference baseline
-
-The implementation targets the version-tagged Nextcloud Server 34.0.3 source
-and Tasks 0.18.1. Server 35 development documentation was not used as the stable
-contract. No live Nextcloud Server, Calendar or Tasks installation has been
-exercised in this implementation environment; their **tested versions are
-unrecorded, not assumed to equal the reference versions**.
-
-Stable protocol references:
-
-- [Webcal caching plugin, v34.0.3](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/WebcalCaching/Plugin.php)
-- [Scheduling plugin, v34.0.3](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/Schedule/Plugin.php)
-- [Deleted object permissions, v34.0.3](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php)
-- [Restore target, v34.0.3](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php)
-- [Calendar home inventory, v34.0.3](https://github.com/nextcloud/server/blob/v34.0.3/apps/dav/lib/CalDAV/CalendarHome.php)
-- [Sabre scheduling limitations](https://sabre.io/dav/scheduling/)
-- [CalDAV scheduling, RFC 6638](https://www.rfc-editor.org/rfc/rfc6638.html),
- [iTIP, RFC 5546](https://www.rfc-editor.org/rfc/rfc5546.html),
- [CalDAV, RFC 4791](https://www.rfc-editor.org/rfc/rfc4791.html),
- [WebDAV sync, RFC 6578](https://www.rfc-editor.org/rfc/rfc6578.html).
-
-## Code ownership
-
-| Area | Main implementation |
-| --- | --- |
-| Effective capabilities, typed collections, contexts | `lib/src/dav/storage/dav_collection_capabilities.dart`, `lib/src/dav/discovery/` |
-| Metadata, shares, publishing, trash | `lib/src/dav/nextcloud/nextcloud_{collection,sharing,trash}_service.dart` |
-| Scheduling identities and raw mutations | `lib/src/dav/nextcloud/nextcloud_{scheduling_policy,scheduling_mutations,meeting_mutations}.dart` |
-| Free/busy, inbox and shared state | `lib/src/dav/nextcloud/nextcloud_scheduling_{service,controller}.dart` |
-| Raw import/export | `lib/src/dav/nextcloud/nextcloud_native_{import,export}.dart` |
-| Offline replay and canonical refetch | `lib/src/dav/mutation/`, `lib/src/dav/sync/dav_account_sync_engine.dart` |
-| Event/task projection and repositories | `lib/src/dav/storage/dav_object_repository.dart`, `lib/src/features/calendar/data/calendar_repository.dart`, existing tasks repository |
-| Linux controls | `lib/src/dav/presentation/`, existing event editor, schedule workspace and sidebar |
-| Windows controls | `lib/src/ui/windows/windows_nextcloud*_dialog*.dart`, event editor, schedule/task pages and source pane |
-| Composition and strings | `lib/src/app/app_bootstrap.dart`, `lib/l10n/` |
-
-No new Nextcloud database or provider stack was introduced. Delegated principal
-contexts and advertised metadata use the existing DAV service/collection JSON.
-Discovery and projection versions trigger reprojection of cached resources.
-The workspace already contains the separate locations migration **13 → 14**;
-this Nextcloud work adds no additional SQL schema migration. Existing accounts,
-raw resources, pending operations and conflicts are retained.
-
-## Automated evidence
-
-The scheduling commit `eb3ca3c` passed 591 tests across `test/dav`, calendar
-presentation, schedule, shared schedule and Windows suites, with 10 explicitly
-skipped live tests. Static analysis and platform boundaries passed at that
-commit. Subsequent restoration/sharing safety changes passed 24 focused
-administration/native-import/native-dialog tests. Final regression results are
-recorded below after the final run; an earlier pass is not proof of later edits.
-
-Fake transport tests cover exact ETags, per-property multistatus failures,
-read-only content with writable metadata, delegated discovery failure, cached
-subscriptions, occurrence mutation coalescing, server scheduling canonicalization,
-offline intent, recipient free/busy failures, inbox acknowledgement isolation,
-native import/export and restoration of returned event/task/calendar hrefs.
-They do not prove a real server's invitation delivery, federation, retention,
-Calendar app rendering or Tasks recurring-completion interoperability.
-
-For source revision `0ced525` on 2026-09-10, the complete Linux test run passed
-1,975 tests with zero failures. Ten live-provider tests were explicitly skipped
-because the opt-in credentials and disposable server configuration were not
-present. Focused administration coverage confirms that unresolved incoming
-calendar and task moves block deletion of their destination and source across
-pending, retry, failed, authentication-blocked, and conflict states, without a
-remote `DELETE`; unrelated accounts/collections remain removable and supported
-discard permits deletion afterward. Focused export and conflict tests also
-confirm that retained failed event/task edits remain in native exports and that
-accepting an already-cached server version immediately restores the visible
-projection and its reminder schedule.
-
-## Live versions and result for this candidate
-
-| Component | Actually installed/tested version | Result |
-| --- | --- | --- |
-| Nextcloud Server | Not available in this environment | Not run |
-| Nextcloud Calendar | Not available in this environment | Not run |
-| Nextcloud Tasks | Not available in this environment | Not run |
-
-The source tags in the reference baseline are protocol references only. They
-must not be reported as tested versions. This candidate is not live-provider
-verified until the versioned results below are completed on the designated QA
-installation.
-
-## Release verification gates
-
-Use disposable QA accounts and the configuration in
-[live_provider_testing.md](live_provider_testing.md). Record Server, Calendar
-and Tasks versions, source revision, OS, commands and results. Required live
-checks remain: root/subdirectory authentication and reconnect, all owner/shared/
-delegated/federated/subscription roles, permission changes while queued, actual
-invitation/reply/cancellation delivery without duplicates, free/busy, repeating
-task completion and subtree moves in Tasks, silent imports, and object/collection
-trash restoration including collisions and retention expiry.
-
-Run Linux and Windows native builds and desktop interaction on their respective
-hosts. Linux widget tests of Fluent controls are **not** Windows build or desktop
-evidence. Check keyboard navigation, small windows, light/dark/high-contrast
-themes, browser launch and 100%, 125%, 150%, 200% scaling. Windows-native and
-live-server verification are external release dependencies, not passed tests.
diff --git a/docs/nextcloud_setup.md b/docs/nextcloud_setup.md
index e4694b54..c6d1f1e6 100644
--- a/docs/nextcloud_setup.md
+++ b/docs/nextcloud_setup.md
@@ -1,198 +1,115 @@
# Nextcloud Calendar and Tasks setup
-BusyMax connects directly to a selected Nextcloud server over CalDAV. It
-synchronizes `VEVENT` calendars and `VTODO` task lists, including mixed
-collections when the server exposes both component types.
+This guide explains how to connect and use a Nextcloud account in BusyMax.
+BusyMax supports Nextcloud calendar collections, task lists, and mixed
+collections that contain both events and tasks.
## Server requirements
-- Enter either the public HTTPS URL used in your browser or Nextcloud's
- standard **primary CalDAV address**. For example, both
- `https://cloud.example.net/nextcloud/` and
- `https://cloud.example.net/nextcloud/remote.php/dav` are accepted.
-- BusyMax recognizes `/remote.php/dav` (including a copied calendar-specific
- DAV path), removes the DAV suffix, and starts Login Flow v2 at the actual
- Nextcloud installation root. You do not need to edit a primary CalDAV
- address before pasting it.
-- The URL must use HTTPS, contain no username/password, and pass normal
- platform certificate validation.
-- This release does not support HTTP-only servers, private-CA exceptions, an
- invalid-certificate toggle, or arbitrary non-Nextcloud CalDAV accounts. A
- standard Nextcloud CalDAV address is supported as an input shortcut.
-
-Reverse proxies must preserve the installation path and return the canonical
-public server URL. BusyMax accepts only same-origin Login Flow redirects that
-remain within that path contract.
-
-## Connect with the default browser
+Enter either the public HTTPS address used in your browser or Nextcloud's
+**primary CalDAV address**. For example, both of these forms are accepted:
+
+```text
+https://cloud.example.net/nextcloud/
+https://cloud.example.net/nextcloud/remote.php/dav
+```
+
+A copied calendar-specific DAV path is also normalized back to the Nextcloud
+installation. The address must use HTTPS, contain no username or password, and
+pass normal platform certificate validation. HTTP-only servers, private-CA or
+invalid-certificate exceptions, arbitrary CalDAV servers, and incorrectly
+configured reverse-proxy paths are not supported.
+
+## Connect in the browser
1. In BusyMax, open **Add account** and choose **Nextcloud**.
-2. Enter the server URL or paste **Copy primary CalDAV address**, then select
- **Continue in browser**.
-3. BusyMax anonymously starts Nextcloud Login Flow v2 and opens the login URL
- in your system's default browser.
-4. Sign in there, complete any two-factor or identity-provider step, and grant
- BusyMax access.
-5. Return to BusyMax. It polls the one-time endpoint until Nextcloud returns
- the canonical server, `loginName`, and a dedicated app password.
-
-BusyMax never asks for or stores the user's primary Nextcloud password. The
-polling token and browser URL are treated as secrets and excluded from logs.
-Nextcloud documents that the polling token is valid for 20 minutes and a
-successful credential result is returned only once. Canceling the BusyMax
-dialog cancels local polling.
-
-## Calendars, task lists, and capabilities
-
-BusyMax inventories the account's DAV collections and exposes event and task
-views only when the advertised component set permits them. It uses ACL
-privileges as the primary writability signal:
-
-- content-read-only sources remain visible; read, write-content, bind, unbind,
- and write-properties privileges are evaluated separately. A reader may be
- allowed to rename or recolor their collection without editing its objects;
-- calendar and task fields are enabled only when the relevant collection and
- component capabilities are present;
-- a task shared with the account remains editable only when the collection is
- writable and its classification is `PUBLIC`; classification itself cannot be
- changed by the recipient;
-- unsupported scheduling or collection operations stay disabled rather than
- being guessed from the server brand.
-
-For writable Nextcloud task lists, BusyMax supports the same task data model as
-the official Nextcloud Tasks 0.18.1 editor: start and due values, all-day state,
-status, percentage complete, completion time, iCalendar priority, description,
-categories, location, URL, classification, multiple alarms, recurrence,
-subtasks, pinning, and subtask-visibility flags. It also supports recursive
-duplicate and delete, raw iCalendar export, clear-completed, task ordering,
-and moving a complete task subtree between writable Nextcloud lists.
-
-The account inventory includes the authenticated home and returned delegated
-principal/home contexts. Subscriptions cached by Nextcloud remain Nextcloud
-sources; BusyMax enables the advertised `nc-calendar-webcal-cache` extension
-and never fetches the upstream subscription with account credentials. Scheduling
-inboxes, outboxes, deleted calendars and trash containers are not normal sources.
-An incomplete discovery does not mark cached sources missing.
-
-### Collection settings and sharing
-
-On Linux and Windows, use the existing calendar/task-list menu's **Collection
-settings** action. Settings include display name, color, server order,
-description, calendar timezone, server-enabled state and scheduling transparency
-when write-properties is granted. Unsupported properties can still be rejected
-by the server; BusyMax checks each PROPPATCH property result. Event transparency,
-collection availability, local visibility and sidebar order are separate.
-
-Event-calendar creation uses VEVENT-only MKCALENDAR. Task-list creation keeps
-the existing VTODO-only extended MKCOL path. Both are online operations; no
-optimistic calendar is created before server confirmation. A mixed collection
-remains one backing resource: owner deletion removes **both events and tasks**.
-Removing a received share removes the recipient's access, not the owner's data.
-
-The same settings dialog exposes advertised sharing modes, server principal
-search, user/group read or write grants, revocation and publishing. Publishing
-requires confirmation and displays the URL returned by the server. Being a
-writer does not imply sharing administration rights. No Files Sharing API is
-used. A committed write followed by failed refresh is reported as refresh
-pending; do not repeat it. An uncertain outcome requires reconciliation.
-
-### Invitations and availability
-
-Nextcloud meeting actions use discovered calendar-user addresses and the
-selected principal's scheduling outbox privileges, not the account login name.
-An authorized organizer can add/remove required or optional guests. Attendees
-can accept, tentatively accept or decline from event details. Recurring responses
-require an occurrence/series choice; attendee "this and following" is not offered.
-Organizer cancellation and attendee decline/removal have distinct actions.
-
-Meeting changes are queued locally and use Nextcloud's implicit VEVENT
-scheduling during ordinary conditional CalDAV writes. BusyMax does not send a
-second invitation or SMTP message. A locally saved response is **pending**, not
-proof of delivery. After a successful write, the server's canonical resource and
-new ETag are fetched, including reported participant scheduling status. Permission
-and identity changes are rechecked before replay. Schedule-Tag and VTODO
-invitations are not assumed.
-
-**Check guest availability** in the event editor uses the discovered outbox and
-VFREEBUSY. Missing, denied, malformed or failed recipient results mean unknown
-availability, not free time. **Scheduling inbox** in collection settings shows
-cached/fresh messages; acknowledging one removes only the inbox message, not
-the calendar event already processed by Nextcloud.
-
-Federated content follows actual content permissions, but Nextcloud does not
-implicitly schedule federated calendar objects. BusyMax does not offer meeting
-guest changes or RSVP there. Meeting moves are restricted to native MOVE under
-the same scheduling identity; a copy-and-delete move is rejected to avoid
-unintended invitations and cancellations.
-
-### Native import and export
-
-The existing iCalendar import flow offers Nextcloud calendars and task-capable
-destinations. It groups VEVENT/VTODO by component type and UID, keeping recurrence
-exceptions, embedded timezones, alarms, participant parameters and unknown fields.
-An imported scheduling METHOD requires explicit normalization acknowledgement;
-this stores data, not an iTIP invitation or reply. Imports are explicitly silent
-using `X-NC-Scheduling: false`, including offline replay. This header is not applied
-to normal meeting edits.
-
-Existing UIDs are skipped unless **Import as new copies** is selected. BusyMax
-does not overwrite an existing UID implicitly. Copied task parent references are
-rewritten together, queued in dependency order, and cycles or unavailable parents
-are reported. Results are per resource; a rejected item is not counted as saved.
-
-Individual event/task export uses the effective raw resource, including pending
-local edits and the whole recurrence set. **Export collection resources** writes
-separate `.ics` resources into a new directory under a location you choose. This
-preserves calendar-level extensions and embedded timezones without merging
-different definitions with the same TZID. It exports locally cached content;
-synchronize first if you need the latest complete server snapshot.
-
-### Deleted items
-
-Use **Deleted calendars and tasks** in account/source management on either
-desktop. The view uses Nextcloud's advertised CalDAV trash extension, including
-VEVENT, VTODO and deleted calendars/lists, and displays server retention. Restore
-uses the returned deleted-resource href and the discovered restore target, with
-fresh permission and identity checks. An expired or changed entry cannot be
-restored from an old cached identity. Server collisions are not resolved by
-inventing new UIDs.
-
-Ordinary deletion retains Nextcloud's normal trash behavior. **Permanently
-delete** is a separate confirmed action. Restored items disappear from the view
-only after success and refresh; a refresh failure is not a failed restore.
-
-Calendar and task objects are cached locally for offline use. Object writes,
-including cross-list task moves, are queued and use exact ETags when
-connectivity returns. A server-side concurrent edit is merged only when the
-changed fields are provably disjoint; otherwise BusyMax creates an explicit
-conflict for the user. The complete iCalendar resource remains authoritative,
-so unsupported properties and recurrence forms survive edits unchanged.
+2. Enter the server URL and select **Continue in browser**.
+3. Sign in through the default browser, complete any two-factor or
+ identity-provider step, and grant BusyMax access.
+4. Return to BusyMax and wait for connection and collection discovery to
+ finish.
+
+BusyMax uses Nextcloud Login Flow v2. It never asks for the primary Nextcloud
+password; Nextcloud returns a dedicated app password for BusyMax.
+
+## Permissions and collection settings
+
+BusyMax shows the event and task views advertised by each collection and
+enables actions only when the server reports the required permissions.
+Read-only content stays visible. Object editing, property changes, collection
+administration, sharing, and scheduling can have different permissions, so one
+available action does not imply access to every other action.
+
+Open a calendar or task-list menu and choose **Collection settings** to manage
+the supported name, color, description, order, timezone, enabled state, and
+calendar transparency fields. The server can reject individual changes.
+Calendar and task-list creation are online operations.
+
+A mixed collection has one remote owner. **Deleting an owned mixed collection
+removes both its events and its tasks.** Removing a collection shared with you
+removes your access rather than the owner's data.
+
+Task fields and provider differences are documented in the
+[provider support matrix](provider_support_matrix.md).
+
+## Sharing and meetings
+
+Collection settings expose sharing, publishing, and removal controls when
+Nextcloud advertises them and the account has permission. Write access to event
+or task content does not automatically grant sharing administration.
+
+For supported calendars, organizers can manage guests and attendees can
+respond from event details. Recurring responses require the offered
+occurrence-or-series choice. BusyMax also exposes guest availability and the
+scheduling inbox when the server provides them.
+
+Meeting changes synchronize through the selected Nextcloud calendar. **A
+response saved locally is pending work, not proof that the server delivered
+it.** Confirm important invitations, replies, and cancellations with the
+recipient or Nextcloud Calendar. Federation and permission differences can
+disable meeting actions.
+
+## Import and export
+
+The iCalendar import flow accepts events and tasks for compatible Nextcloud
+destinations. An imported scheduling method must be explicitly normalized:
+**importing data stores calendar content; it does not send an invitation or
+reply.** Existing UIDs are skipped unless **Import as new copies** is selected.
+
+Individual export includes the effective local event or task resource.
+**Export collection resources** writes separate `.ics` files to a new
+directory chosen by the user. Collection export uses locally cached content;
+synchronize first when you need the newest server snapshot.
+
+See the [iCalendar and DAV data model](icalendar_data_model.md) for preservation,
+scheduling, and import/export semantics.
+
+## Deleted items
+
+Open **Deleted calendars and tasks** from account or source management to view
+items exposed by Nextcloud's CalDAV trash support. Restoration follows the
+current permissions and destination reported by the server; expired entries or
+collisions can prevent restoration.
+
+Ordinary deletion uses Nextcloud's normal trash behavior. **Permanently
+delete** is a separate, confirmed action. A successful restore is not treated
+as complete until the list refreshes.
## Reconnect, revoke, or remove
-- Use **Reconnect** to repeat Login Flow v2 after an app password is revoked.
- Cached data and pending work remain available while reauthentication is
- required.
-- When a Nextcloud account is removed, BusyMax attempts the official
- authenticated app-password deletion endpoint, then always removes its local
- credential and account data. A network or server failure can prevent remote
- revocation, so check **Personal settings > Security > Devices & sessions**
- and revoke the BusyMax token manually when removal reports a warning.
-- Revoking the BusyMax token in Nextcloud pauses synchronization without
- deleting local pending work.
-
-Official protocol guidance: [Nextcloud Login Flow
-v2](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/LoginFlow/index.html)
-and [Nextcloud WebDAV
-basics](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/WebDAV/basic.html).
-Task behavior is cross-checked against the official
-[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1).
-
-The initial protocol reference is Server **34.0.3** and Tasks **0.18.1**. These
-are reference versions, **not a claim of completed live interoperability**.
-The Calendar app version must be recorded separately in a QA run. See the
-[implementation and verification record](nextcloud_interoperability.md) for
-automated evidence and remaining release checks.
-
-For maintainer instructions covering disposable-server and live-account tests,
-see [Live provider tests](live_provider_testing.md).
+- Use **Reconnect** to repeat browser authorization after the app password is
+ revoked. Cached data and pending work remain available while the account
+ needs authentication.
+- Revoking the BusyMax token in **Personal settings > Security > Devices &
+ sessions** stops future synchronization until reconnection.
+- When removing an account, BusyMax attempts to revoke its app password and
+ then removes the local credential and account data. **Remote revocation can
+ fail even when local removal completes.** If BusyMax reports a warning,
+ remove the token manually in Nextcloud.
+
+Official setup references:
+[Nextcloud Login Flow v2](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/LoginFlow/index.html)
+and
+[Nextcloud WebDAV basics](https://docs.nextcloud.com/server/stable/developer_manual/client_apis/WebDAV/basic.html).
+Maintainers should use [live-provider testing](live_provider_testing.md) before
+making release-specific interoperability claims.
diff --git a/docs/privacy_data_map.md b/docs/privacy_data_map.md
index f947a207..5447f1a2 100644
--- a/docs/privacy_data_map.md
+++ b/docs/privacy_data_map.md
@@ -1,69 +1,112 @@
# Privacy and data map
-BusyMax processes calendar, event, task, account, reminder, and synchronization
-data to provide its user-requested desktop functions. It does not add telemetry
-or analytics.
-
-| Data | Purpose | Local handling | Network destination |
-|---|---|---|---|
-| OAuth access/refresh tokens and DAV app passwords | Authenticate selected accounts | Windows secure-storage backend or Linux portal/keyring backend; removed on sign-out/account removal as applicable | Google, Microsoft, Apple iCloud, or the user's Nextcloud server over HTTPS |
-| OAuth authorization code, PKCE verifier, and state | Complete browser/loopback sign-in | Short-lived in memory; callback binds only to loopback; never logged | Selected provider token endpoint |
-| Calendar/event/task/source identifiers and content, including location text | Display, edit, synchronize, recur, import/export, and work offline | Drift database in the user-writable application-support directory | The account provider selected by the user during normal synchronization; location is calendar/task content and can be synchronized |
-| Provider-native coordinates | Preserve imported/provider `GEO` and Microsoft structured locations, including through synchronization and copies | Native coordinate columns projected from the provider or iCalendar resource | The selected calendar provider during normal synchronization when its format supports coordinates; external application only after activation |
-| Supplemental coordinates (item or provider-series identity, location snapshot, point, source, attribution) | Retain an imported/copied point when a Google event cannot store it natively, including future occurrences of an imported series, and preserve valid existing records through owner reconciliation | A `location_resolutions` row for the exact saved owner or scoped Google provider-series identity and location snapshot; occurrence-specific rows take precedence. A Google series split copies the row without removing the earlier owner only when the new series keeps the snapshot location; ordinary text edits and native-coordinate creation do not author another row | Not uploaded through Google Calendar and not automatically available on another installation; passed to an external application only after activation |
-| User-activated external location destination | Open a saved address, coordinate, or complete HTTP(S) location value | Prepared locally for one handoff; not retained as opening history and does not mutate the saved item | Registered `geo:`/`maps:` handler on Linux; Google Maps in the browser after native-handler failure; default browser on Windows; a supplied complete HTTP(S) link goes to its own host |
-| Reminder schedule rows | Deliver in-process reminders and make actions idempotent | Drift; stopped by explicit Quit | No new scheduling service; Windows toast receives stable opaque IDs only |
-| `.ics` files and export destinations | User-requested import review/export | Read or written only through explicit activation/file selection; external activation never silently imports | None unless the user later synchronizes confirmed content |
-| WebCal URI | User-requested calendar subscription | Validated and confirmed before subscription | The confirmed HTTPS/WebCal calendar endpoint |
-| Local settings | Theme, locale, notification, tray, and startup preferences | User-writable settings storage | None |
-| Feedback category, subject, message, optional reply email, random submission ID, app/version/build/platform | Submit feedback requested by the user | Prepared only for an explicit submission | Configured BusyStack feedback HTTPS endpoint |
-| Optional feedback technical details | Diagnose explicitly reported issues | OS version and application locale only; checkbox is off by default | Same feedback endpoint when the user opts in |
-
-Feedback never silently attaches logs, account/provider content, calendar/task
-content, filenames, screenshots, environment variables, stable device IDs,
-tokens, secrets, or activation payloads.
-
-BusyMax performs no location autocomplete or geocoding and sends no location
-query to a geocoder. Merely typing a location, opening an editor, loading a
-calendar, or synchronizing does not initiate map traffic. Normal calendar/task
-synchronization may still send stored location content to the selected account
-provider.
-
-External opening happens only after activation. BusyMax passes the selected
-saved text or coordinates to the registered external maps handler; if Linux
-native launching fails, or on Windows, it opens a Google Maps search in the
-default browser. A complete HTTP(S) location value is instead passed directly
-to its host. Those external applications and sites can receive the destination
-and related network metadata. BusyMax does not request the device's current
-location. Opening does not edit an event/task, update timestamps or dirty flags,
-or enqueue synchronization.
-
-Existing source and attribution values in `location_resolutions` remain stored
-as provenance. Exact records follow unambiguous owner reconciliation. Google
-series records are scoped by account, calendar, and provider series identity,
-and apply only to an occurrence with the same location snapshot. Records are
-invalidated when the relevant location changes or the item, series, calendar,
-or account is deleted. BusyMax does not bulk-delete older records merely
-because their point also exists natively or their provenance names a retired
-service.
-The removed renderer's former `maps` application-cache child is inactive; the
-database, credentials, and unrelated caches are not cleanup targets.
-
-Windows package declarations are limited to internet client and full-trust
-desktop execution needed by Flutter Win32. BusyMax does not request location,
-microphone, webcam, contacts, broad-filesystem, or private-network capability.
-File import/export uses the selected file or manifest-declared `.ics`
-association. Startup uses the user-controlled package StartupTask.
-
-The Windows privacy-policy and support URLs are build inputs. Production
-validation rejects missing, placeholder, or non-HTTPS values. App disclosures
-and the external privacy page must be reviewed whenever provider calls,
-feedback fields, storage behavior, or package capabilities change.
-Before release, maintainers must update the public privacy page to describe the
-user-requested external handoff and removal of geocoding/tile processing. This
-repository documents that external release action; it does not claim the public
-page has already been published.
-
-Uninstall and package-update retention behavior must be verified with the
-installed MSIX because Windows owns package data lifecycle. BusyMax never writes
-the database or credentials into the immutable MSIX installation directory.
+This is the canonical repository description of data BusyMax stores locally,
+sends to account providers, or hands to external applications. BusyMax does not
+add telemetry or analytics.
+
+## Local storage and provider traffic
+
+| Data | Local handling | Network or external destination |
+|---|---|---|
+| Google/Microsoft OAuth tokens, Apple app-specific password, Nextcloud app password, WebCal subscription secret | Operating-system credential storage on unpackaged Linux and Windows; the strict Snap uses an encrypted Secret portal file | Used only with the selected provider, Nextcloud server, or WebCal endpoint |
+| OAuth authorization code, PKCE verifier, and state | Short-lived in memory during browser sign-in; callbacks are restricted to loopback and secrets are redacted from logs | Selected provider's authorization/token endpoints |
+| Account, calendar, task-list, event, task, attendee, reminder, recurrence, DAV resource, synchronization, pending-operation, and conflict data | Cached and projected in the Drift database under the platform application-support directory | Selected provider during discovery, synchronization, or a user-requested mutation |
+| Local settings | Platform application-support/settings storage | No provider destination |
+| iCalendar imports and exports | Read or written only after file activation or explicit user selection; imports require review | No network destination unless confirmed imported content is later synchronized |
+| WebCal data | Confirmed URL stored as a credential; downloaded calendar content is cached read-only | Confirmed subscription endpoint |
+| Notification schedule and identifier mapping | Stored locally to deliver and deduplicate reminders | Platform notification service as described below |
+| Feedback submission | Prepared only after the user selects **Send feedback** | Configured BusyStack feedback HTTPS endpoint |
+
+Credential protection applies to credential records, not to every local database
+row. Cached calendar/task content and synchronization metadata in
+`busymax.sqlite` are not described as encrypted by BusyMax.
+
+On unpackaged Linux, BusyMax uses the system keyring through its secure-storage
+backend. Windows uses the operating-system credential-storage backend. The
+strict Snap retrieves key material through the XDG Secret portal and stores an
+AES-GCM encrypted credential envelope under its writable data directory. That
+directory is restricted to mode `0700` and the file to `0600`; the `posix`
+dependency applies and repairs those Linux file permissions. Account removal
+deletes the applicable local credential and account data. Remote revocation is
+provider-specific and can require a separate user action.
+
+## Locations and external applications
+
+Ordinary location text is event or task content. Normal synchronization sends
+it to the selected account provider when that provider and object support the
+field. Provider-native Microsoft structured coordinates and iCalendar `GEO`
+values remain part of provider data.
+
+When an imported or copied point cannot be represented by a Google Calendar
+event, BusyMax can retain a supplemental local `location_resolutions` record.
+The record belongs to the exact item or to a scoped Google account, calendar,
+provider-series identity, and matching location snapshot. An occurrence-specific
+record takes precedence. A series split copies supplemental ownership only when
+the new series keeps the location snapshot; the earlier series keeps its own
+record. These supplemental coordinates are not uploaded through Google
+Calendar and do not appear automatically on another BusyMax installation.
+Records are invalidated when the owning location, item, series, calendar, or
+account no longer matches.
+
+BusyMax does not geocode, autocomplete, request device location, display an
+embedded map, or contact a map service merely because a location is typed,
+viewed, saved, or synchronized.
+
+External opening occurs only after the user chooses **Show on map** or
+**Open link**. BusyMax hands the saved address or coordinates to a registered
+`geo:`/maps handler on Linux, with a Google Maps browser fallback, or to the
+default browser on Windows. A complete saved HTTP(S) value opens its own host.
+The receiving application or website can receive the destination and normal
+network metadata. BusyMax does not retain a separate opening history or mutate
+the event/task during this handoff.
+
+## Windows notifications
+
+Windows notification display and activation are separate data flows:
+
+- BusyMax passes the notification title and body to the Windows notification
+ API so Windows can display them.
+- The toast payload and action arguments contain bounded BusyMax identifiers
+ such as the notification schedule, item kind, account, source, and item IDs,
+ together with the stable ID and selected action. They do not contain the
+ notification title/body, account email, credential, authorization code, or
+ token.
+
+Click, Open, Snooze, and Dismiss activation data returns through the packaged
+Windows activation bridge. Linux notifications use the desktop notification
+service to display reminder content and route actions.
+
+## Feedback
+
+Every explicit feedback submission contains:
+
+- a random submission ID;
+- application ID `busymax`, application version, and build number;
+- runtime platform;
+- category, subject, and message; and
+- the optional reply email, or `null` when omitted.
+
+The optional technical-details checkbox is off by default. When selected, it
+adds only the operating-system version and application locale. Feedback does
+not silently attach logs, account/provider content, calendar/task content,
+filenames, screenshots, environment variables, stable device IDs, tokens,
+secrets, or activation payloads.
+
+Developers can override the endpoint through `BUSYSTACK_FEEDBACK_ENDPOINT` as
+described in [Development](development.md).
+
+## Windows package disclosures
+
+The Windows package declares internet client and full-trust desktop execution
+for the Flutter Win32 application. It does not request location, microphone,
+webcam, contacts, broad-filesystem, or private-network capability. Import and
+export use user-selected files or the declared `.ics` association; startup
+uses the user-controlled packaged StartupTask.
+
+Privacy-policy and support URLs are production build inputs. Before release,
+review the disclosures actually published for the artifact against this data
+map. Client code does not establish provider, website, or feedback-service
+retention policies, does not prove that an external privacy page is published,
+and does not establish operating-system cleanup after uninstall. Record those
+as external policy or installed-package verification rather than inferring
+them from the source.
diff --git a/docs/provider_support_matrix.md b/docs/provider_support_matrix.md
index 8dc6cab7..a3399f04 100644
--- a/docs/provider_support_matrix.md
+++ b/docs/provider_support_matrix.md
@@ -1,118 +1,93 @@
-# Provider capabilities
+# Provider support matrix
-This document describes the capabilities exposed by the current source tree.
-Actual write access also depends on the permissions reported by each account
-and collection. Unknown capabilities fail closed; BusyMax does not infer write
-access from a provider name.
+This page is the central comparison of BusyMax's provider capabilities.
+Available actions also depend on account state and the permissions reported for
+a specific calendar or task list. A visible or enabled control is not evidence
+that a workflow has completed live-provider verification.
-| Provider | Calendars | Tasks | Authentication |
+## Services and authentication
+
+| Provider | Calendar service | Task service | Account connection |
|---|---|---|---|
-| Google | Google Calendar | Google Tasks | OAuth desktop client |
-| Microsoft | Microsoft Calendar | Microsoft To Do | OAuth public client |
-| Apple iCloud | iCloud Calendar | Not supported; Apple Reminders is not supported | Apple app-specific password over HTTPS |
-| Nextcloud | `VEVENT` collections | `VTODO` collections | Login Flow v2 in the default browser |
-| WebCal | Read-only calendar subscriptions | Not supported | Subscription URL stored as a secret |
+| Google | Google Calendar | Google Tasks | Browser OAuth with a configured desktop client |
+| Microsoft | Microsoft Calendar | Microsoft To Do | Browser OAuth with a configured public client |
+| Apple | iCloud Calendar | Not supported | Apple Account email and app-specific password |
+| Nextcloud | CalDAV events | CalDAV tasks | Login Flow v2 in the default browser |
+| WebCal | Read-only subscription | Not supported | Confirmed subscription URL |
+
+Apple Reminders, generic CalDAV accounts, Nextcloud Deck, and Nextcloud Notes
+are not supported. Production connections require normal platform TLS
+validation; BusyMax has no HTTP or invalid-certificate mode. The loopback HTTP
+fixtures documented for live tests are test-only.
-## Collection creation
+## Event objects
-| Capability | Google | Microsoft | Nextcloud | Apple iCloud | WebCal |
+| Capability | Google | Microsoft | Apple iCloud | Nextcloud | WebCal |
|---|---|---|---|---|---|
-| Create calendar | Yes | Yes | Yes | No | No |
-| Create task list | Yes | Yes | Yes | No | No |
+| View events | Yes | Yes | Yes | Yes | Yes |
+| Create, edit, delete | Supported | Supported | Writable calendars | Permission-dependent | No |
+| All-day and timed events | Yes | Yes | Yes | Yes | Preserved for display |
+| Recurrence and reminders | Yes | Yes | Preserved and editable subset | Preserved and editable subset | Read-only |
+| Attendee editing | Yes | Yes | No | Permission- and scheduling-dependent | No |
+| Free/busy workflow | Yes | No | No | Permission- and scheduling-dependent | No |
+| Attachments | Supported subset | No | Preserved when present | Preserved when present | Read-only |
-Nextcloud event calendars are created remotely with CalDAV `MKCALENDAR` and
-an event-only (`VEVENT`) component set. Nextcloud task lists continue to use
-their existing `VTODO` collection implementation. This is Nextcloud-specific;
-BusyMax does not claim generic CalDAV collection creation. Apple collection
-creation remains unsupported, and WebCal subscriptions remain read-only.
+DAV-backed events preserve data BusyMax does not edit, including recurrence
+exceptions, alarms, timezones, parameters, and provider extensions. See the
+[iCalendar and DAV data model](icalendar_data_model.md).
-## CalDAV synchronization
+## Task objects
-| Capability | Apple iCloud Calendar | Nextcloud Calendar | Nextcloud Tasks |
+| Capability | Google Tasks | Microsoft To Do | Nextcloud Tasks |
|---|---|---|---|
-| Collection discovery | Supported | Supported | Supported |
-| Initial and incremental synchronization | Supported | Supported | Supported |
-| Offline object create, edit, delete, and replay | Writable collections | Writable collections | Writable collections |
-| Conditional ETag writes and explicit conflict handling | Supported | Supported | Supported |
-| All-day, floating, UTC, and `TZID` date-time values | Supported | Supported | Supported |
-| Recurrence rules and exceptions | Supported | Supported | Supported; see the editable subset below |
-| `VALARM` preservation | Supported | Supported | Supported |
-| Read-only and shared collections | Supported | Supported | Supported |
-| Collection creation | Not supported | Supported online through `MKCALENDAR` | Supported online when permitted |
-| Collection rename, delete, unshare, metadata, color, or order editing | Not supported | Supported online when permitted | Supported online when permitted |
-| Cross-list task moves | Not applicable | Not applicable | Supported between writable Nextcloud task collections |
-| Clear completed tasks | Not applicable | Not applicable | Supported |
-| Sharing, publishing, trash restoration, and native import/export | Not supported | Supported online when permitted | Supported online when permitted |
-| Invitation, scheduling identities, organizer/attendee changes, replies, free/busy, and scheduling inbox | Not supported | Supported when the advertised collection and scheduling permissions allow it | Not applicable |
-| HTTP-only or private-CA servers | Not supported | Not supported | Not supported |
-
-## Nextcloud task data
-
-BusyMax exposes the task data used by the official Nextcloud Tasks 0.18.1
-editor:
-
-- title, description, categories, start, due date, and all-day state;
-- status, completion percentage, completion date and time, and iCalendar
- priority from 0 through 9;
-- location, URL, and `PUBLIC`, `CONFIDENTIAL`, or `PRIVATE` classification;
-- parent-child relationships, recursive subtasks, and Nextcloud's numeric task
- order, including its `CREATED`-based fallback;
-- multiple reminders, including absolute and before-start/before-due triggers;
-- daily, weekly, monthly, and yearly recurrence with interval, supported
- day/month selectors, count, or end date;
-- pinning and the Nextcloud subtask-visibility flags;
-- recursive duplication, raw iCalendar export, recursive deletion, moving a
- subtree between lists, and deleting Nextcloud closed root task trees.
-
-Completing a recurring task creates a completed recurrence instance and
-advances the master task when another occurrence exists. Recurrence rules that
-the editor cannot represent remain intact and read-only. BusyMax also preserves
-unknown properties, parameters, duplicate properties, unsupported alarm
-actions, sibling components, and time-zone definitions instead of rebuilding
-the resource from the visible fields.
-
-The parity reference is the official
-[Nextcloud Tasks 0.18.1 source](https://github.com/nextcloud/tasks/tree/v0.18.1).
-The wire format and DAV operations follow
-[RFC 5545](https://www.rfc-editor.org/rfc/rfc5545),
-[RFC 4791](https://www.rfc-editor.org/rfc/rfc4791),
-[RFC 4918](https://www.rfc-editor.org/rfc/rfc4918), and
-[RFC 5689](https://www.rfc-editor.org/rfc/rfc5689).
-
-## Nextcloud collaboration and administration
-
-Nextcloud collections expose their server-advertised metadata, sharing and
-publishing controls, trash restoration, and native iCalendar import/export.
-Calendar collections additionally support scheduling identities, organizer and
-attendee changes, replies, free/busy, and scheduling-inbox workflows when the
-server grants the relevant capabilities. Linux and Windows present these
-Nextcloud-specific controls. These workflows have automated coverage, but
-still require the live-server verification listed in
-`nextcloud_interoperability.md` before a release claim.
-
-## Locations and external opening
-
-BusyMax keeps ordinary location text and opens a saved destination only after
-the user chooses **Show on map** or **Open link**. Linux first uses a registered
-maps handler and falls back to a browser; Windows uses the browser. Complete
-HTTP(S) location values open directly. There is no embedded map, geocoding,
-autocomplete, mapping account, or mapping credential.
-
-Microsoft structured coordinates and iCalendar `GEO` remain provider-native.
-For an imported or copied point that Google Calendar cannot represent, BusyMax
-stores only local supplemental data. Recurring-series data is scoped by
-account, calendar, actual provider series identity, and matching location
-snapshot; an occurrence-specific point has precedence. A Google **This and
-following** split retains the earlier series association and copies it to the
-new series only when the location is unchanged. Ordinary native-coordinate
-creation does not create a supplemental duplicate.
-
-## Deliberate boundaries
-
-- Generic CalDAV account setup
-- Apple Reminders
-- Nextcloud Deck or Notes
-- Editing an individual detached task occurrence directly; synchronized
- exceptions are preserved, and recurring completion is handled on the master
-- EventKit or private Apple APIs
-- HTTP fallback or invalid-certificate exceptions
+| Create, edit, complete, delete | Yes | Yes | Permission-dependent |
+| Due date | Date only | Date and time | Date or date-time |
+| Start date/time | No | Yes | Yes |
+| Reminder | No | Single provider reminder | Multiple iCalendar alarms |
+| Recurrence | No | Provider recurrence subset | BusyMax/Nextcloud editable subset |
+| Importance/categories | No | Yes | Yes |
+| Hierarchy | Yes | Yes | Yes |
+| Move between lists | Yes | Not exposed | Between writable Nextcloud lists |
+| Status/progress/completed time | Completion only | Provider completion state | iCalendar status, percentage, and completion time |
+| Location, URL, classification | No | No | Yes |
+| Advanced iCalendar fields | No | No | Priority, alarms, recurrence, pinning, and subtask visibility |
+
+Google exposes assigned and hidden tasks from its API, but provider rules can
+limit which of those tasks BusyMax may change. Nextcloud recurrence rules or
+alarm forms outside BusyMax's editable subset remain preserved and read-only;
+detached task occurrences are not edited directly.
+
+## Collection administration
+
+Object editing and collection administration are separate capabilities.
+
+| Capability | Google | Microsoft | Apple iCloud | Nextcloud | WebCal |
+|---|---|---|---|---|---|
+| Create calendar | Yes | Yes | No | Online, with permission | No |
+| Rename/recolor/delete calendar | Yes | Yes | No | Online, property/owner permission-dependent | No |
+| Create task list | Yes | Yes | No | Online, with permission | No |
+| Rename/delete task list | Yes | Yes | No | Permission-dependent | No |
+| Share or publish collections | No | No | No | Advertised, permission-dependent | No |
+| Restore deleted objects/collections | No | No | No | Advertised, permission-dependent | No |
+| Native collection import/export | No | No | No | Supported subset | No |
+
+Deleting an owned mixed Nextcloud collection removes both its events and tasks.
+Removing a received share removes the recipient's access. Nextcloud scheduling,
+sharing, publishing, trash, and native import/export behavior is described in
+[Nextcloud setup](nextcloud_setup.md) and the
+[technical reference](icalendar_data_model.md).
+
+## Offline and verification boundaries
+
+All providers cache calendar/task data locally for viewing. Queued offline
+changes are supported where the provider adapter and collection permissions
+allow them; collection administration and several Nextcloud collaboration
+operations require a live server. Conflicting DAV writes use conditional
+requests and can require user resolution.
+
+Deterministic tests cover projections, provider adapters, DAV preservation,
+permission handling, mutation queues, conflicts, and UI state. Real account
+authorization, provider delivery, server/version interoperability, installed
+desktop integration, and permission changes still require the
+[live-provider test guide](live_provider_testing.md) and the applicable release
+checklist.
diff --git a/docs/time_format_validation.md b/docs/time_format_validation.md
new file mode 100644
index 00000000..eb886bfb
--- /dev/null
+++ b/docs/time_format_validation.md
@@ -0,0 +1,134 @@
+# Time-format correction
+
+Validated against this checkout with Flutter 3.47.2, Dart 3.13.2,
+intl 0.20.2, and fluent_ui 4.16.1. The reported Linux picker, parser,
+Fluent picker, schedule boundary, and Windows tray-cache defects were present.
+
+The distinction between a locale default and forced 12-hour formatting is
+confirmed by Flutter's [formatTimeOfDay contract](https://api.flutter.dev/flutter/material/MaterialLocalizations/formatTimeOfDay.html).
+Windows locale changes use the `intl` category of
+[WM_SETTINGCHANGE](https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-settingchange).
+
+## Implementation
+
+- One persisted System / 12-hour / 24-hour preference uses the existing settings
+ mutation, loading, persistence and error-handling machinery. Both settings
+ interfaces expose it alongside schedule display settings.
+- `lib/src/l10n/time_format.dart` owns context-independent formatting, explicit
+ 12-hour patterns, localized periods, component conversion and strict user
+ parsing. Its inherited scope sits above each application navigator. No time
+ zone conversion takes place in the formatter.
+- The Linux anchored popover has a keyboard-accessible period selector and
+ canonical hour stepping. Focused text retains its original parsing locale;
+ incomplete components retain their parsing mode until they can normalize.
+ Valid changes remain live, and parent-rejected changes restore the accepted
+ value. Nullable clearing, disabled controls, time zones and validation remain
+ connected to their editors.
+- All ten Windows picker sites use `WindowsTimePicker`. The vendored 4.16.1
+ Fluent patch fixes midnight/noon labels, wheel labels, padding, period
+ conversion and controller synchronization. Format changes retain pending
+ selections, including initially empty pickers. Unchanged confirmation does
+ not dirty an editor. See [vendoring notes](../third_party/fluent_ui/README.busymax.md).
+- Both trays use the shared policy. The Windows cache includes locale and
+ effective clock; callbacks fetch the current formatting snapshot after data
+ loading. Relevant changes refresh the actual tray presentation.
+- Schedule boundaries preserve 0 versus 1440. The 12-hour end boundary includes
+ a translated end-of-day qualifier. Rulers measure localized labels with the
+ active text scale, and gutter-dependent resize positioning uses the same
+ width. A small HoursPainter subclass fixes the upstream refusal to repaint
+ changed labels when canvas dimensions stay the same.
+- The Windows runner reads the user clock preference at initialization and on
+ locale-setting messages/reactivation. It notifies Dart only on changes and
+ retains existing message processing. It reads the first preferred
+ `LOCALE_SSHORTTIME` pattern, interpreting unquoted `h`/`H` tokens and doubled
+ apostrophes; it does not infer the short clock from the long-time setting.
+ Linux continues using Flutter's existing
+ settings path; the pinned engine's settings handler, GNOME settings and portal
+ sources were inspected for clock propagation and change subscriptions.
+- The optional GTK picker remains opt-in. Its request supplies format and
+ translated component/period labels; native input and output remain canonical
+ `HH:mm`. Native tests exercise strict whole-input parsing, conversions and
+ the actual GTK controls. Confirmation validates the entry text before
+ accepting a response. Invalid components survive GTK focus-out/update without
+ clamping or replacement by the old value; the dialog stays open with the
+ application's localized validation label. Correction and cancellation retain
+ the existing canonical-time/null-result contract. The Linux workflow runs
+ these dialog interaction tests under Xvfb.
+
+The short-time source and first-entry rule follow Microsoft's
+[LOCALE_SSHORTTIME documentation](https://learn.microsoft.com/en-us/windows/win32/intl/locale-sshorttime);
+literal handling follows its
+[time format picture rules](https://learn.microsoft.com/en-us/windows/win32/intl/hour--minute--and-second-format-pictures).
+
+## Display audit
+
+The supplied inventory was updated, including schedule rulers/current-time
+labels, event ranges, agenda/details, previews, reminders, DAV timestamps,
+Nextcloud proposals/availability, schedule boundaries, quiet hours and trays.
+Existing date-only/all-day presentation and each caller's zone conversion are
+retained. Truncated Text widgets keep their full semantic text; existing event
+and compact-item tooltips retain complete clock labels.
+
+Final searches under `lib/src` classify the remaining occurrences as follows:
+
+| Search | Remaining use |
+| --- | --- |
+| `DateFormat.jm`, `add_jm`, `formatTimeOfDay`, direct `.format(context)` | None |
+| `DateFormat.Hm` | Shared 24-hour formatting and discovery of the locale's input separator |
+| `alwaysUse24HourFormat` | Original platform reads and presentation overrides at the two roots; a scope fallback for isolated widgets/tests |
+| Raw hour/minute padding | Canonical settings storage, task/provider date-time strings, Graph serialization, iCalendar/recurrence export |
+| `TimeOfDay(hour: 24)` | Calendar layout end boundary; explicitly displayed as midnight |
+
+Provider/storage parsing still requires canonical `HH:mm`. User parsing accepts
+localized clock output, supported localized digits, whitespace variants and
+English AM/PM case variants. It validates hour/minute ranges before period
+conversion. Unmarked `2:30` remains 02:30; single-digit minute input such as
+`9:5` retains its prior unambiguous interpretation. No notification payload or
+schedule calculation was changed.
+
+## Verification
+
+The SDK verifier passed for the workflow pins. Localization output was
+regenerated with `flutter gen-l10n`; generated classes were not edited manually.
+Static analysis, formatting, platform-boundary checks and the Linux release
+build passed. The full suite was rerun after the native follow-up corrections
+and passed with 2,189 tests and 10 skips; all 52 focused clock/picker tests also
+passed.
+
+Regression coverage includes all 1,440 minutes in both formats for every exposed
+locale, malformed period input, native-digit input, English examples, German
+forced 12-hour output, end boundaries, actual Linux/Windows picker interactions,
+open-control format changes, keyboard period selection, RTL/scaled Linux input,
+settings persistence/loading/failure, immediate Windows tray label refresh,
+Linux root system updates without router replacement, ruler repainting and
+calendar gestures. Task/event tests retain draft state, canonical times, dates,
+time zones, recurrence and absolute alarms across format changes.
+
+The native GTK executable compiled with `-Wall -Wextra -Werror` and passed under
+Xvfb. Windows clock reader/message tests are included in the existing runner
+native-test target used by `tool/windows/run_native_tests.ps1`.
+The follow-up native tests cover opposed short/long Windows clocks in both
+directions through an injected locale reader, and quoted/escaped literals and
+preferred-pattern ordering through a portable parser test. The portable test
+passed on this Linux host too. The GTK dialog tests exercise invalid hour/minute text before
+and after update, focus-out and activation, repeated confirmation, correction,
+and cancellation. Explicit BusyMax modes remain covered by the shared clock
+resolution tests and are independent of the native System source.
+
+### Outstanding platform verification
+
+Windows native compilation, the Windows workflow for this uncommitted checkout,
+and interactive system-clock changes in new installed Snap/MSIX packages were
+not executed. The installed Linux Snap is 0.2.0 and was not replaced; testing it
+would not verify this patch. The Linux release bundle build and widget/native
+tests do not establish packaged desktop behavior.
+
+For release acceptance, use the checked-in Linux and Windows packaging
+workflows. In each installed package, switch the OS between 12 and 24 hours
+while BusyMax is open and while it is inactive, then reactivate it. In System
+mode, verify the calendar, an open picker/editor and tray all update. Repeat
+with explicit 12/24-hour preferences and German/English application language;
+explicit preferences must stay fixed. Check midnight/noon, schedule end-of-day,
+RTL and larger text, and confirm unchanged editors without producing mutations.
+On Windows, deliberately oppose the short- and long-time clock conventions in
+both directions and verify System mode follows the short-time preference.
diff --git a/docs/windows_architecture.md b/docs/windows_architecture.md
index 96e29876..4c86646b 100644
--- a/docs/windows_architecture.md
+++ b/docs/windows_architecture.md
@@ -1,119 +1,120 @@
# Windows architecture
-BusyMax has one domain and data model with two desktop compositions.
+BusyMax shares its domain and data layers while keeping separate Linux and
+Windows desktop compositions.
```text
-domain / providers / repositories / Drift / sync / OAuth / recurrence
+providers / repositories / Drift / sync / OAuth / recurrence
|
- common desktop interfaces
- / \
- Linux composition Windows composition
- MaterialApp.router FluentApp.router
- Yaru + GTK/DBus/XDG Fluent + Win32/MSIX
+ BusyMax desktop interfaces
+ / \
+ Linux composition Windows composition
+ Material + Yaru Fluent
+ GTK/DBus/XDG Win32/MSIX
```
-`lib/main.dart` delegates to `lib/main_linux.dart` to preserve the existing
-Linux default. Windows build commands explicitly select `lib/main_windows.dart`.
-`LinuxBusyMaxApp` retains Material/Yaru, Ubuntu localization delegates, and the
-GTK header-bar integration. `WindowsBusyMaxApp` owns the Fluent root and does
-not import the Linux composition.
-
-## Dependency boundaries
-
-Common application, domain, repository, synchronization, OAuth, recurrence,
-database, and scheduling code can depend on Flutter primitives and BusyMax
-types. It cannot import Yaru, Fluent UI, Ubuntu/GTK/DBus/XDG implementations, or
-Windows runner adapters. Platform code translates native/package types into
-BusyMax-owned models at the boundary.
-
-The injected desktop interfaces are:
-
-- `DesktopWindowService`
-- `DesktopTrayService`
-- `DesktopAutostartService`
-- `DesktopNotificationBackend`
-- `DesktopActivationService`
-- `SystemAppearanceSource`
-- `LocalTimeZoneSource`
-
-The automated policy is implemented by `tool/check_platform_boundaries.dart`
-and runs in Linux and Windows CI.
-
-`tray_manager` 0.5.3 and `flutter_timezone` 5.1.0 are pinned through committed
-Windows-only packaging of their upstream plugins. This prevents Flutter's
-generated Linux plugin graph from registering a second AppIndicator tray or
-replacing the existing Linux timezone path. The boundary check requires both
-plugins only in the Windows graph. Their Dart APIs, Windows sources, versions,
-and upstream licenses are preserved.
-
-## Window and single instance
-
-The Win32 runner uses the standard Windows title bar, caption buttons, system
-menu, maximize/restore behavior, and per-monitor-v2 DPI awareness. Flutter
-content begins below the non-client title bar. Minimum tracking size is
-900x600 logical pixels and the initial content size is 1280x800.
-
-One process per Windows user owns a named mutex containing that user's SID.
-Later processes validate a bounded activation, connect to a local named pipe
-whose DACL grants only the current SID, forward a length-prefixed UTF-8 message,
-and wait for a one-byte acceptance acknowledgment before exiting. Connection
-retries cover the bounded interval between the primary acquiring its mutex and
-creating its first pipe instance. The secondary allows the primary process to
-take foreground focus and never starts Dart or Drift. The primary acknowledges
-only after it validates and queues the activation until the method channel is
-ready. No second package owns window lifecycle.
-
-The native single-instance IPC accepts only exact runner-generated normal,
-`--start-minimized`, `.ics`, `webcal:`, and bounded notification JSON shapes. It
-rejects unsupported kinds, actions, schemes, files, fields, malformed
-UTF-8/JSON, NULs, and oversized data; Dart independently validates the common
-activation model again.
-
-Toast callbacks normally arrive through the notification plugin's registered
-COM activator in the primary process. If Windows starts an activation-server
-process while the primary process already owns the mutex, the runner starts a
-private, hidden forwarding mode instead of the application composition. That
-mode initializes only the notification callback, starts no providers or Drift
-database, forwards the validated stable-ID activation through the same
-current-user pipe, and exits. This preserves notification actions without ever
-letting a second process open BusyMax data.
-
-## Lifecycle and tray
-
-The Windows tray adapter uses `tray_manager`; Linux retains its DBus/XDG
-adapter. Shared presentation state supplies Open BusyMax, new event, new task,
-today/agenda, synchronize, Settings, and Quit. Close hides only when the setting
-is enabled and the tray is available. A failed tray causes the sole window to
-remain visible. The icon and menu are periodically reapplied so Explorer's
-notification-area restart can recover.
-
-Explicit Quit stops reminder and synchronization schedulers, destroys the tray,
-disposes the Windows notification backend, closes Drift, and finally requests
-native termination. Closing without background mode exits through the normal
-runner lifecycle.
-
-## Activation routes
-
-Manifest `.ics` and `webcal` activations enter the same single-instance bridge.
-An `.ics` file always opens the existing review and confirmation flow; it is
-never imported silently. A `webcal:` URI opens the existing subscription
-confirmation. Cold and warm delivery use the same common activation model.
-
-Windows toast activation uses `flutter_local_notifications` with stable AUMID
-and committed CLSID `7B854A6D-8B2A-45A5-B998-1F51EC5A81D7`. Toast arguments
-contain only stable internal IDs. Body, Open, Snooze, and Dismiss callbacks are
-routed to the idempotent scheduler action handler; event and task text is not
-placed in activation arguments.
+`lib/main.dart` delegates to `lib/main_linux.dart` for the existing Linux
+default. Windows commands explicitly select `lib/main_windows.dart`.
+`LinuxBusyMaxApp` owns the Material/Yaru root and GTK integration;
+`WindowsBusyMaxApp` owns `FluentApp.router` and does not import the Linux
+composition.
+
+## Shared and platform boundaries
+
+Provider clients, repositories, synchronization, recurrence, database, account
+state, editor drafts, and scheduling controllers are shared. Platform adapters
+translate native or package-specific values into BusyMax-owned models.
+
+The common desktop interfaces cover window, tray, startup, notifications,
+activation, system appearance, and local timezone behavior. Windows and common
+code cannot import Yaru, Ubuntu localization, DBus, freedesktop notification,
+XDG tray, or GTK services. Shared business and Linux code cannot import Fluent
+UI. `tool/check_platform_boundaries.dart` enforces these dependency rules.
+
+Windows registers the vendored `tray_manager` and `flutter_timezone` plugins;
+Linux retains its existing XDG tray and Linux timezone implementation. See the
+[vendoring index](../third_party/README.md) for versions and local packaging
+notes.
+
+## Fluent composition
+
+The Windows shell uses `NavigationView` for Schedule, Tasks, and Settings.
+Calendar and task-list sources remain content panes and collapse at
+BusyMax-controlled widths. Windows screens use Fluent controls and Fluent
+System Icons while repositories, provider state, editor drafts, and mutation
+controllers remain shared.
+
+The Windows-specific day/week adapter renders shared schedule data through
+`infinite_calendar_view` and supplies Fluent colors, controls, and
+interactions around that planner. Linux UI, Ubuntu localization delegates, and
+Yaru chrome do not enter the Windows application graph.
+
+Theme selection follows the saved system/light/dark preference. The Windows
+composition applies the system accent, opaque high-contrast colors, reduced
+motion, Segoe UI Variable with the system Segoe UI fallback, Flutter text
+scaling, locale direction, focus traversal, tooltips, and semantics.
+
+The Win32 runner owns the standard non-client title bar, caption buttons,
+system menu, maximize/restore behavior, and per-monitor DPI response. Flutter
+does not draw duplicate window controls. The runner starts at 1280x800 logical
+pixels and enforces a 900x600 logical minimum.
+
+## Window ownership and single instance
+
+One process per Windows user owns a named mutex containing the user's SID.
+Later processes validate a bounded activation, forward it through a
+current-user-only named pipe, wait for acceptance, and exit before starting
+Dart or Drift. The primary queues accepted activation until its method channel
+is ready.
+
+The native bridge accepts only runner-generated normal launch,
+`--start-minimized`, `.ics`, `webcal:`, and bounded notification shapes.
+It rejects unsupported actions, schemes, files, fields, malformed data, and
+oversized input. Dart validates the shared activation model again.
+
+If Windows starts a toast activation server while the primary is running, the
+runner enters a hidden forwarding mode. It initializes only the notification
+callback, forwards the validated activation through the same pipe, and exits.
+No secondary process opens BusyMax data.
+
+## Tray and lifecycle
+
+Windows uses `tray_manager`; Linux uses the XDG/DBus tray adapter. Shared
+presentation state supplies the menu actions. Close hides the window only when
+background operation is enabled and a tray is available; a tray failure keeps
+the only window visible. The Windows adapter reapplies its icon and menu after
+Explorer notification-area restarts.
+
+Explicit Quit stops reminder and synchronization schedulers, destroys the
+tray, disposes notifications, closes Drift, and requests native termination.
+Closing when background mode is disabled follows the runner's normal exit path.
+
+## Activation and notifications
+
+Manifest `.ics` and `webcal:` activations use the single-instance bridge. An
+iCalendar file enters the existing review and confirmation flow and is never
+silently imported. A WebCal URI enters subscription confirmation. Cold and
+warm activation share the same model.
+
+Windows notifications use a stable package AUMID and the committed toast
+activator CLSID. Notification title and body are passed to Windows for display.
+Activation arguments contain only bounded BusyMax identifiers and action
+information. Body, Open, Snooze, and Dismiss callbacks reach the idempotent
+scheduler action handler.
## Startup and timezone
-The MSIX declares one disabled-by-default `BusyMaxStartupTask`. The runner uses
-the packaged StartupTask API and preserves enabled, disabled, user-disabled,
-policy-disabled, and unpackaged states. There is no registry or Startup-folder
-fallback.
+The MSIX declares `BusyMaxStartupTask` disabled by default. The Windows
+adapter uses the packaged StartupTask API and distinguishes enabled, disabled,
+user-disabled, policy-disabled, and unpackaged states. There is no registry or
+Startup-folder fallback.
-`flutter_timezone` supplies the Windows system IANA zone. Recognized IANA values
-are used directly; a deterministic Windows-ID mapping covers defensive fallback
-cases. An unrecognized or unavailable result records a non-sensitive diagnostic
-and falls back to `Etc/UTC`, never an assumed local zone. Timezone search uses
-BusyMax's generic IANA catalog, not GWeather.
+`flutter_timezone` supplies the Windows system IANA zone. Recognized IANA
+values are used directly; a deterministic Windows-ID mapping handles fallback
+values. An unavailable or unknown zone records a non-sensitive diagnostic and
+uses `Etc/UTC`. Windows timezone search uses BusyMax's IANA catalog rather
+than the Linux GWeather source.
+
+Native rendering, activation, DPI, accessibility, and installed-package
+verification belong in the
+[Windows release checklist](windows_release_checklist.md).
diff --git a/docs/windows_development.md b/docs/windows_development.md
deleted file mode 100644
index 1c4389e1..00000000
--- a/docs/windows_development.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Windows development
-
-BusyMax supports Windows 11 24H2 (`10.0.26100.0`) or newer on x64. Windows 10,
-x86, ARM64, MSI, and EXE installers are intentionally outside the target.
-Flutter and Dart must come from the repository's pinned Flutter 3.47.2 SDK,
-which bundles Dart 3.13.2.
-
-## Prerequisites
-
-- A 64-bit Windows build host for compilation and unsigned packaging. The
- GitHub workflow uses `windows-latest`.
-- 64-bit Windows 11 24H2 or newer for installing, exercising, signing, and
- WACK-validating the release-equivalent package.
-- Flutter 3.47.2 with Windows desktop enabled.
-- Visual Studio 2022 with **Desktop development with C++**, including the x64
- MSVC toolchain and CMake tools.
-- Windows SDK `10.0.26100.0` or newer.
-- Pester 5 (CI pins 5.7.1) for packaging and WACK parser contracts.
-- Developer Mode for unpackaged development, or a local test certificate for
- installed MSIX testing.
-
-Check the build toolchain from a PowerShell prompt at the repository root.
-This requires SDK 10.0.26100.0 or newer and never lowers the MSIX manifest's
-Windows 11 24H2 minimum:
-
-```powershell
-flutter config --enable-windows-desktop
-.\tool\windows\check_prerequisites.ps1
-```
-
-The prerequisite check selects the first `flutter` application on `PATH`,
-normalizes that path, and invokes the Dart executable under that SDK's
-`bin\cache\dart-sdk\bin`. It does not select an unrelated global Dart or
-require Flutter and Dart to have the same immediate parent directory.
-
-## Entry points
-
-Entrypoints are always explicit. The Windows application must never be built
-from the Linux default entrypoint.
-
-```powershell
-flutter pub get --enforce-lockfile
-flutter gen-l10n
-dart run build_runner build --delete-conflicting-outputs --force-jit
-flutter run -d windows -t lib/main_windows.dart `
- --dart-define=BUSYMAX_WINDOWS_AUMID=BusyStack.BusyMax.Development `
- --dart-define=GOOGLE_OAUTH_CLIENT_ID= `
- --dart-define=MICROSOFT_OAUTH_CLIENT_ID=
-```
-
-Location fields require no map-service configuration. Windows opens a saved
-location through the default browser using a Google Maps search URL, or opens a
-complete saved HTTP(S) link directly; BusyMax does not embed a map or geocoder.
-Microsoft structured coordinates and iCalendar `GEO` remain native provider
-data. Local supplemental rows are authored only when an imported/copied Google
-event or series would otherwise lose its point; they are neither a search
-history nor a Google-synchronized extension. Series reuse is account-,
-calendar-, provider-series-, and location-snapshot-scoped.
-
-An unpackaged build accurately reports Windows StartupTask as unavailable. It
-does not create a registry or Startup-folder fallback. Windows notifications
-are also deliberately unavailable without installed package identity, and the
-Settings page shows that limitation. Display, cancellation, and action
-callbacks must be exercised from a locally installed test-signed MSIX.
-
-Linux continues to use:
-
-```bash
-flutter run -d linux -t lib/main_linux.dart
-flutter build linux --release -t lib/main_linux.dart
-```
-
-## Source validation
-
-Run these before requesting Windows CI:
-
-```powershell
-flutter pub get --enforce-lockfile
-flutter gen-l10n
-dart run build_runner build --delete-conflicting-outputs --force-jit
-dart format --set-exit-if-changed .
-flutter analyze
-flutter test
-dart run tool/check_platform_boundaries.dart
-```
-
-Both platform workflows run automatically for pull requests targeting `main`
-and pushes to `main`; pushes to other branches do not independently start
-either workflow. The Windows workflow can still be started manually with
-`workflow_dispatch` on a selected branch.
-
-For a given workflow and ref, a newer run cancels the superseded run without
-affecting the other platform or unrelated pull requests. CI artifacts are
-retained for seven days. Pull requests still build and validate the package,
-but the Windows package artifact is retained only for successful `main` pushes
-and manual runs.
-
-The boundary checker fails if Windows/common code reaches Yaru, Ubuntu
-localizations, DBus, freedesktop notifications, XDG tray, or GTK services, or
-if business code reaches Fluent UI.
-
-## Baseline recorded before the port
-
-The original Linux tree was checked with Flutter 3.44.4 using `flutter pub
-get`, `flutter gen-l10n`, `dart run build_runner build
---delete-conflicting-outputs --force-jit`, the formatting check, `flutter
-analyze`, `flutter test`, and `flutter build linux --release`. Dependency
-resolution, generation, formatting, analysis, and the Linux release build
-succeeded. The test run recorded 1,508 passing tests, 10 skipped tests, and five
-pre-existing failures: one Nextcloud recurrence parse/serialize case and four
-account-add routing widget cases. This is a baseline record, not a waiver for
-CI; the release workflows require a fully passing current test suite.
-
-The Drift schema version at that baseline was 13. A later location-data
-migration established schema 14; removing embedded maps does not roll it back
-or delete its coordinate columns and `location_resolutions` records. Existing
-migrations, the Linux Snap configuration, and the Linux workflow remain in
-place.
-
-## Current source-side validation
-
-On 2026-09-10, the PR 15 release-blocker fixes and toolchain update through
-source revision `c2e0e29` were validated on Linux with
-`/home/albert/flutter-sdk/bin/flutter` 3.47.2 and its bundled
-`/home/albert/flutter-sdk/bin/cache/dart-sdk/bin/dart` 3.13.2. The completion
-report records the final documentation revision:
-
-| Command | Result |
-| --- | --- |
-| SDK verifier | Passed; the selected Flutter executable, bundled Dart executable, normalized SDK roots, and exact 3.47.2/3.13.2 versions matched. |
-| `flutter pub get --enforce-lockfile` | Passed; the Flutter 3.47.2 resolution matched the committed lockfile without changing it. The same command, localization generation, and build-runner generation also left a clean checkout of `c2e0e29` unchanged. |
-| `flutter gen-l10n` | Passed; every supported catalog generated. |
-| `dart run build_runner build --delete-conflicting-outputs --force-jit` | Passed; generated Drift content remained consistent and the schema version remained 14. The pinned build runner reported that the legacy delete-conflicting option is ignored. Its analyzer supports BusyMax's Dart 3.12 language constraint; the generator also reports that the newer bundled Dart 3.13 language is not yet enabled for source analysis. |
-| `dart format --output=none --set-exit-if-changed .` | Passed; 528 files checked, zero changes required. |
-| `flutter analyze` | Passed; no issues found. |
-| `dart run tool/check_platform_boundaries.dart` | Passed. |
-| Toolchain/workflow regressions | Passed; 9 tests cover SDK selection, wrapper and bundled-executable layouts, use of the verified executables by later build steps, and both workflow contracts. |
-| `flutter test --concurrency=1 --reporter failures-only --file-reporter json:build/linux/test-results/flutter-tests-flutter-3.47.2.jsonl` | Passed; 1,975 tests passed, 10 credential-gated live tests skipped with explicit reasons, and zero failed. Report SHA-256: `05aa4c6c7104fe121dce048943412b4c38dba33b4b03437e20805613904e080b`. |
-| `flutter build linux --release -t lib/main_linux.dart` | Passed; produced `build/linux/x64/release/bundle/busymax`. The completion report records the rebuilt artifact's SHA-256. |
-| Snap packaging and installation | Not run for this revision by owner instruction. No Snap was built, installed, uploaded, or published as part of the toolchain update. |
-
-These results establish Linux and platform-neutral source health only. They do
-not replace the Windows gates below, successful Linux and Windows workflow runs
-for the final revision, installed-package testing, or credential-gated live
-provider checks.
-
-## Windows-only verification
-
-Compilation, native bridge validation, installed-package activation, package
-identity notifications, StartupTask, screenshots, and WACK cannot be certified
-from Linux. Run the checklist in [windows_release_checklist.md](windows_release_checklist.md)
-on a clean Windows 11 host and retain the resulting artifacts.
diff --git a/docs/windows_packaging.md b/docs/windows_packaging.md
index 1800a4d8..8b6fc460 100644
--- a/docs/windows_packaging.md
+++ b/docs/windows_packaging.md
@@ -1,119 +1,88 @@
# Windows MSIX packaging
-BusyMax produces an x64 Microsoft Store-mode MSIX. The scripts never upload or
-deploy it. The production artifact is unsigned locally because Microsoft Store
-signing is separate from local test signing.
+BusyMax produces an x64 Microsoft Store-mode MSIX for Windows 11 24H2 or newer.
+The scripts build and inspect the package but never upload or deploy it.
+Production Store artifacts remain unsigned locally; Store signing and local
+test signing are separate workflows.
-## Identity configuration
+## Store configuration
-Copy `config/windows_store.example.json` to the ignored path
-`config/windows_store.local.json` and replace every placeholder with values the
-owner obtained for this product:
+Copy `config/windows_store.example.json` to the ignored
+`config/windows_store.local.json` and replace every placeholder with the
+owner's registered values:
-- package identity name;
-- Publisher CN;
-- publisher display name;
-- four-part Store package version;
-- previous package version when monotonic comparison is wanted;
+- package identity name, Publisher CN, and publisher display name;
+- four-part Store package version and optional previous version;
- privacy-policy, support, and homepage HTTPS URLs;
-- Google desktop OAuth client ID/configuration;
-- Microsoft public client ID and authority tenant;
-
-Do not commit the local file. Identity values are not secrets, but they must
-match the final product registration exactly. OAuth tokens, authorization
-codes, certificates, PFX files, and passwords must never enter configuration or
-the package.
-
-No mapping credential is a build input. Saved locations are handed to the
-default browser as Google Maps searches, while complete HTTP(S) location values
-are opened directly. No embedded map or address-search service is packaged.
-Provider-native coordinates remain calendar data. Supplemental points retained
-for otherwise-lossy Google imports/copies stay in the local database and are not
-embedded in a Google event or package configuration.
-
-The product version remains in `pubspec.yaml`. `msixVersion` is independent and
-must have four numeric components, a first component of at least 1, a fourth
-component of 0, and components no greater than 65535. If a previous version is
-provided, the new value must be greater. CI deliberately uses non-production
-`1.0.0.0`; it is not an owner production version.
-
-The scripts enforce three non-interchangeable validation modes:
-
-- `ProductionStore` is selected by the normal release build. It requires the
- owner's final identity and `production=true`.
-- `CiNonProduction` is selected only by `-Ci`. It requires
- `production=false` and the exact committed CI identity used by the Windows
- workflow. An owner or production configuration is rejected.
-- `LocalTestSigning` is selected by the certificate and installation scripts.
- It accepts the owner's complete production identity so the local certificate
- subject can exactly match the Publisher rendered into the package. It never
- applies CI identity rules.
-
-Every mode rejects placeholders, fake data, development backends, invalid
-Store versions, and incomplete inputs. Production and local-test-signing modes
-also require production HTTPS and OAuth configuration.
-
-## Build
-
-From a 64-bit Windows 11 PowerShell prompt:
+- Google desktop OAuth client ID and client secret;
+- Microsoft public client ID and authority tenant; and
+- `production=true`, `fakeData=false`, and `developmentBackend=false`.
+
+Never commit the local file. Identity and desktop client values are embedded
+configuration, not protected server-side secrets. Tokens, authorization codes,
+certificates, PFX files, and passwords must never enter the configuration or
+package.
+
+The application version remains in `pubspec.yaml`. `msixVersion` is a
+separate Store version with four numeric components. The first must be at least
+1, the fourth must be 0, each must be at most 65535, and a supplied previous
+version must compare lower. CI uses the non-production version `1.0.0.0`;
+that is not the owner's release version.
+
+Configuration validation has three modes:
+
+- **ProductionStore** is the normal release build and requires the final owner
+ identity with `production=true`.
+- **CiNonProduction** is selected by `-Ci` and accepts only the committed
+ workflow's non-production identity.
+- **LocalTestSigning** accepts a complete production identity so the local test
+ certificate can match the Publisher rendered into the package.
+
+All modes reject placeholders, fake data, development backends, invalid
+versions, and incomplete input. Production and local-test-signing modes require
+production HTTPS URLs and complete OAuth configuration.
+
+## Build and package
+
+Use a 64-bit Windows 11 PowerShell prompt:
```powershell
.\tool\windows\build_release.ps1 `
-ConfigPath config\windows_store.local.json
```
-The script checks Windows, Flutter 3.47.2 with bundled Dart 3.13.2, Visual
-Studio x64 C++ tools, and a
-Windows 11 SDK; resolves dependencies; generates localization and Drift code;
-checks committed generation, formatting, analysis, tests, architecture
-boundaries, PowerShell contracts, native runner/plugin tests, and release
-inputs; builds `lib/main_windows.dart`; stages release runtime files; renders
-and validates the manifest; packs the MSIX; and prints its path, metadata, and
-SHA-256. It performs no network deployment.
-
-`MaxVersionTested` is taken from the Windows SDK selected by the prerequisite
-check, so the manifest cannot claim a newer SDK than the release builder used.
+The script verifies Flutter 3.47.2 and bundled Dart 3.13.2, Visual Studio x64
+C++ tools, and Windows SDK 10.0.26100.0 or newer. It runs source generation,
+formatting, analysis, Dart/Flutter and PowerShell tests, platform-boundary and
+native tests, builds `lib/main_windows.dart`, stages runtime files, validates
+the manifest, packages the MSIX, unpacks the exact artifact, compares file
+inventories, and prints metadata and SHA-256. It performs no deployment.
-The manifest is rendered reproducibly from
+The manifest is rendered from
`tool/windows/AppxManifest.xml.template`. It declares x64, minimum OS
-`10.0.26100.0`, only internet client and full-trust desktop capabilities, `.ics`,
-`webcal`, StartupTask, toast activation, resource languages derived from ARB
-catalogs, and BusyMax visual assets. It contains no location, microphone,
-webcam, contacts, broad-filesystem, or private-network capability.
-
-Toast activation uses the base `desktop:Extension` and
-`desktop:ToastNotificationActivation` schema. Its CLSID is validated against
-both the COM class and the notification backend constant. The obsolete
-`desktop4` namespace is neither declared nor accepted for this extension.
-
-`msix` remains pinned as the repository's Store packaging tool dependency, but
-its generated configuration does not express BusyMax's complete combination of
-StartupTask parameters, full-trust toast COM activation, and resource-derived
-languages. The release script therefore follows the documented deterministic
-fallback: stage the Flutter release, render the committed manifest template,
-validate it, and invoke the selected Windows SDK's `MakeAppx`. No generated
-output is hand-edited.
-
-Runner, tray, file-association, and package logo variants are derived from the
-existing `assets/branding/busymax-logo.png`; no separate product mark is used.
-After changing that source branding asset, regenerate and review all committed
-Windows assets with:
+`10.0.26100.0`, internet-client and full-trust desktop capabilities,
+`.ics` and `webcal` activation, a disabled-by-default StartupTask, toast COM
+activation, localized resources, and BusyMax assets. `MaxVersionTested` comes
+from the Windows SDK selected by the prerequisite check.
+
+The repository keeps the `msix` Dart dependency pinned, but the release path
+uses the selected Windows SDK's `MakeAppx` because the committed manifest
+contains the complete StartupTask, toast activation, and language declarations.
+The script stages the Flutter release, renders and validates the manifest, and
+runs `MakeAppx pack /v`; no generated package output is hand-edited.
+
+The package includes a whitelisted set of version-matched x64 Visual C++
+runtime DLLs from the selected Visual Studio installation. Branding variants
+come from `assets/branding/busymax-logo.png`. After changing that source,
+regenerate and review the committed Windows assets:
```powershell
dart run tool/generate_windows_assets.dart
```
-The Store package deliberately stages concrete, unqualified PNG files for all
-four manifest logo paths. Scale-qualified source variants remain committed for
-review and possible future PRI-based packaging, but this packaging path does
-not place them in the MSIX. `validate_package_contents.ps1` rejects a staged
-`.scale-*` or `.targetsize-*` asset and confirms every logo reference resolves
-to a real file after the exact MSIX is unpacked. Consequently this path does
-not require or pretend to contain `resources.pri`.
-
-The startup extension passes `--start-minimized` through its schema-valid
-`uap10:Parameters` attribute. The nested `desktop:StartupTask` remains disabled
-by default; that element does not define an arguments attribute.
+The current MakeAppx staging path uses the unqualified PNG files referenced by
+the manifest. It rejects scale- or target-size-qualified staged variants and
+does not require `resources.pri`.
## Local test signing and installation
@@ -128,52 +97,46 @@ For installed-package testing only:
-PfxPath build\windows\test-signing\busymax-test-only.pfx
```
-The password is prompted, never stored. PFX/CER files and the signed copy stay
-under ignored build output. The installer prints a package-specific uninstall
-command for cleanup. Both scripts validate in `LocalTestSigning` mode. They
-verify the generated/imported certificate subject against the configured
-Publisher, and the installer unpacks the actual MSIX and verifies the same
-subject against its `AppxManifest.xml` before signing. The unsigned Store-mode
-artifact must not depend on this development certificate.
-
-## Validation
-
-The staging step includes only the whitelisted, version-matched x64 Visual C++
-redistributable DLLs from the selected Visual Studio installation, so a clean
-machine does not need developer tools or a preinstalled VC runtime.
-
-`validate_manifest.ps1` enforces architecture, OS target, extensions,
-stable application/executable identity, StartupTask defaults, the exact `.ics`
-and `webcal` registration set, capabilities, and placeholder removal.
-`validate_package_contents.ps1` rejects source, PFX/CER, logs, databases, test
-data, fake data, debug symbols, Linux/Snap content, extra executables, and
-template branding while checking required Flutter assets and DLLs.
-
-Validation is not limited to staging. After `MakeAppx` packs the artifact, the
-packaging script unpacks that exact MSIX into a new clean directory, reruns the
-manifest/content checks, writes a per-file SHA-256 inventory, and compares the
-unpacked inventory with the deterministic staging inventory (excluding only
-the package metadata files produced by `MakeAppx`). Both inventories are CI
-artifacts. `MakeAppx pack /v` is also the mandatory Windows SDK schema and
-package validation gate; a successful custom XML check alone cannot produce an
-artifact.
-
-After installing a release-equivalent locally signed package, run:
+The certificate script creates a local self-signed certificate and ignored
+PFX/CER files, then prompts for a password. The installer imports the
+certificate, verifies its subject against both configuration and the unpacked
+MSIX manifest, signs a copied `BusyMax-test-signed.msix`, trusts it for the
+current user, installs it, and prints package and certificate cleanup commands.
+Never reuse or distribute the local test certificate as a production signing
+credential.
+
+## Package validation
+
+`validate_manifest.ps1` checks architecture, OS target, identity, extensions,
+StartupTask defaults, capabilities, language/resources, and placeholder
+removal. `validate_package_contents.ps1` checks required Flutter, plugin,
+SQLite, VC runtime, and asset files while rejecting source, certificates, logs,
+databases, tests, fake data, debug symbols, Linux content, and unexpected
+executables.
+
+After packing, `package_store.ps1` unpacks the exact MSIX to a clean managed
+directory, validates it again, records SHA-256 file inventories, and compares
+the payload with staging except for MakeAppx package metadata. Retain the final
+manifest, inventories, package metadata, and artifact checksum.
+
+## WACK
+
+After installing the release-equivalent test-signed package, run:
```powershell
.\tool\windows\run_wack.ps1 `
-PackagePath build\windows\store\BusyMax-test-signed.msix
```
-Retain the XML report. Any native runner, dependency, manifest, or packaging
-change invalidates an earlier WACK result and requires a repeat. See the
-[release checklist](windows_release_checklist.md) for behavioral coverage.
-
-`run_wack.ps1` parses both `OVERALL_RESULT` and every per-test `RESULT`; a zero
-process exit code or a report file by itself is not accepted. Missing, empty,
-malformed, result-free, or failing reports stop the release. When WACK emits a
-warning, create a local JSON object whose keys exactly match the warning names
-printed by the script and whose values are the written release dispositions,
-then pass it with `-WarningDispositionPath`. The script refuses warnings with
-missing, placeholder, or empty dispositions and emits `wack-summary.json`
-beside the complete XML report.
+Retain the complete XML report and generated `wack-summary.json`. The script
+requires a passing overall result and passing per-test results; a zero process
+exit code or report file alone is insufficient.
+
+For warnings, create a local JSON object whose keys exactly match the reported
+warning names and whose values contain the written release disposition. Pass
+it with `-WarningDispositionPath`. Empty, placeholder, or missing
+dispositions fail validation. Repeat WACK after any native runner, dependency,
+manifest, or package-content change.
+
+Use the [Windows release checklist](windows_release_checklist.md) for installed
+behavior, privacy review, accessibility, DPI, and release evidence.
diff --git a/docs/windows_release_checklist.md b/docs/windows_release_checklist.md
index be019e2c..2a88a8bf 100644
--- a/docs/windows_release_checklist.md
+++ b/docs/windows_release_checklist.md
@@ -1,105 +1,156 @@
# Windows release checklist
-This checklist is evidence-driven. Source review or a Linux build cannot mark
-the Windows package ready. Record host OS/build, display setup, package version,
-MSIX filename/SHA-256, commands, results, screenshots, and WACK report beside
-the release candidate.
+Use this as the single verification checklist for a Windows release. Source
+review and Linux tests do not establish native Windows behavior.
+
+Start a release record with the source revision, Windows edition/build and
+hardware, display/DPI setup, Flutter and Windows SDK versions, configuration
+mode, MSIX filename/version/SHA-256, CI run and artifact identity, and the
+person/date responsible for each result.
## Automated gates
-- [ ] Clean checkout uses Flutter 3.47.2 and its bundled Dart 3.13.2.
-- [ ] Localization and build-runner generation are committed and reproducible.
-- [ ] Formatting, analyzer, unit tests, widget tests, and platform-boundary
- checks pass on Linux and `windows-latest`.
-- [ ] Linux release builds explicitly from `lib/main_linux.dart`; Snap checks
- remain green and Yaru/GTK behavior is unchanged.
-- [ ] Windows x64 release builds explicitly from `lib/main_windows.dart`.
-- [ ] CI creates only the explicitly non-production unsigned test MSIX. Test
- reports upload on every run; the MSIX and package evidence upload only
- after a successful `main` push or manual run, not for pull requests. No
- deployment job exists.
-- [ ] Manifest and package-content validators pass.
-- [ ] Executable runner, timezone, and tray native tests pass on Windows.
-- [ ] The packed MSIX is unpacked to a clean directory; the exact final
- manifest, runtime inventory, and per-file SHA-256 report pass validation.
-
-## Installed package lifecycle
-
-- [ ] Ordinary start, offline start, and `--start-minimized`.
-- [ ] Close-to-tray enabled/disabled, explicit Quit, reopen and restore.
-- [ ] Second launch cannot open Drift and restores the primary instance.
-- [ ] Explorer restart recovers the tray; tray failure leaves a visible window.
-- [ ] Sign-out/restart and launch-at-login behavior.
-- [ ] StartupTask disabled by default, user enable/disable, start minimized,
- user-disabled, policy-disabled, and unpackaged states.
+- [ ] A clean checkout uses Flutter 3.47.2 and its bundled Dart 3.13.2.
+- [ ] Dependency resolution, localization generation, and Drift generation
+ follow [Development](development.md) and leave committed outputs clean.
+- [ ] The non-writing formatting check, analyzer, normal tests, and
+ platform-boundary check pass.
+- [ ] Linux CI still builds `lib/main_linux.dart` and its strict Snap checks
+ pass; Windows CI builds `lib/main_windows.dart`.
+- [ ] Windows Pester packaging contracts and runner, tray, and timezone native
+ tests pass.
+- [ ] Production configuration validation uses the owner identity and provider
+ settings; CI configuration remains explicitly non-production.
+- [ ] Manifest and content validation pass before and after MakeAppx packaging.
+- [ ] The exact unpacked MSIX matches the staged runtime inventory except for
+ expected MakeAppx metadata.
+
+## Installed-package lifecycle
+
+Use a locally test-signed copy of the release-equivalent Store package.
+
+- [ ] Ordinary, offline, and `--start-minimized` launches work.
+- [ ] Close-to-tray on/off, hide, restore, explicit Quit, and reopen work.
+- [ ] A second launch forwards activation and never opens a second Drift owner.
+- [ ] Explorer restart recovers the tray; a tray failure leaves a visible
+ window.
+- [ ] Sign-out, OS restart, and launch-at-login behavior work.
+- [ ] StartupTask starts disabled and correctly represents enable, disable,
+ start-minimized, user-disabled, policy-disabled, and unpackaged states.
+- [ ] Package update preserves supported local data and account state; package
+ removal behavior is observed and recorded rather than inferred.
+- [ ] A clean Windows 11 24H2 x64 system without developer tools installs and
+ runs the package. Exercise any newer Windows 11 release claimed by the
+ release as well.
## Activation and notifications
-- [ ] Cold, warm, hidden-window, and initialization-time `.ics` activation opens
- review/confirmation without silent import.
-- [ ] Cold/warm/hidden `webcal:` enters subscription confirmation.
-- [ ] Malformed URI/JSON/UTF-8, unsupported file/scheme, oversized input, and
- concurrent activations are safely rejected or serialized.
-- [ ] Notification display, body click, Open, Snooze, Dismiss, duplicate action,
- cancellation, warm/cold activation, hidden window, and package-identity
- behavior pass.
-- [ ] Activation arguments contain stable internal IDs only and no title, body,
- account email, token, code, or secret.
-
-## Accounts, storage, and files
-
-- [ ] Google and Microsoft browser/loopback OAuth pass unpackaged, installed
- test-signed, and final Store-mode configurations, including cancellation,
- timeout, PKCE/state validation, and restored focus.
-- [ ] Secure credentials persist after restart and are removed on sign-out and
- account removal; no plaintext preference/log/payload contains a token.
-- [ ] Existing Drift schema/migrations, SQLite DLLs, restart, package update,
- backup/recovery, Unicode paths, and long paths pass.
-- [ ] `.ics` import/export, overwrite, cancellation, read-only destination,
- Unicode/long filename, malformed iCalendar, and WebCal pass without broad
- filesystem capability.
-- [ ] Recurrence and timezone behavior covers UTC, standard/DST, skipped and
- ambiguous local time, non-hour offsets, and a system-zone change followed
- by restart.
-
-## UI and accessibility
-
-- [ ] Every existing screen has Fluent presentation; no Windows-visible Yaru,
- Ubuntu, Material shell control, or Flutter template icon remains.
-- [ ] Native title bar, caption controls, snap/maximize/restore/system menu.
+- [ ] Cold, warm, hidden-window, and initialization-time `.ics` activation
+ opens review/confirmation and never imports silently.
+- [ ] Cold, warm, and hidden-window `webcal:` activation opens subscription
+ confirmation.
+- [ ] Malformed URI/JSON/UTF-8, unsupported file/scheme/action, oversized
+ input, and concurrent activations are rejected or serialized safely.
+- [ ] Notifications display the intended title and body; body click, Open,
+ Snooze, Dismiss, duplicate action, cancellation, warm/cold activation,
+ hidden window, and installed package identity all work.
+- [ ] Activation arguments contain only bounded identifiers and action data,
+ with no display text, account email, token, code, or secret.
+- [ ] Opening a notification reveals the exact event or task, including an item
+ that arrives after background synchronization.
+- [ ] The tray **Today** action selects today's day view after navigation to a
+ distant date.
+
+## Accounts, storage, files, and synchronization
+
+- [ ] Google and Microsoft browser/loopback sign-in pass in unpackaged and
+ installed configurations, including cancellation, timeout, callback
+ validation, and restored focus.
+- [ ] Apple iCloud and Nextcloud setup, reconnect, remote revocation behavior,
+ read-only permissions, and account removal match their setup guides.
+- [ ] Credentials survive restart in the secure backend and are removed by the
+ applicable sign-out/account-removal action; tokens do not appear in
+ preferences, logs, activation data, or exported evidence.
+- [ ] Existing Drift migrations, SQLite runtime, restart, backup/recovery,
+ Unicode paths, and long paths work.
+- [ ] iCalendar import/export, overwrite/copy choice, cancellation, read-only
+ destinations, malformed input, Unicode/long filenames, and WebCal work
+ without broad filesystem capability.
+- [ ] Timezone handling covers UTC, standard/DST, skipped and ambiguous local
+ times, non-hour offsets, and a system-zone change followed by restart.
+- [ ] Editing a zoned event without changing its times preserves both endpoint
+ zones, seconds, and instants.
+- [ ] Switching an event to another compatible calendar preserves unrelated
+ recurrence, reminders, meeting data, personal fields, and title edits;
+ switching away and back does not erase the draft.
+- [ ] Schedule and Tasks views refresh after background object, account,
+ source, list, capability, and permission changes.
+- [ ] Provider-supported task completion, recurrence, hierarchy, reparenting,
+ list moves, and subtree behavior persist after refresh and restart.
+
+## Workflow and editor behavior
+
+- [ ] Tasks account/list scope, completed filtering, hierarchy indentation,
+ account-qualified list labels, creation context, and empty/loading/error
+ states behave correctly.
+- [ ] Removing a selected account or list reconciles the active task filter;
+ retry and refresh recover failed account, list, and task queries.
+- [ ] Month view adapts row capacity, preserves source identity/color, and
+ opens all hidden items through the overflow control with keyboard access
+ and larger text.
+- [ ] Year view renders weekday/date grids, item markers, and selectable dates;
+ long-range navigation returns accurately to Today.
+- [ ] Event and task editors preserve unsupported recurrence and alarm data
+ when opened and saved without related edits.
+- [ ] Schedule, URL, recurrence, and destination validation follow the selected
+ provider without destroying fields retained for another destination.
+- [ ] Escape, Back, window close, and hierarchy navigation prompt before
+ discarding unsaved edits, including edits made only to notes or
+ description.
+
+## UI, accessibility, and DPI
+
+- [ ] The Windows shell and dialogs use Fluent presentation; no Yaru or Ubuntu
+ UI enters the Windows graph.
+- [ ] Native title bar, caption controls, system menu, snap, maximize, restore,
+ resize, and minimum-size behavior work.
- [ ] Light, dark, high contrast, reduced motion, system accent changes, text
- scaling, keyboard-only order/focus/tooltips, and basic Narrator.
-- [ ] 100%, 125%, 150%, and 200%; 1280x800 and 1920x1080; mixed-DPI monitors.
-- [ ] Arabic and Persian RTL through navigation, dialogs, and editors; all
- other supported locales open without English Windows placeholders.
-- [ ] Real installed-MSIX light/dark screenshots captured at both target sizes,
- including at least one 125% or 150% run.
-- [ ] Windows 11 24H2 and 25H2 both exercised.
-
-## Final package and WACK
-
-- [ ] Production configuration has exact owner identity/OAuth values, valid
- HTTPS privacy/support URLs, fake data off, and no development backend.
-- [ ] Product version and Store package version are displayed distinctly.
-- [ ] MSIX is x64, minimum `10.0.26100.0`, fourth version segment 0, expected
- capabilities/extensions/languages/assets only, and newer than the prior
- supplied version.
-- [ ] Manual content review finds no certificate/PFX, secret/token, logs,
- database, source, debug symbols, test data, Linux content, unreferenced
- assets, or missing Flutter/plugin/SQLite runtime DLL.
-- [ ] Clean Windows test environment without build tools installs and runs.
-- [ ] WACK finishes with zero failures; every warning is investigated and its
- written disposition, complete XML report, and parsed summary are
- retained. Repeat after package-affecting changes.
-
-## Release record
-
-- [ ] Architecture/dependency summary.
-- [ ] Exact commands and results for Linux, Windows, CI, installed MSIX, and
- WACK.
-- [ ] CI run links/artifact identifiers.
-- [ ] Windows screenshots.
-- [ ] MSIX filename, version, architecture, minimum OS, and SHA-256.
-- [ ] WACK report path/result.
-- [ ] Exact remaining external owner values, if any.
-- [ ] Confirmation that no package was uploaded or deployed.
+ scaling, keyboard-only traversal, visible focus, tooltips, and basic
+ Narrator labels are usable.
+- [ ] Test 100%, 125%, 150%, and 200% scaling, 1280x800 and 1920x1080, and
+ movement between mixed-DPI monitors.
+- [ ] Arabic and Persian RTL work through navigation, dialogs, calendars, and
+ editors; every supported locale opens without Windows-only placeholder
+ text.
+- [ ] Capture installed-MSIX light and dark screenshots at both target sizes,
+ including at least one 125% or 150% run. Include high contrast, RTL,
+ keyboard focus, Month overflow, Year selection, Tasks hierarchy, and
+ populated editors in the evidence.
+
+## Final package, privacy, and WACK
+
+- [ ] The exact production configuration contains the owner identity,
+ production OAuth values, valid HTTPS privacy/support URLs, fake data off,
+ and no development backend.
+- [ ] Application version and four-part Store package version are displayed and
+ recorded distinctly.
+- [ ] The MSIX is x64, targets minimum `10.0.26100.0`, has fourth version
+ component 0, contains only expected capabilities/extensions/languages,
+ and is newer than the previous supplied Store version.
+- [ ] Manual inspection finds no certificate/PFX, secret/token, log, database,
+ source, debug symbol, test/fake data, Linux content, unreferenced asset,
+ or missing Flutter/plugin/SQLite/VC runtime file.
+- [ ] Published privacy disclosures are reviewed against the
+ [privacy and data map](privacy_data_map.md). Record discrepancies as
+ release blockers; do not assume an external page is current.
+- [ ] WACK reports no failure. Investigate every warning and retain its written
+ disposition, complete XML report, and parsed summary.
+- [ ] Repeat package inspection and WACK after package-affecting changes.
+
+## Complete the release record
+
+Record the tested revision, environment, exact artifact identity and checksum,
+commands or checklist cases executed, pass/fail/not-run results, CI links,
+screenshots, WACK files, privacy review, and relevant redacted evidence. List
+remaining external verification or owner-supplied values explicitly. Do not
+mark unexecuted cases as passed.
diff --git a/docs/windows_ui.md b/docs/windows_ui.md
deleted file mode 100644
index 996be80f..00000000
--- a/docs/windows_ui.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Windows UI
-
-Windows uses `FluentApp.router` and Fluent controls throughout the application
-shell. It is a separate presentation target, not the Linux Yaru widget tree in
-a Windows runner.
-
-## Shell and destinations
-
-`NavigationView` maps only the existing Schedule, Tasks, and Settings areas.
-Settings is a footer destination. The source/calendar/task-list sidebar remains
-a content-specific pane and collapses at BusyMax-owned breakpoints so narrow
-windows do not show two permanently expanded sidebars.
-
-The Windows presentation covers sign-in and account addition; schedule day,
-week, month, year, and agenda views; source visibility; task lists/details;
-calendar and task-list creation/rename/removal and reminder/color actions;
-event/task editors; recurrence; reminders; date/time and timezone inputs;
-calendar import and WebCal subscription; settings; account removal;
-diagnostics; feedback; About; third-party licenses; and keyboard shortcuts. The
-view/controller and provider logic remains shared.
-
-Existing-task editing is capability-aware. Microsoft and DAV start, due,
-reminder, recurrence, category/importance and iCalendar fields are patched
-through `TaskDetailsDraft`; unchanged all-day values, unsupported recurrence,
-and multi-alarm data are preserved instead of being normalized by merely
-opening and saving the dialog. The Fluent task dialog also exposes supported
-cross-list moves, hierarchy and checklist mutations, reparenting, DAV
-duplication, and native iCalendar export. Event reminders retain multiple
-Google/DAV values while Microsoft remains limited to its single provider
-reminder. New tasks expose explicit all-day/timed scheduling and retain every
-DAV alarm. Event and task editors confirm before discarding unsaved edits,
-including Escape/back and hierarchy navigation.
-
-BusyMax presentation contracts use semantic glyphs such as calendar, task,
-add, delete, synchronize, reminder, and diagnostics. The Windows mapper uses
-Microsoft Fluent System Icons. Feature code does not request `YaruIcons` or
-Material `Icons` for Windows chrome.
-
-## Theme and accessibility
-
-- System, light, and dark modes follow the persisted BusyMax preference.
-- Accent color follows the Windows system accent.
-- High contrast uses opaque black/white Fluent surfaces.
-- Reduced-motion preference zeroes Fluent animation durations.
-- Segoe UI Variable is requested and Windows supplies its own fallback; no
- Microsoft font is bundled.
-- Native text scaling, keyboard traversal, focus indicators, tooltips,
- screen-reader semantics, and RTL direction flow through Flutter/Fluent.
-- The supported Windows validation matrix is 100%, 125%, 150%, and 200% display
- scaling, mixed-DPI monitors, 1280x800 and 1920x1080, and Arabic/Persian RTL.
-
-The native title bar remains untouched. There are no Flutter-drawn minimize,
-maximize, restore, or close controls and Mica is not a release dependency.
-
-## Compatibility subtrees
-
-Material controls are not used as Windows application chrome. If a custom or
-third-party calendar subtree requires Material internals, compatibility must be
-narrowly scoped to that subtree and inherit the Fluent brightness, color,
-typography, locale, and direction. Material surfaces must not escape into the
-shell. Ubuntu localization delegates remain Linux-only.
-
-## Visual verification
-
-Widget tests are not substitutes for native rendering. The release checklist
-requires real installed-MSIX screenshots of major screens in light and dark at
-1280x800 and 1920x1080, including a 125% or 150% run. Capture RTL, high-contrast,
-focus, and mixed-DPI observations in the same Windows test record.
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 7ec64f6a..cc071677 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "تنسيق الوقت",
+ "timeFormatTwelveHour": "12 ساعة",
+ "timeFormatTwentyFourHour": "24 ساعة",
+ "timeEndOfDay": "نهاية اليوم",
+ "timePeriod": "الفترة",
+ "taskRecurrenceDestinationUnsupported": "لا يمكن استخدام نمط التكرار هذا في هذه القائمة. غيّر النمط أو اختر قائمة أخرى.",
+ "taskRecurrenceRequiresDate": "أضف تاريخ بدء أو استحقاق لاستخدام نمط التكرار هذا، أو أوقف التكرار.",
+ "launchAtLoginReadFailed": "تعذر تحديد حالة التشغيل عند تسجيل الدخول.",
+ "launchAtLoginUnavailable": "التشغيل عند تسجيل الدخول غير متاح على هذا النظام.",
+ "windowsStartupEnabledByPolicy": "فعّل المسؤول التشغيل عند بدء النظام ولا يمكن تغييره هنا.",
+ "settingsSaveFailed": "تعذر حفظ الإعدادات. قد تُفقد تغييراتك عند إعادة تشغيل BusyMax.",
"nextcloudExportCollection": "تصدير موارد المجموعة",
"nextcloudImportItems": "تم العثور على {count} من موارد الأحداث والمهام",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index 7137dddc..dcc96092 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Zeitformat",
+ "timeFormatTwelveHour": "12 Stunden",
+ "timeFormatTwentyFourHour": "24 Stunden",
+ "timeEndOfDay": "Tagesende",
+ "timePeriod": "Tageszeit",
+ "taskRecurrenceDestinationUnsupported": "Diese Liste unterstützt dieses Wiederholungsmuster nicht. Ändere das Muster oder wähle eine andere Liste.",
+ "taskRecurrenceRequiresDate": "Füge ein Start- oder Fälligkeitsdatum hinzu, um dieses Wiederholungsmuster zu verwenden, oder deaktiviere die Wiederholung.",
+ "launchAtLoginReadFailed": "Der Status des Starts bei der Anmeldung konnte nicht ermittelt werden.",
+ "launchAtLoginUnavailable": "Der Start bei der Anmeldung ist auf diesem System nicht verfügbar.",
+ "windowsStartupEnabledByPolicy": "Der Autostart wurde von Ihrem Administrator aktiviert und kann hier nicht geändert werden.",
+ "settingsSaveFailed": "Die Einstellungen konnten nicht gespeichert werden. Ihre Änderungen können beim Neustart von BusyMax verloren gehen.",
"nextcloudExportCollection": "Sammlungsressourcen exportieren",
"nextcloudImportItems": "{count} Ereignis-/Aufgabenressourcen gefunden",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index a0ec00e4..74d16664 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -1,4 +1,13 @@
{
+ "timeFormat": "Time format",
+ "timeFormatTwelveHour": "12-hour",
+ "timeFormatTwentyFourHour": "24-hour",
+ "timeEndOfDay": "end of day",
+ "timePeriod": "Day period",
+ "launchAtLoginReadFailed": "Could not determine the launch-at-login state.",
+ "launchAtLoginUnavailable": "Launch at login is unavailable on this system.",
+ "windowsStartupEnabledByPolicy": "Startup is enabled by your administrator and cannot be changed here.",
+ "settingsSaveFailed": "Could not save settings. Your changes may be lost when BusyMax restarts.",
"nextcloudExportCollection": "Export collection resources",
"nextcloudImportItems": "{count} event/task resources found",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
@@ -868,6 +877,8 @@
}
},
"unsupportedRecurrencePreserved": "This recurrence rule uses options that this editor does not change.",
+ "taskRecurrenceDestinationUnsupported": "This list cannot use this repeat pattern. Change the pattern or choose another list.",
+ "taskRecurrenceRequiresDate": "Add a start or due date to use this repeat pattern, or turn off repeat.",
"recurrenceUnsupportedByProvider": "This recurrence cannot be used with {provider}.",
"@recurrenceUnsupportedByProvider": {
"placeholders": {
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 77819cc3..faaec07f 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Formato de hora",
+ "timeFormatTwelveHour": "12 horas",
+ "timeFormatTwentyFourHour": "24 horas",
+ "timeEndOfDay": "fin del día",
+ "timePeriod": "Período del día",
+ "taskRecurrenceDestinationUnsupported": "Esta lista no admite este patrón de repetición. Cambia el patrón o elige otra lista.",
+ "taskRecurrenceRequiresDate": "Añade una fecha de inicio o de vencimiento para usar este patrón, o desactiva la repetición.",
+ "launchAtLoginReadFailed": "No se pudo determinar el estado del inicio automático.",
+ "launchAtLoginUnavailable": "El inicio automático no está disponible en este sistema.",
+ "windowsStartupEnabledByPolicy": "Tu administrador ha activado el inicio automático y no se puede cambiar aquí.",
+ "settingsSaveFailed": "No se pudo guardar la configuración. Los cambios podrían perderse al reiniciar BusyMax.",
"nextcloudExportCollection": "Exportar recursos de la colección",
"nextcloudImportItems": "Se encontraron {count} recursos de eventos/tareas",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb
index 28cf6261..5e37c343 100644
--- a/lib/l10n/app_et.arb
+++ b/lib/l10n/app_et.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Kellaajavorming",
+ "timeFormatTwelveHour": "12-tunnine",
+ "timeFormatTwentyFourHour": "24-tunnine",
+ "timeEndOfDay": "päeva lõpp",
+ "timePeriod": "Päevaosa",
+ "taskRecurrenceDestinationUnsupported": "See loend ei toeta seda kordusmustrit. Muuda mustrit või vali teine loend.",
+ "taskRecurrenceRequiresDate": "Selle kordusmustri kasutamiseks lisa algus- või tähtpäev või lülita kordamine välja.",
+ "launchAtLoginReadFailed": "Sisselogimisel käivitamise olekut ei õnnestunud tuvastada.",
+ "launchAtLoginUnavailable": "Sisselogimisel käivitamine pole selles süsteemis saadaval.",
+ "windowsStartupEnabledByPolicy": "Administraator on automaatkäivituse lubanud ja seda ei saa siin muuta.",
+ "settingsSaveFailed": "Seadeid ei õnnestunud salvestada. Muudatused võivad BusyMaxi taaskäivitamisel kaduda.",
"nextcloudExportCollection": "Ekspordi kogu ressursid",
"nextcloudImportItems": "Leiti {count} sündmuse/ülesande ressurssi",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb
index d2c53e2f..c35fee64 100644
--- a/lib/l10n/app_fa.arb
+++ b/lib/l10n/app_fa.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "قالب زمان",
+ "timeFormatTwelveHour": "۱۲ ساعته",
+ "timeFormatTwentyFourHour": "۲۴ ساعته",
+ "timeEndOfDay": "پایان روز",
+ "timePeriod": "بازهٔ روز",
+ "taskRecurrenceDestinationUnsupported": "این فهرست از این الگوی تکرار پشتیبانی نمیکند. الگو را تغییر دهید یا فهرست دیگری انتخاب کنید.",
+ "taskRecurrenceRequiresDate": "برای استفاده از این الگوی تکرار، تاریخ شروع یا سررسید اضافه کنید یا تکرار را خاموش کنید.",
+ "launchAtLoginReadFailed": "وضعیت اجرای خودکار هنگام ورود مشخص نشد.",
+ "launchAtLoginUnavailable": "اجرای خودکار هنگام ورود در این سیستم در دسترس نیست.",
+ "windowsStartupEnabledByPolicy": "اجرای خودکار توسط مدیر فعال شده و از اینجا قابل تغییر نیست.",
+ "settingsSaveFailed": "تنظیمات ذخیره نشد. ممکن است تغییرات شما با راهاندازی دوبارهٔ BusyMax از بین برود.",
"nextcloudExportCollection": "برونبری منابع مجموعه",
"nextcloudImportItems": "{count} منبع رویداد/کار پیدا شد",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb
index f40e345a..a3cb1bdc 100644
--- a/lib/l10n/app_fi.arb
+++ b/lib/l10n/app_fi.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Aikamuoto",
+ "timeFormatTwelveHour": "12 tuntia",
+ "timeFormatTwentyFourHour": "24 tuntia",
+ "timeEndOfDay": "vuorokauden loppu",
+ "timePeriod": "Vuorokaudenaika",
+ "taskRecurrenceDestinationUnsupported": "Tämä lista ei tue tätä toistomallia. Muuta mallia tai valitse toinen lista.",
+ "taskRecurrenceRequiresDate": "Lisää alkamis- tai määräpäivä käyttääksesi tätä toistomallia tai poista toisto käytöstä.",
+ "launchAtLoginReadFailed": "Kirjautumisen yhteydessä käynnistymisen tilaa ei voitu selvittää.",
+ "launchAtLoginUnavailable": "Käynnistys kirjautumisen yhteydessä ei ole käytettävissä tässä järjestelmässä.",
+ "windowsStartupEnabledByPolicy": "Ylläpitäjä on ottanut automaattisen käynnistyksen käyttöön, eikä sitä voi muuttaa täällä.",
+ "settingsSaveFailed": "Asetuksia ei voitu tallentaa. Muutokset saattavat kadota, kun BusyMax käynnistetään uudelleen.",
"nextcloudExportCollection": "Vie kokoelman resurssit",
"nextcloudImportItems": "Löytyi {count} tapahtuma-/tehtäväresurssia",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index 8d5499ae..5046e9b5 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Format de l’heure",
+ "timeFormatTwelveHour": "12 heures",
+ "timeFormatTwentyFourHour": "24 heures",
+ "timeEndOfDay": "fin de journée",
+ "timePeriod": "Période de la journée",
+ "taskRecurrenceDestinationUnsupported": "Cette liste ne prend pas en charge ce modèle de répétition. Modifiez le modèle ou choisissez une autre liste.",
+ "taskRecurrenceRequiresDate": "Ajoutez une date de début ou d’échéance pour utiliser ce modèle de répétition, ou désactivez la répétition.",
+ "launchAtLoginReadFailed": "Impossible de déterminer l’état du lancement à la connexion.",
+ "launchAtLoginUnavailable": "Le lancement à la connexion n’est pas disponible sur ce système.",
+ "windowsStartupEnabledByPolicy": "Le démarrage automatique est activé par votre administrateur et ne peut pas être modifié ici.",
+ "settingsSaveFailed": "Impossible d’enregistrer les paramètres. Vos modifications risquent d’être perdues au redémarrage de BusyMax.",
"nextcloudExportCollection": "Exporter les ressources de la collection",
"nextcloudImportItems": "{count} ressources d’événements/tâches trouvées",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb
index 82ad1a64..37ebe383 100644
--- a/lib/l10n/app_hi.arb
+++ b/lib/l10n/app_hi.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "समय प्रारूप",
+ "timeFormatTwelveHour": "12 घंटे",
+ "timeFormatTwentyFourHour": "24 घंटे",
+ "timeEndOfDay": "दिन का अंत",
+ "timePeriod": "दिन की अवधि",
+ "taskRecurrenceDestinationUnsupported": "यह सूची इस दोहराव पैटर्न का समर्थन नहीं करती। पैटर्न बदलें या दूसरी सूची चुनें।",
+ "taskRecurrenceRequiresDate": "इस दोहराव पैटर्न का उपयोग करने के लिए शुरू होने या नियत तिथि जोड़ें, या दोहराव बंद करें।",
+ "launchAtLoginReadFailed": "लॉगिन पर शुरू होने की स्थिति का पता नहीं चल सका।",
+ "launchAtLoginUnavailable": "इस सिस्टम पर लॉगिन के समय शुरू करना उपलब्ध नहीं है।",
+ "windowsStartupEnabledByPolicy": "आपके व्यवस्थापक ने स्वचालित शुरुआत चालू की है और इसे यहाँ बदला नहीं जा सकता।",
+ "settingsSaveFailed": "सेटिंग्स सहेजी नहीं जा सकीं। BusyMax फिर से शुरू होने पर आपके बदलाव खो सकते हैं।",
"nextcloudExportCollection": "संग्रह के संसाधन निर्यात करें",
"nextcloudImportItems": "{count} इवेंट/कार्य संसाधन मिले",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index 242492fe..a3024906 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Formato ora",
+ "timeFormatTwelveHour": "12 ore",
+ "timeFormatTwentyFourHour": "24 ore",
+ "timeEndOfDay": "fine giornata",
+ "timePeriod": "Periodo del giorno",
+ "taskRecurrenceDestinationUnsupported": "Questo elenco non supporta questo schema di ripetizione. Modifica lo schema o scegli un altro elenco.",
+ "taskRecurrenceRequiresDate": "Aggiungi una data di inizio o di scadenza per usare questo schema di ripetizione, oppure disattiva la ripetizione.",
+ "launchAtLoginReadFailed": "Impossibile determinare lo stato dell’avvio all’accesso.",
+ "launchAtLoginUnavailable": "L’avvio all’accesso non è disponibile su questo sistema.",
+ "windowsStartupEnabledByPolicy": "L’avvio automatico è attivato dall’amministratore e non può essere modificato qui.",
+ "settingsSaveFailed": "Impossibile salvare le impostazioni. Le modifiche potrebbero andare perse al riavvio di BusyMax.",
"nextcloudExportCollection": "Esporta risorse della raccolta",
"nextcloudImportItems": "Trovate {count} risorse di eventi/attività",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index bf046acf..1105f9cb 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "時刻の表示形式",
+ "timeFormatTwelveHour": "12時間表示",
+ "timeFormatTwentyFourHour": "24時間表示",
+ "timeEndOfDay": "一日の終わり",
+ "timePeriod": "午前・午後",
+ "taskRecurrenceDestinationUnsupported": "このリストではこの繰り返しパターンを使用できません。パターンを変更するか、別のリストを選択してください。",
+ "taskRecurrenceRequiresDate": "この繰り返しパターンを使用するには、開始日または期限を追加するか、繰り返しをオフにしてください。",
+ "launchAtLoginReadFailed": "ログイン時の起動状態を確認できませんでした。",
+ "launchAtLoginUnavailable": "このシステムではログイン時の起動を利用できません。",
+ "windowsStartupEnabledByPolicy": "管理者によって自動起動が有効になっているため、ここでは変更できません。",
+ "settingsSaveFailed": "設定を保存できませんでした。BusyMax を再起動すると変更が失われる可能性があります。",
"nextcloudExportCollection": "コレクションのリソースをエクスポート",
"nextcloudImportItems": "予定/タスクのリソースが {count} 件見つかりました",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index ad1350a9..6d867555 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "시간 형식",
+ "timeFormatTwelveHour": "12시간",
+ "timeFormatTwentyFourHour": "24시간",
+ "timeEndOfDay": "하루의 끝",
+ "timePeriod": "오전/오후",
+ "taskRecurrenceDestinationUnsupported": "이 목록은 이 반복 패턴을 지원하지 않습니다. 패턴을 변경하거나 다른 목록을 선택하세요.",
+ "taskRecurrenceRequiresDate": "이 반복 패턴을 사용하려면 시작일 또는 마감일을 추가하거나 반복을 끄세요.",
+ "launchAtLoginReadFailed": "로그인 시 실행 상태를 확인할 수 없습니다.",
+ "launchAtLoginUnavailable": "이 시스템에서는 로그인 시 실행을 사용할 수 없습니다.",
+ "windowsStartupEnabledByPolicy": "관리자가 자동 시작을 활성화했으며 여기서는 변경할 수 없습니다.",
+ "settingsSaveFailed": "설정을 저장할 수 없습니다. BusyMax를 다시 시작하면 변경 사항이 사라질 수 있습니다.",
"nextcloudExportCollection": "컬렉션 리소스 내보내기",
"nextcloudImportItems": "일정/작업 리소스 {count}개 발견",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index aeb2b121..262365d6 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Format czasu",
+ "timeFormatTwelveHour": "12-godzinny",
+ "timeFormatTwentyFourHour": "24-godzinny",
+ "timeEndOfDay": "koniec dnia",
+ "timePeriod": "Pora dnia",
+ "taskRecurrenceDestinationUnsupported": "Ta lista nie obsługuje tego wzorca powtarzania. Zmień wzorzec lub wybierz inną listę.",
+ "taskRecurrenceRequiresDate": "Dodaj datę rozpoczęcia lub termin wykonania, aby użyć tego wzorca, albo wyłącz powtarzanie.",
+ "launchAtLoginReadFailed": "Nie udało się ustalić stanu uruchamiania przy logowaniu.",
+ "launchAtLoginUnavailable": "Uruchamianie przy logowaniu jest niedostępne w tym systemie.",
+ "windowsStartupEnabledByPolicy": "Administrator włączył automatyczne uruchamianie i nie można tego tutaj zmienić.",
+ "settingsSaveFailed": "Nie udało się zapisać ustawień. Zmiany mogą zostać utracone po ponownym uruchomieniu BusyMax.",
"nextcloudExportCollection": "Eksportuj zasoby kolekcji",
"nextcloudImportItems": "Liczba znalezionych zasobów wydarzeń i zadań: {count}",
"@nextcloudImportItems": {
diff --git a/lib/l10n/app_pt_PT.arb b/lib/l10n/app_pt_PT.arb
index 74a8bd4a..46eed407 100644
--- a/lib/l10n/app_pt_PT.arb
+++ b/lib/l10n/app_pt_PT.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Formato da hora",
+ "timeFormatTwelveHour": "12 horas",
+ "timeFormatTwentyFourHour": "24 horas",
+ "timeEndOfDay": "fim do dia",
+ "timePeriod": "Período do dia",
+ "taskRecurrenceDestinationUnsupported": "Esta lista não suporta este padrão de repetição. Altere o padrão ou escolha outra lista.",
+ "taskRecurrenceRequiresDate": "Adicione uma data de início ou de conclusão para usar este padrão, ou desative a repetição.",
+ "launchAtLoginReadFailed": "Não foi possível determinar o estado do arranque ao iniciar sessão.",
+ "launchAtLoginUnavailable": "O arranque ao iniciar sessão não está disponível neste sistema.",
+ "windowsStartupEnabledByPolicy": "O arranque automático foi ativado pelo administrador e não pode ser alterado aqui.",
+ "settingsSaveFailed": "Não foi possível guardar as definições. As alterações poderão perder-se ao reiniciar o BusyMax.",
"nextcloudExportCollection": "Exportar recursos da coleção",
"nextcloudImportItems": "{count} recursos de eventos/tarefas encontrados",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 63576a42..a4f1a4e2 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Формат времени",
+ "timeFormatTwelveHour": "12 часов",
+ "timeFormatTwentyFourHour": "24 часа",
+ "timeEndOfDay": "конец дня",
+ "timePeriod": "Время суток",
+ "taskRecurrenceDestinationUnsupported": "Этот список не поддерживает такой шаблон повторения. Измените шаблон или выберите другой список.",
+ "taskRecurrenceRequiresDate": "Добавьте дату начала или срок выполнения для этого шаблона либо отключите повторение.",
+ "launchAtLoginReadFailed": "Не удалось определить состояние запуска при входе в систему.",
+ "launchAtLoginUnavailable": "Запуск при входе в систему недоступен в этой системе.",
+ "windowsStartupEnabledByPolicy": "Автозапуск включён администратором, и его нельзя изменить здесь.",
+ "settingsSaveFailed": "Не удалось сохранить настройки. Изменения могут быть потеряны при перезапуске BusyMax.",
"nextcloudExportCollection": "Экспортировать ресурсы коллекции",
"nextcloudImportItems": "Найдено ресурсов событий/задач: {count}",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_vi.arb b/lib/l10n/app_vi.arb
index 9a5962f8..108340a1 100644
--- a/lib/l10n/app_vi.arb
+++ b/lib/l10n/app_vi.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "Định dạng giờ",
+ "timeFormatTwelveHour": "12 giờ",
+ "timeFormatTwentyFourHour": "24 giờ",
+ "timeEndOfDay": "cuối ngày",
+ "timePeriod": "Buổi trong ngày",
+ "taskRecurrenceDestinationUnsupported": "Danh sách này không hỗ trợ kiểu lặp này. Hãy đổi kiểu lặp hoặc chọn danh sách khác.",
+ "taskRecurrenceRequiresDate": "Thêm ngày bắt đầu hoặc hạn chót để dùng kiểu lặp này, hoặc tắt lặp lại.",
+ "launchAtLoginReadFailed": "Không thể xác định trạng thái khởi chạy khi đăng nhập.",
+ "launchAtLoginUnavailable": "Hệ thống này không hỗ trợ khởi chạy khi đăng nhập.",
+ "windowsStartupEnabledByPolicy": "Quản trị viên đã bật tự động khởi động và không thể thay đổi tại đây.",
+ "settingsSaveFailed": "Không thể lưu cài đặt. Các thay đổi có thể bị mất khi BusyMax khởi động lại.",
"nextcloudExportCollection": "Xuất tài nguyên bộ sưu tập",
"nextcloudImportItems": "Tìm thấy {count} tài nguyên sự kiện/công việc",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 90cffcea..ea8614c3 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "时间格式",
+ "timeFormatTwelveHour": "12小时制",
+ "timeFormatTwentyFourHour": "24小时制",
+ "timeEndOfDay": "当天结束",
+ "timePeriod": "上午/下午",
+ "taskRecurrenceDestinationUnsupported": "此列表不支持此重复模式。请更改模式或选择其他列表。",
+ "taskRecurrenceRequiresDate": "请添加开始日期或截止日期以使用此重复模式,或关闭重复。",
+ "launchAtLoginReadFailed": "无法确定登录时启动的状态。",
+ "launchAtLoginUnavailable": "此系统不支持登录时启动。",
+ "windowsStartupEnabledByPolicy": "管理员已启用自动启动,无法在此更改。",
+ "settingsSaveFailed": "无法保存设置。重新启动 BusyMax 时,您的更改可能会丢失。",
"nextcloudExportCollection": "导出集合资源",
"nextcloudImportItems": "找到 {count} 个事件/任务资源",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_zh_Hans.arb b/lib/l10n/app_zh_Hans.arb
index 7d7d274b..7adbcb39 100644
--- a/lib/l10n/app_zh_Hans.arb
+++ b/lib/l10n/app_zh_Hans.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "时间格式",
+ "timeFormatTwelveHour": "12小时制",
+ "timeFormatTwentyFourHour": "24小时制",
+ "timeEndOfDay": "当天结束",
+ "timePeriod": "上午/下午",
+ "taskRecurrenceDestinationUnsupported": "此列表不支持此重复模式。请更改模式或选择其他列表。",
+ "taskRecurrenceRequiresDate": "请添加开始日期或截止日期以使用此重复模式,或关闭重复。",
+ "launchAtLoginReadFailed": "无法确定登录时启动的状态。",
+ "launchAtLoginUnavailable": "此系统不支持登录时启动。",
+ "windowsStartupEnabledByPolicy": "管理员已启用自动启动,无法在此更改。",
+ "settingsSaveFailed": "无法保存设置。重新启动 BusyMax 时,您的更改可能会丢失。",
"nextcloudExportCollection": "导出集合资源",
"nextcloudImportItems": "找到 {count} 个事件/任务资源",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb
index 725b0419..1b16e4fd 100644
--- a/lib/l10n/app_zh_Hant.arb
+++ b/lib/l10n/app_zh_Hant.arb
@@ -1,4 +1,15 @@
{
+ "timeFormat": "時間格式",
+ "timeFormatTwelveHour": "12小時制",
+ "timeFormatTwentyFourHour": "24小時制",
+ "timeEndOfDay": "當天結束",
+ "timePeriod": "上午/下午",
+ "taskRecurrenceDestinationUnsupported": "此清單不支援此重複模式。請變更模式或選擇其他清單。",
+ "taskRecurrenceRequiresDate": "請新增開始日期或截止日期以使用此重複模式,或關閉重複。",
+ "launchAtLoginReadFailed": "無法確定登入時啟動的狀態。",
+ "launchAtLoginUnavailable": "此系統不支援登入時啟動。",
+ "windowsStartupEnabledByPolicy": "管理員已啟用自動啟動,無法在此變更。",
+ "settingsSaveFailed": "無法儲存設定。重新啟動 BusyMax 時,您的變更可能會遺失。",
"nextcloudExportCollection": "匯出集合資源",
"nextcloudImportItems": "找到 {count} 個事件/工作資源",
"@nextcloudImportItems": {"placeholders": {"count": {"type": "int"}}},
diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart
index 641080ff..085a3033 100644
--- a/lib/l10n/generated/app_localizations.dart
+++ b/lib/l10n/generated/app_localizations.dart
@@ -135,6 +135,60 @@ abstract class AppLocalizations {
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'),
];
+ /// No description provided for @timeFormat.
+ ///
+ /// In en, this message translates to:
+ /// **'Time format'**
+ String get timeFormat;
+
+ /// No description provided for @timeFormatTwelveHour.
+ ///
+ /// In en, this message translates to:
+ /// **'12-hour'**
+ String get timeFormatTwelveHour;
+
+ /// No description provided for @timeFormatTwentyFourHour.
+ ///
+ /// In en, this message translates to:
+ /// **'24-hour'**
+ String get timeFormatTwentyFourHour;
+
+ /// No description provided for @timeEndOfDay.
+ ///
+ /// In en, this message translates to:
+ /// **'end of day'**
+ String get timeEndOfDay;
+
+ /// No description provided for @timePeriod.
+ ///
+ /// In en, this message translates to:
+ /// **'Day period'**
+ String get timePeriod;
+
+ /// No description provided for @launchAtLoginReadFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not determine the launch-at-login state.'**
+ String get launchAtLoginReadFailed;
+
+ /// No description provided for @launchAtLoginUnavailable.
+ ///
+ /// In en, this message translates to:
+ /// **'Launch at login is unavailable on this system.'**
+ String get launchAtLoginUnavailable;
+
+ /// No description provided for @windowsStartupEnabledByPolicy.
+ ///
+ /// In en, this message translates to:
+ /// **'Startup is enabled by your administrator and cannot be changed here.'**
+ String get windowsStartupEnabledByPolicy;
+
+ /// No description provided for @settingsSaveFailed.
+ ///
+ /// In en, this message translates to:
+ /// **'Could not save settings. Your changes may be lost when BusyMax restarts.'**
+ String get settingsSaveFailed;
+
/// No description provided for @nextcloudExportCollection.
///
/// In en, this message translates to:
@@ -3120,6 +3174,18 @@ abstract class AppLocalizations {
/// **'This recurrence rule uses options that this editor does not change.'**
String get unsupportedRecurrencePreserved;
+ /// No description provided for @taskRecurrenceDestinationUnsupported.
+ ///
+ /// In en, this message translates to:
+ /// **'This list cannot use this repeat pattern. Change the pattern or choose another list.'**
+ String get taskRecurrenceDestinationUnsupported;
+
+ /// No description provided for @taskRecurrenceRequiresDate.
+ ///
+ /// In en, this message translates to:
+ /// **'Add a start or due date to use this repeat pattern, or turn off repeat.'**
+ String get taskRecurrenceRequiresDate;
+
/// No description provided for @recurrenceUnsupportedByProvider.
///
/// In en, this message translates to:
diff --git a/lib/l10n/generated/app_localizations_ar.dart b/lib/l10n/generated/app_localizations_ar.dart
index b201d0f2..1a12e206 100644
--- a/lib/l10n/generated/app_localizations_ar.dart
+++ b/lib/l10n/generated/app_localizations_ar.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsAr extends AppLocalizations {
AppLocalizationsAr([String locale = 'ar']) : super(locale);
+ @override
+ String get timeFormat => 'تنسيق الوقت';
+
+ @override
+ String get timeFormatTwelveHour => '12 ساعة';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 ساعة';
+
+ @override
+ String get timeEndOfDay => 'نهاية اليوم';
+
+ @override
+ String get timePeriod => 'الفترة';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'تعذر تحديد حالة التشغيل عند تسجيل الدخول.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'التشغيل عند تسجيل الدخول غير متاح على هذا النظام.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'فعّل المسؤول التشغيل عند بدء النظام ولا يمكن تغييره هنا.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'تعذر حفظ الإعدادات. قد تُفقد تغييراتك عند إعادة تشغيل BusyMax.';
+
@override
String get nextcloudExportCollection => 'تصدير موارد المجموعة';
@@ -1899,6 +1930,14 @@ class AppLocalizationsAr extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'تستخدم قاعدة التكرار هذه خيارات لا يغيّرها هذا المحرر.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'لا يمكن استخدام نمط التكرار هذا في هذه القائمة. غيّر النمط أو اختر قائمة أخرى.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'أضف تاريخ بدء أو استحقاق لاستخدام نمط التكرار هذا، أو أوقف التكرار.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'لا يمكن استخدام هذا التكرار مع $provider.';
diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart
index cfe684c6..343efae8 100644
--- a/lib/l10n/generated/app_localizations_de.dart
+++ b/lib/l10n/generated/app_localizations_de.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
+ @override
+ String get timeFormat => 'Zeitformat';
+
+ @override
+ String get timeFormatTwelveHour => '12 Stunden';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 Stunden';
+
+ @override
+ String get timeEndOfDay => 'Tagesende';
+
+ @override
+ String get timePeriod => 'Tageszeit';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Der Status des Starts bei der Anmeldung konnte nicht ermittelt werden.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Der Start bei der Anmeldung ist auf diesem System nicht verfügbar.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Der Autostart wurde von Ihrem Administrator aktiviert und kann hier nicht geändert werden.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Die Einstellungen konnten nicht gespeichert werden. Ihre Änderungen können beim Neustart von BusyMax verloren gehen.';
+
@override
String get nextcloudExportCollection => 'Sammlungsressourcen exportieren';
@@ -1833,6 +1864,14 @@ class AppLocalizationsDe extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Diese Wiederholungsregel verwendet Optionen, die dieser Editor nicht ändert.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Diese Liste unterstützt dieses Wiederholungsmuster nicht. Ändere das Muster oder wähle eine andere Liste.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Füge ein Start- oder Fälligkeitsdatum hinzu, um dieses Wiederholungsmuster zu verwenden, oder deaktiviere die Wiederholung.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Diese Wiederholung kann nicht mit $provider verwendet werden.';
diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart
index cfec7bed..aa8cc416 100644
--- a/lib/l10n/generated/app_localizations_en.dart
+++ b/lib/l10n/generated/app_localizations_en.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
+ @override
+ String get timeFormat => 'Time format';
+
+ @override
+ String get timeFormatTwelveHour => '12-hour';
+
+ @override
+ String get timeFormatTwentyFourHour => '24-hour';
+
+ @override
+ String get timeEndOfDay => 'end of day';
+
+ @override
+ String get timePeriod => 'Day period';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Could not determine the launch-at-login state.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Launch at login is unavailable on this system.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Startup is enabled by your administrator and cannot be changed here.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Could not save settings. Your changes may be lost when BusyMax restarts.';
+
@override
String get nextcloudExportCollection => 'Export collection resources';
@@ -1852,6 +1883,14 @@ class AppLocalizationsEn extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'This recurrence rule uses options that this editor does not change.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'This list cannot use this repeat pattern. Change the pattern or choose another list.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Add a start or due date to use this repeat pattern, or turn off repeat.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'This recurrence cannot be used with $provider.';
diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart
index f44359b3..d46e319d 100644
--- a/lib/l10n/generated/app_localizations_es.dart
+++ b/lib/l10n/generated/app_localizations_es.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsEs extends AppLocalizations {
AppLocalizationsEs([String locale = 'es']) : super(locale);
+ @override
+ String get timeFormat => 'Formato de hora';
+
+ @override
+ String get timeFormatTwelveHour => '12 horas';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 horas';
+
+ @override
+ String get timeEndOfDay => 'fin del día';
+
+ @override
+ String get timePeriod => 'Período del día';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'No se pudo determinar el estado del inicio automático.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'El inicio automático no está disponible en este sistema.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Tu administrador ha activado el inicio automático y no se puede cambiar aquí.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'No se pudo guardar la configuración. Los cambios podrían perderse al reiniciar BusyMax.';
+
@override
String get nextcloudExportCollection => 'Exportar recursos de la colección';
@@ -1838,6 +1869,14 @@ class AppLocalizationsEs extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Esta regla de repetición usa opciones que este editor no modifica.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Esta lista no admite este patrón de repetición. Cambia el patrón o elige otra lista.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Añade una fecha de inicio o de vencimiento para usar este patrón, o desactiva la repetición.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Esta repetición no se puede usar con $provider.';
diff --git a/lib/l10n/generated/app_localizations_et.dart b/lib/l10n/generated/app_localizations_et.dart
index 0a449cf8..055d4941 100644
--- a/lib/l10n/generated/app_localizations_et.dart
+++ b/lib/l10n/generated/app_localizations_et.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsEt extends AppLocalizations {
AppLocalizationsEt([String locale = 'et']) : super(locale);
+ @override
+ String get timeFormat => 'Kellaajavorming';
+
+ @override
+ String get timeFormatTwelveHour => '12-tunnine';
+
+ @override
+ String get timeFormatTwentyFourHour => '24-tunnine';
+
+ @override
+ String get timeEndOfDay => 'päeva lõpp';
+
+ @override
+ String get timePeriod => 'Päevaosa';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Sisselogimisel käivitamise olekut ei õnnestunud tuvastada.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Sisselogimisel käivitamine pole selles süsteemis saadaval.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Administraator on automaatkäivituse lubanud ja seda ei saa siin muuta.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Seadeid ei õnnestunud salvestada. Muudatused võivad BusyMaxi taaskäivitamisel kaduda.';
+
@override
String get nextcloudExportCollection => 'Ekspordi kogu ressursid';
@@ -1818,6 +1849,14 @@ class AppLocalizationsEt extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'See kordusreegel kasutab valikuid, mida see redaktor ei muuda.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'See loend ei toeta seda kordusmustrit. Muuda mustrit või vali teine loend.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Selle kordusmustri kasutamiseks lisa algus- või tähtpäev või lülita kordamine välja.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Seda kordust ei saa teenusega $provider kasutada.';
diff --git a/lib/l10n/generated/app_localizations_fa.dart b/lib/l10n/generated/app_localizations_fa.dart
index 8dd1443c..eaa75e4d 100644
--- a/lib/l10n/generated/app_localizations_fa.dart
+++ b/lib/l10n/generated/app_localizations_fa.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsFa extends AppLocalizations {
AppLocalizationsFa([String locale = 'fa']) : super(locale);
+ @override
+ String get timeFormat => 'قالب زمان';
+
+ @override
+ String get timeFormatTwelveHour => '۱۲ ساعته';
+
+ @override
+ String get timeFormatTwentyFourHour => '۲۴ ساعته';
+
+ @override
+ String get timeEndOfDay => 'پایان روز';
+
+ @override
+ String get timePeriod => 'بازهٔ روز';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'وضعیت اجرای خودکار هنگام ورود مشخص نشد.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'اجرای خودکار هنگام ورود در این سیستم در دسترس نیست.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'اجرای خودکار توسط مدیر فعال شده و از اینجا قابل تغییر نیست.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'تنظیمات ذخیره نشد. ممکن است تغییرات شما با راهاندازی دوبارهٔ BusyMax از بین برود.';
+
@override
String get nextcloudExportCollection => 'برونبری منابع مجموعه';
@@ -1853,6 +1884,14 @@ class AppLocalizationsFa extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'این قانون تکرار از گزینههایی استفاده میکند که این ویرایشگر تغییر نمیدهد.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'این فهرست از این الگوی تکرار پشتیبانی نمیکند. الگو را تغییر دهید یا فهرست دیگری انتخاب کنید.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'برای استفاده از این الگوی تکرار، تاریخ شروع یا سررسید اضافه کنید یا تکرار را خاموش کنید.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'این تکرار را نمیتوان با $provider استفاده کرد.';
diff --git a/lib/l10n/generated/app_localizations_fi.dart b/lib/l10n/generated/app_localizations_fi.dart
index 1e6c8020..b86302cd 100644
--- a/lib/l10n/generated/app_localizations_fi.dart
+++ b/lib/l10n/generated/app_localizations_fi.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsFi extends AppLocalizations {
AppLocalizationsFi([String locale = 'fi']) : super(locale);
+ @override
+ String get timeFormat => 'Aikamuoto';
+
+ @override
+ String get timeFormatTwelveHour => '12 tuntia';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 tuntia';
+
+ @override
+ String get timeEndOfDay => 'vuorokauden loppu';
+
+ @override
+ String get timePeriod => 'Vuorokaudenaika';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Kirjautumisen yhteydessä käynnistymisen tilaa ei voitu selvittää.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Käynnistys kirjautumisen yhteydessä ei ole käytettävissä tässä järjestelmässä.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Ylläpitäjä on ottanut automaattisen käynnistyksen käyttöön, eikä sitä voi muuttaa täällä.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Asetuksia ei voitu tallentaa. Muutokset saattavat kadota, kun BusyMax käynnistetään uudelleen.';
+
@override
String get nextcloudExportCollection => 'Vie kokoelman resurssit';
@@ -1847,6 +1878,14 @@ class AppLocalizationsFi extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Tämä toistosääntö käyttää asetuksia, joita tämä muokkain ei muuta.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Tämä lista ei tue tätä toistomallia. Muuta mallia tai valitse toinen lista.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Lisää alkamis- tai määräpäivä käyttääksesi tätä toistomallia tai poista toisto käytöstä.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Tätä toistoa ei voi käyttää palvelussa $provider.';
diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart
index e34fc1ad..624906d5 100644
--- a/lib/l10n/generated/app_localizations_fr.dart
+++ b/lib/l10n/generated/app_localizations_fr.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
+ @override
+ String get timeFormat => 'Format de l’heure';
+
+ @override
+ String get timeFormatTwelveHour => '12 heures';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 heures';
+
+ @override
+ String get timeEndOfDay => 'fin de journée';
+
+ @override
+ String get timePeriod => 'Période de la journée';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Impossible de déterminer l’état du lancement à la connexion.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Le lancement à la connexion n’est pas disponible sur ce système.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Le démarrage automatique est activé par votre administrateur et ne peut pas être modifié ici.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Impossible d’enregistrer les paramètres. Vos modifications risquent d’être perdues au redémarrage de BusyMax.';
+
@override
String get nextcloudExportCollection =>
'Exporter les ressources de la collection';
@@ -1838,6 +1869,14 @@ class AppLocalizationsFr extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Cette règle de récurrence utilise des options que cet éditeur ne modifie pas.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Cette liste ne prend pas en charge ce modèle de répétition. Modifiez le modèle ou choisissez une autre liste.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Ajoutez une date de début ou d’échéance pour utiliser ce modèle de répétition, ou désactivez la répétition.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Cette récurrence ne peut pas être utilisée avec $provider.';
diff --git a/lib/l10n/generated/app_localizations_hi.dart b/lib/l10n/generated/app_localizations_hi.dart
index 77237b21..90187393 100644
--- a/lib/l10n/generated/app_localizations_hi.dart
+++ b/lib/l10n/generated/app_localizations_hi.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsHi extends AppLocalizations {
AppLocalizationsHi([String locale = 'hi']) : super(locale);
+ @override
+ String get timeFormat => 'समय प्रारूप';
+
+ @override
+ String get timeFormatTwelveHour => '12 घंटे';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 घंटे';
+
+ @override
+ String get timeEndOfDay => 'दिन का अंत';
+
+ @override
+ String get timePeriod => 'दिन की अवधि';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'लॉगिन पर शुरू होने की स्थिति का पता नहीं चल सका।';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'इस सिस्टम पर लॉगिन के समय शुरू करना उपलब्ध नहीं है।';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'आपके व्यवस्थापक ने स्वचालित शुरुआत चालू की है और इसे यहाँ बदला नहीं जा सकता।';
+
+ @override
+ String get settingsSaveFailed =>
+ 'सेटिंग्स सहेजी नहीं जा सकीं। BusyMax फिर से शुरू होने पर आपके बदलाव खो सकते हैं।';
+
@override
String get nextcloudExportCollection => 'संग्रह के संसाधन निर्यात करें';
@@ -1838,6 +1869,14 @@ class AppLocalizationsHi extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'यह दोहराव नियम ऐसे विकल्पों का उपयोग करता है जिन्हें यह संपादक नहीं बदलता।';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'यह सूची इस दोहराव पैटर्न का समर्थन नहीं करती। पैटर्न बदलें या दूसरी सूची चुनें।';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'इस दोहराव पैटर्न का उपयोग करने के लिए शुरू होने या नियत तिथि जोड़ें, या दोहराव बंद करें।';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'इस पुनरावृत्ति का उपयोग $provider के साथ नहीं किया जा सकता।';
diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart
index b8c22815..a118f630 100644
--- a/lib/l10n/generated/app_localizations_it.dart
+++ b/lib/l10n/generated/app_localizations_it.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsIt extends AppLocalizations {
AppLocalizationsIt([String locale = 'it']) : super(locale);
+ @override
+ String get timeFormat => 'Formato ora';
+
+ @override
+ String get timeFormatTwelveHour => '12 ore';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 ore';
+
+ @override
+ String get timeEndOfDay => 'fine giornata';
+
+ @override
+ String get timePeriod => 'Periodo del giorno';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Impossibile determinare lo stato dell’avvio all’accesso.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'L’avvio all’accesso non è disponibile su questo sistema.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'L’avvio automatico è attivato dall’amministratore e non può essere modificato qui.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Impossibile salvare le impostazioni. Le modifiche potrebbero andare perse al riavvio di BusyMax.';
+
@override
String get nextcloudExportCollection => 'Esporta risorse della raccolta';
@@ -1850,6 +1881,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Questa regola di ricorrenza usa opzioni che questo editor non modifica.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Questo elenco non supporta questo schema di ripetizione. Modifica lo schema o scegli un altro elenco.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Aggiungi una data di inizio o di scadenza per usare questo schema di ripetizione, oppure disattiva la ripetizione.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Questa ricorrenza non può essere usata con $provider.';
diff --git a/lib/l10n/generated/app_localizations_ja.dart b/lib/l10n/generated/app_localizations_ja.dart
index e2b739e1..1a3121b2 100644
--- a/lib/l10n/generated/app_localizations_ja.dart
+++ b/lib/l10n/generated/app_localizations_ja.dart
@@ -12,6 +12,35 @@ import 'app_localizations.dart';
class AppLocalizationsJa extends AppLocalizations {
AppLocalizationsJa([String locale = 'ja']) : super(locale);
+ @override
+ String get timeFormat => '時刻の表示形式';
+
+ @override
+ String get timeFormatTwelveHour => '12時間表示';
+
+ @override
+ String get timeFormatTwentyFourHour => '24時間表示';
+
+ @override
+ String get timeEndOfDay => '一日の終わり';
+
+ @override
+ String get timePeriod => '午前・午後';
+
+ @override
+ String get launchAtLoginReadFailed => 'ログイン時の起動状態を確認できませんでした。';
+
+ @override
+ String get launchAtLoginUnavailable => 'このシステムではログイン時の起動を利用できません。';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ '管理者によって自動起動が有効になっているため、ここでは変更できません。';
+
+ @override
+ String get settingsSaveFailed =>
+ '設定を保存できませんでした。BusyMax を再起動すると変更が失われる可能性があります。';
+
@override
String get nextcloudExportCollection => 'コレクションのリソースをエクスポート';
@@ -1796,6 +1825,14 @@ class AppLocalizationsJa extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'この繰り返しルールには、このエディターで変更されないオプションが含まれています。';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'このリストではこの繰り返しパターンを使用できません。パターンを変更するか、別のリストを選択してください。';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'この繰り返しパターンを使用するには、開始日または期限を追加するか、繰り返しをオフにしてください。';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'この繰り返しは $provider では使用できません。';
diff --git a/lib/l10n/generated/app_localizations_ko.dart b/lib/l10n/generated/app_localizations_ko.dart
index 06db48e9..42871a56 100644
--- a/lib/l10n/generated/app_localizations_ko.dart
+++ b/lib/l10n/generated/app_localizations_ko.dart
@@ -12,6 +12,35 @@ import 'app_localizations.dart';
class AppLocalizationsKo extends AppLocalizations {
AppLocalizationsKo([String locale = 'ko']) : super(locale);
+ @override
+ String get timeFormat => '시간 형식';
+
+ @override
+ String get timeFormatTwelveHour => '12시간';
+
+ @override
+ String get timeFormatTwentyFourHour => '24시간';
+
+ @override
+ String get timeEndOfDay => '하루의 끝';
+
+ @override
+ String get timePeriod => '오전/오후';
+
+ @override
+ String get launchAtLoginReadFailed => '로그인 시 실행 상태를 확인할 수 없습니다.';
+
+ @override
+ String get launchAtLoginUnavailable => '이 시스템에서는 로그인 시 실행을 사용할 수 없습니다.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ '관리자가 자동 시작을 활성화했으며 여기서는 변경할 수 없습니다.';
+
+ @override
+ String get settingsSaveFailed =>
+ '설정을 저장할 수 없습니다. BusyMax를 다시 시작하면 변경 사항이 사라질 수 있습니다.';
+
@override
String get nextcloudExportCollection => '컬렉션 리소스 내보내기';
@@ -1800,6 +1829,14 @@ class AppLocalizationsKo extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'이 반복 규칙에는 이 편집기에서 변경하지 않는 옵션이 사용됩니다.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ '이 목록은 이 반복 패턴을 지원하지 않습니다. 패턴을 변경하거나 다른 목록을 선택하세요.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ '이 반복 패턴을 사용하려면 시작일 또는 마감일을 추가하거나 반복을 끄세요.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return '이 반복은 $provider에서 사용할 수 없습니다.';
diff --git a/lib/l10n/generated/app_localizations_pl.dart b/lib/l10n/generated/app_localizations_pl.dart
index 18f1582c..a4b4f087 100644
--- a/lib/l10n/generated/app_localizations_pl.dart
+++ b/lib/l10n/generated/app_localizations_pl.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsPl extends AppLocalizations {
AppLocalizationsPl([String locale = 'pl']) : super(locale);
+ @override
+ String get timeFormat => 'Format czasu';
+
+ @override
+ String get timeFormatTwelveHour => '12-godzinny';
+
+ @override
+ String get timeFormatTwentyFourHour => '24-godzinny';
+
+ @override
+ String get timeEndOfDay => 'koniec dnia';
+
+ @override
+ String get timePeriod => 'Pora dnia';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Nie udało się ustalić stanu uruchamiania przy logowaniu.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Uruchamianie przy logowaniu jest niedostępne w tym systemie.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Administrator włączył automatyczne uruchamianie i nie można tego tutaj zmienić.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Nie udało się zapisać ustawień. Zmiany mogą zostać utracone po ponownym uruchomieniu BusyMax.';
+
@override
String get nextcloudExportCollection => 'Eksportuj zasoby kolekcji';
@@ -1978,6 +2009,14 @@ class AppLocalizationsPl extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Ta reguła powtarzania korzysta z opcji, których ten edytor nie zmienia.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Ta lista nie obsługuje tego wzorca powtarzania. Zmień wzorzec lub wybierz inną listę.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Dodaj datę rozpoczęcia lub termin wykonania, aby użyć tego wzorca, albo wyłącz powtarzanie.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Tego powtarzania nie można użyć u dostawcy $provider.';
diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart
index ab6fddf3..1d3f5cab 100644
--- a/lib/l10n/generated/app_localizations_pt.dart
+++ b/lib/l10n/generated/app_localizations_pt.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsPt extends AppLocalizations {
AppLocalizationsPt([String locale = 'pt']) : super(locale);
+ @override
+ String get timeFormat => 'Time format';
+
+ @override
+ String get timeFormatTwelveHour => '12-hour';
+
+ @override
+ String get timeFormatTwentyFourHour => '24-hour';
+
+ @override
+ String get timeEndOfDay => 'end of day';
+
+ @override
+ String get timePeriod => 'Day period';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Could not determine the launch-at-login state.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Launch at login is unavailable on this system.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Startup is enabled by your administrator and cannot be changed here.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Could not save settings. Your changes may be lost when BusyMax restarts.';
+
@override
String get nextcloudExportCollection => 'Export collection resources';
@@ -1852,6 +1883,14 @@ class AppLocalizationsPt extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'This recurrence rule uses options that this editor does not change.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'This list cannot use this repeat pattern. Change the pattern or choose another list.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Add a start or due date to use this repeat pattern, or turn off repeat.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'This recurrence cannot be used with $provider.';
@@ -2920,6 +2959,37 @@ class AppLocalizationsPt extends AppLocalizations {
class AppLocalizationsPtPt extends AppLocalizationsPt {
AppLocalizationsPtPt() : super('pt_PT');
+ @override
+ String get timeFormat => 'Formato da hora';
+
+ @override
+ String get timeFormatTwelveHour => '12 horas';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 horas';
+
+ @override
+ String get timeEndOfDay => 'fim do dia';
+
+ @override
+ String get timePeriod => 'Período do dia';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Não foi possível determinar o estado do arranque ao iniciar sessão.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'O arranque ao iniciar sessão não está disponível neste sistema.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'O arranque automático foi ativado pelo administrador e não pode ser alterado aqui.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Não foi possível guardar as definições. As alterações poderão perder-se ao reiniciar o BusyMax.';
+
@override
String get nextcloudExportCollection => 'Exportar recursos da coleção';
@@ -4756,6 +4826,14 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
String get unsupportedRecurrencePreserved =>
'Esta regra de recorrência utiliza opções que este editor não altera.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Esta lista não suporta este padrão de repetição. Altere o padrão ou escolha outra lista.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Adicione uma data de início ou de conclusão para usar este padrão, ou desative a repetição.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Esta recorrência não pode ser usada com $provider.';
diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart
index 1a1f13d5..8107a4ce 100644
--- a/lib/l10n/generated/app_localizations_ru.dart
+++ b/lib/l10n/generated/app_localizations_ru.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
+ @override
+ String get timeFormat => 'Формат времени';
+
+ @override
+ String get timeFormatTwelveHour => '12 часов';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 часа';
+
+ @override
+ String get timeEndOfDay => 'конец дня';
+
+ @override
+ String get timePeriod => 'Время суток';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Не удалось определить состояние запуска при входе в систему.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Запуск при входе в систему недоступен в этой системе.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Автозапуск включён администратором, и его нельзя изменить здесь.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Не удалось сохранить настройки. Изменения могут быть потеряны при перезапуске BusyMax.';
+
@override
String get nextcloudExportCollection => 'Экспортировать ресурсы коллекции';
@@ -1854,6 +1885,14 @@ class AppLocalizationsRu extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Это правило повторения использует параметры, которые этот редактор не изменяет.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Этот список не поддерживает такой шаблон повторения. Измените шаблон или выберите другой список.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Добавьте дату начала или срок выполнения для этого шаблона либо отключите повторение.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Это повторение нельзя использовать с $provider.';
diff --git a/lib/l10n/generated/app_localizations_vi.dart b/lib/l10n/generated/app_localizations_vi.dart
index 9d4eea17..10ca3c5e 100644
--- a/lib/l10n/generated/app_localizations_vi.dart
+++ b/lib/l10n/generated/app_localizations_vi.dart
@@ -12,6 +12,37 @@ import 'app_localizations.dart';
class AppLocalizationsVi extends AppLocalizations {
AppLocalizationsVi([String locale = 'vi']) : super(locale);
+ @override
+ String get timeFormat => 'Định dạng giờ';
+
+ @override
+ String get timeFormatTwelveHour => '12 giờ';
+
+ @override
+ String get timeFormatTwentyFourHour => '24 giờ';
+
+ @override
+ String get timeEndOfDay => 'cuối ngày';
+
+ @override
+ String get timePeriod => 'Buổi trong ngày';
+
+ @override
+ String get launchAtLoginReadFailed =>
+ 'Không thể xác định trạng thái khởi chạy khi đăng nhập.';
+
+ @override
+ String get launchAtLoginUnavailable =>
+ 'Hệ thống này không hỗ trợ khởi chạy khi đăng nhập.';
+
+ @override
+ String get windowsStartupEnabledByPolicy =>
+ 'Quản trị viên đã bật tự động khởi động và không thể thay đổi tại đây.';
+
+ @override
+ String get settingsSaveFailed =>
+ 'Không thể lưu cài đặt. Các thay đổi có thể bị mất khi BusyMax khởi động lại.';
+
@override
String get nextcloudExportCollection => 'Xuất tài nguyên bộ sưu tập';
@@ -1836,6 +1867,14 @@ class AppLocalizationsVi extends AppLocalizations {
String get unsupportedRecurrencePreserved =>
'Quy tắc lặp lại này sử dụng các tùy chọn mà trình chỉnh sửa này không thay đổi.';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ 'Danh sách này không hỗ trợ kiểu lặp này. Hãy đổi kiểu lặp hoặc chọn danh sách khác.';
+
+ @override
+ String get taskRecurrenceRequiresDate =>
+ 'Thêm ngày bắt đầu hoặc hạn chót để dùng kiểu lặp này, hoặc tắt lặp lại.';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return 'Không thể sử dụng kiểu lặp lại này với $provider.';
diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart
index fede9cf2..fd5047c2 100644
--- a/lib/l10n/generated/app_localizations_zh.dart
+++ b/lib/l10n/generated/app_localizations_zh.dart
@@ -12,6 +12,33 @@ import 'app_localizations.dart';
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
+ @override
+ String get timeFormat => '时间格式';
+
+ @override
+ String get timeFormatTwelveHour => '12小时制';
+
+ @override
+ String get timeFormatTwentyFourHour => '24小时制';
+
+ @override
+ String get timeEndOfDay => '当天结束';
+
+ @override
+ String get timePeriod => '上午/下午';
+
+ @override
+ String get launchAtLoginReadFailed => '无法确定登录时启动的状态。';
+
+ @override
+ String get launchAtLoginUnavailable => '此系统不支持登录时启动。';
+
+ @override
+ String get windowsStartupEnabledByPolicy => '管理员已启用自动启动,无法在此更改。';
+
+ @override
+ String get settingsSaveFailed => '无法保存设置。重新启动 BusyMax 时,您的更改可能会丢失。';
+
@override
String get nextcloudExportCollection => '导出集合资源';
@@ -1774,6 +1801,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get unsupportedRecurrencePreserved => '此重复规则使用了编辑器不会更改的选项。';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ '此列表不支持此重复模式。请更改模式或选择其他列表。';
+
+ @override
+ String get taskRecurrenceRequiresDate => '请添加开始日期或截止日期以使用此重复模式,或关闭重复。';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return '此重复规则不能用于 $provider。';
@@ -2793,6 +2827,33 @@ class AppLocalizationsZh extends AppLocalizations {
class AppLocalizationsZhHans extends AppLocalizationsZh {
AppLocalizationsZhHans() : super('zh_Hans');
+ @override
+ String get timeFormat => '时间格式';
+
+ @override
+ String get timeFormatTwelveHour => '12小时制';
+
+ @override
+ String get timeFormatTwentyFourHour => '24小时制';
+
+ @override
+ String get timeEndOfDay => '当天结束';
+
+ @override
+ String get timePeriod => '上午/下午';
+
+ @override
+ String get launchAtLoginReadFailed => '无法确定登录时启动的状态。';
+
+ @override
+ String get launchAtLoginUnavailable => '此系统不支持登录时启动。';
+
+ @override
+ String get windowsStartupEnabledByPolicy => '管理员已启用自动启动,无法在此更改。';
+
+ @override
+ String get settingsSaveFailed => '无法保存设置。重新启动 BusyMax 时,您的更改可能会丢失。';
+
@override
String get nextcloudExportCollection => '导出集合资源';
@@ -4555,6 +4616,13 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
@override
String get unsupportedRecurrencePreserved => '此重复规则使用了编辑器不会更改的选项。';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ '此列表不支持此重复模式。请更改模式或选择其他列表。';
+
+ @override
+ String get taskRecurrenceRequiresDate => '请添加开始日期或截止日期以使用此重复模式,或关闭重复。';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return '此重复规则不能用于 $provider。';
@@ -5574,6 +5642,33 @@ class AppLocalizationsZhHans extends AppLocalizationsZh {
class AppLocalizationsZhHant extends AppLocalizationsZh {
AppLocalizationsZhHant() : super('zh_Hant');
+ @override
+ String get timeFormat => '時間格式';
+
+ @override
+ String get timeFormatTwelveHour => '12小時制';
+
+ @override
+ String get timeFormatTwentyFourHour => '24小時制';
+
+ @override
+ String get timeEndOfDay => '當天結束';
+
+ @override
+ String get timePeriod => '上午/下午';
+
+ @override
+ String get launchAtLoginReadFailed => '無法確定登入時啟動的狀態。';
+
+ @override
+ String get launchAtLoginUnavailable => '此系統不支援登入時啟動。';
+
+ @override
+ String get windowsStartupEnabledByPolicy => '管理員已啟用自動啟動,無法在此變更。';
+
+ @override
+ String get settingsSaveFailed => '無法儲存設定。重新啟動 BusyMax 時,您的變更可能會遺失。';
+
@override
String get nextcloudExportCollection => '匯出集合資源';
@@ -7336,6 +7431,13 @@ class AppLocalizationsZhHant extends AppLocalizationsZh {
@override
String get unsupportedRecurrencePreserved => '此重複規則使用了編輯器不會更改的選項。';
+ @override
+ String get taskRecurrenceDestinationUnsupported =>
+ '此清單不支援此重複模式。請變更模式或選擇其他清單。';
+
+ @override
+ String get taskRecurrenceRequiresDate => '請新增開始日期或截止日期以使用此重複模式,或關閉重複。';
+
@override
String recurrenceUnsupportedByProvider(String provider) {
return '此重複規則無法用於 $provider。';
diff --git a/lib/src/app/app_bootstrap.dart b/lib/src/app/app_bootstrap.dart
index 0953aadc..3838ac32 100644
--- a/lib/src/app/app_bootstrap.dart
+++ b/lib/src/app/app_bootstrap.dart
@@ -3,10 +3,15 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import '../dav/nextcloud/nextcloud_native_export.dart';
import 'package:http/http.dart' as http;
import 'package:uuid/uuid.dart';
+import 'package:busymax/src/core/secrets/secret_store.dart';
+import 'package:busymax/src/providers/busy_provider.dart';
+import 'package:busymax/src/features/tasks/domain/task_capabilities.dart';
+import '../core/time/provider_date_time.dart';
+import '../core/time/stored_temporal_projection.dart';
+import '../dav/nextcloud/nextcloud_native_export.dart';
import '../config/build_config.dart';
import '../calendar_providers/cloud_calendar_client.dart';
import '../db/app_database.dart';
@@ -35,6 +40,8 @@ import '../features/auth/data/auth_repository.dart';
import '../features/connectivity/network_connectivity_service.dart';
import '../features/feedback/data/feedback_api_client.dart';
import '../features/notifications/desktop_notification_service.dart';
+import '../features/notifications/notification_cancellation_queue.dart';
+import '../features/notifications/due_today_notification_scheduler.dart';
import '../features/notifications/notification_schedule_service.dart';
import '../features/notifications/notification_scheduler.dart';
import '../features/sync/account_sync_operations.dart';
@@ -53,15 +60,12 @@ import '../google_tasks/http/authenticated_http_client.dart';
import '../google_tasks/http/retrying_http_client.dart';
import '../google_tasks/oauth/oauth_loopback_flow.dart';
import '../google_tasks/oauth/oauth_service.dart';
-import 'package:busymax/src/core/secrets/secret_store.dart';
import '../google_calendar/google_calendar_api_client.dart';
import '../microsoft_calendar/microsoft_calendar_api_client.dart';
import '../microsoft_todo/api/microsoft_todo_api_client.dart';
import '../microsoft_todo/api/microsoft_todo_task_remote_client.dart';
import '../microsoft_todo/oauth/microsoft_oauth_service.dart';
import '../platform/common/desktop_services.dart';
-import 'package:busymax/src/providers/busy_provider.dart';
-import 'package:busymax/src/features/tasks/domain/task_capabilities.dart';
import '../schedule/schedule_commands.dart';
import '../schedule/schedule_repository.dart';
import '../schedule/schedule_sidebar_order.dart';
@@ -250,13 +254,24 @@ final desktopNotificationReadinessProvider =
(ref) => const DesktopNotificationReadiness.initializing(),
);
+// Survives scheduler invalidation, settings changes and backend replacement.
+final notificationCancellationQueueProvider =
+ Provider(
+ (ref) => NotificationCancellationQueue(),
+ );
+
final desktopNotificationServiceProvider = Provider(
(ref) {
final settings = ref.watch(appSettingsControllerProvider);
return DesktopNotificationService(
backend: ref.watch(desktopNotificationBackendProvider),
+ cancellationQueue: ref.watch(notificationCancellationQueueProvider),
settings: settings,
locale: settings.locale,
+ onDestinationActivated: (destination) async {
+ await ref.read(desktopWindowServiceProvider).showWindow();
+ ref.read(desktopNavigationServiceProvider).open(destination);
+ },
);
},
);
@@ -288,11 +303,40 @@ final launchAtLoginStateProvider = FutureProvider(
);
final launchAtLoginEnabledProvider = FutureProvider(
- (ref) async =>
- await ref.watch(launchAtLoginStateProvider.future) ==
- DesktopAutostartState.enabled,
+ (ref) async => (await ref.watch(launchAtLoginStateProvider.future)).isEnabled,
);
+final launchAtLoginControllerProvider =
+ StateNotifierProvider(
+ (ref) => LaunchAtLoginController(ref),
+ );
+
+/// Serializes changes across Settings instances, including the state readback.
+class LaunchAtLoginController extends StateNotifier {
+ LaunchAtLoginController(this._ref) : super(false);
+
+ final Ref _ref;
+
+ Future setEnabled(bool enabled) async {
+ if (state) return;
+ state = true;
+ try {
+ await _ref.read(desktopAutostartServiceProvider).setEnabled(enabled);
+ } finally {
+ if (mounted) {
+ _ref.invalidate(launchAtLoginStateProvider);
+ try {
+ await _ref.read(launchAtLoginStateProvider.future);
+ } on Object {
+ // The read provider exposes this failure separately from write errors.
+ } finally {
+ if (mounted) state = false;
+ }
+ }
+ }
+ }
+}
+
final desktopNavigationServiceProvider = Provider((
ref,
) {
@@ -636,12 +680,20 @@ final davAccountSyncEngineFactoryProvider =
});
final accountSyncCoordinatorProvider = Provider((ref) {
- return AccountSyncCoordinator();
+ final accountsRepository = ref.watch(accountsRepositoryProvider);
+ final coordinator = AccountSyncCoordinator(
+ restoreIncompleteTaskImports:
+ accountsRepository.incompleteTaskImportAccountIds,
+ persistTaskImportIncomplete: accountsRepository.setTaskImportIncomplete,
+ );
+ ref.onDispose(() => unawaited(coordinator.dispose()));
+ return coordinator;
});
final accountSyncOperationsProvider = Provider((ref) {
final accountsRepository = ref.watch(accountsRepositoryProvider);
final connectivity = ref.watch(networkConnectivityMonitorProvider);
+ final syncCoordinator = ref.watch(accountSyncCoordinatorProvider);
Future providerForAccount(String accountId) async {
final account = await accountsRepository.accountById(accountId);
@@ -653,26 +705,28 @@ final accountSyncOperationsProvider = Provider((ref) {
final routing = RoutingAccountSyncOperations(
providerForAccount: providerForAccount,
- syncDav: (accountId, {required full}) async {
- await ref
+ syncDav: (accountId, {required full}) => syncCoordinator.trackTaskImport(
+ accountId,
+ () => ref
.read(davAccountSyncEngineFactoryProvider)(accountId)
- .synchronize(full: full);
- },
+ .synchronize(full: full),
+ ),
syncWebCal: (accountId, {required full}) => ref
.read(webCalSubscriptionServiceProvider)
.refreshAccount(accountId, force: full),
- syncTasksRest: (accountId, {required full}) async {
- final provider = await providerForAccount(accountId);
- final engine = ref.read(syncEngineForAccountFactoryProvider)(
- accountId,
- provider,
- );
- if (full) {
- await engine.fullSync();
- } else {
- await engine.incrementalSync();
- }
- },
+ syncTasksRest: (accountId, {required full}) =>
+ syncCoordinator.trackTaskImport(accountId, () async {
+ final provider = await providerForAccount(accountId);
+ final engine = ref.read(syncEngineForAccountFactoryProvider)(
+ accountId,
+ provider,
+ );
+ if (full) {
+ await engine.fullSync();
+ } else {
+ await engine.incrementalSync();
+ }
+ }),
syncCalendarRest: (accountId, {required full}) async {
final provider = await providerForAccount(accountId);
final engine = ref.read(calendarSyncEngineForAccountFactoryProvider)(
@@ -687,7 +741,7 @@ final accountSyncOperationsProvider = Provider((ref) {
},
);
return CoordinatedAccountSyncOperations(
- coordinator: ref.watch(accountSyncCoordinatorProvider),
+ coordinator: syncCoordinator,
inner: ConnectivityAwareAccountSyncOperations(
inner: routing,
requireNetwork: connectivity.requireNetwork,
@@ -817,6 +871,30 @@ final davProjectionCoverageServiceProvider =
);
});
+/// One invalidation source for all presentations, including background sync.
+final scheduleDataRevisionProvider = StreamProvider((ref) async* {
+ final changes = ref.watch(scheduleRepositoryProvider).watchChanges();
+ var revision = 0;
+ yield revision;
+ await for (final _ in changes) {
+ yield ++revision;
+ }
+});
+
+final scheduleTaskListsProvider = FutureProvider>((
+ ref,
+) async {
+ ref.watch(scheduleDataRevisionProvider);
+ final accounts = await ref.watch(accountsStreamProvider.future);
+ final groups = await Future.wait([
+ for (final account in accounts.where((account) => account.isTaskCapable))
+ ref
+ .watch(taskListsRepositoryForAccountProvider(account.id))
+ .listTaskLists(),
+ ]);
+ return [for (final group in groups) ...group];
+});
+
final scheduleRepositoryProvider = Provider((ref) {
return ScheduleRepository(
ref.watch(databaseProvider),
@@ -1171,16 +1249,32 @@ Future _markAccountReconnectRequiredForSyncError(
}
}
-final notificationSchedulerProvider = Provider((ref) {
- final scheduler = NotificationScheduler(
- database: ref.watch(databaseProvider),
- notifications: ref.watch(desktopNotificationServiceProvider),
- onNotificationActivated: (row) => _openNotificationSource(ref, row),
- );
- scheduler.start();
- ref.onDispose(scheduler.stop);
- return scheduler;
-});
+// This handler outlives individual schedulers, so a visible Linux notification
+// can route to the current scheduler even after an explicit invalidation.
+final _reminderActionHandlerProvider = Provider(
+ (ref) =>
+ (row, action) => ref
+ .read(notificationSchedulerProvider)
+ .handleReminderAction(row, action),
+);
+
+final Provider notificationSchedulerProvider =
+ Provider((ref) {
+ final scheduler = NotificationScheduler(
+ database: ref.watch(databaseProvider),
+ notifications: ref.read(desktopNotificationServiceProvider),
+ // Read the independent router without making it a dependency of the
+ // scheduler it resolves when an action arrives.
+ onReminderAction: ref.read(_reminderActionHandlerProvider),
+ onNotificationActivated: (row) => _openNotificationSource(ref, row),
+ );
+ ref.listen(desktopNotificationServiceProvider, (_, notifications) {
+ scheduler.updateNotifications(notifications);
+ });
+ scheduler.start();
+ ref.onDispose(scheduler.stop);
+ return scheduler;
+ });
final _notificationOpenSequenceProvider = StateProvider((ref) => 0);
@@ -1217,7 +1311,7 @@ Future _openEventNotification(
await _openScheduleItemFromNotification(
ref,
kind: ScheduleWorkspaceCommandKind.openCalendarEvent,
- date: _calendarEventCommandDate(event),
+ date: calendarEventStartAsLocal(event),
accountId: event.accountId,
sourceId: event.calendarSourceId,
itemId: event.id,
@@ -1283,101 +1377,38 @@ Future _openScheduleItemFromNotification(
.open(DesktopNavigationDestination.schedule);
}
-DateTime? _calendarEventCommandDate(CalendarEvent event) {
- if (event.allDay) {
- return _parseLocalDate(event.startDate);
- }
- return _parseProviderDateTime(event.startDateTime, event.startTimeZone);
-}
-
DateTime? _taskCommandDate(Task task) {
- return _parseProviderDateTime(
+ return providerDateTimeAsLocal(
task.microsoftDueDateTime,
task.microsoftDueTimeZone,
) ??
- _parseProviderDateTime(
+ providerDateTimeAsLocal(
task.microsoftReminderDateTime,
task.microsoftReminderTimeZone,
) ??
- _parseProviderDateTime(task.dueUtc, 'UTC');
-}
-
-DateTime? _parseLocalDate(String? value) {
- if (value == null || value.length < 10) {
- return null;
- }
- return DateTime.tryParse('${value.substring(0, 10)}T00:00:00');
-}
-
-DateTime? _parseProviderDateTime(String? value, String? timeZone) {
- final parsed = DateTime.tryParse(value ?? '');
- if (parsed == null) {
- return null;
- }
- if (parsed.isUtc) {
- return parsed.toLocal();
- }
-
- final normalizedZone = timeZone?.trim().toLowerCase();
- if (normalizedZone == 'utc' ||
- normalizedZone == 'etc/utc' ||
- normalizedZone == 'gmt' ||
- normalizedZone == 'etc/gmt') {
- return DateTime.utc(
- parsed.year,
- parsed.month,
- parsed.day,
- parsed.hour,
- parsed.minute,
- parsed.second,
- parsed.millisecond,
- parsed.microsecond,
- ).toLocal();
- }
-
- return parsed;
+ taskDueAsLocal(task);
}
-final dueTodayNotificationProvider = Provider((ref) {
- final settings = ref.watch(appSettingsControllerProvider);
- final accountId = ref.watch(activeAccountProvider);
- if (!settings.notifyDueToday || accountId == null) {
- return;
- }
-
- unawaited(_notifyDueTodayIfNeeded(ref, accountId, settings));
+final dueTodayNotificationProvider = Provider((
+ ref,
+) {
+ final scheduler = DueTodayNotificationScheduler(
+ database: ref.watch(databaseProvider),
+ settings: () => ref.read(appSettingsControllerProvider),
+ activeAccountId: () => ref.read(activeAccountProvider),
+ notifications: () => ref.read(desktopNotificationServiceProvider),
+ markNotified: (date) => ref
+ .read(appSettingsControllerProvider.notifier)
+ .markDueTodayNotified(date),
+ syncBlocksDelivery: ref.read(accountSyncCoordinatorProvider).blocksDueToday,
+ accountSyncChanges: ref.read(accountSyncCoordinatorProvider).runningChanges,
+ );
+ ref.listen(
+ appSettingsControllerProvider,
+ (_, _) => scheduler.inputsChanged(),
+ );
+ ref.listen(activeAccountProvider, (_, _) => scheduler.inputsChanged());
+ scheduler.start();
+ ref.onDispose(scheduler.stop);
+ return scheduler;
});
-
-Future _notifyDueTodayIfNeeded(
- Ref ref,
- String accountId,
- AppSettings settings,
-) async {
- final now = DateTime.now();
- final today =
- '${now.year.toString().padLeft(4, '0')}-'
- '${now.month.toString().padLeft(2, '0')}-'
- '${now.day.toString().padLeft(2, '0')}';
- if (settings.lastDueTodayNotificationDate == today) {
- return;
- }
-
- final database = ref.read(databaseProvider);
- final tasks =
- await (database.select(database.tasks)..where(
- (row) =>
- row.accountId.equals(accountId) &
- row.dueUtc.equals(today) &
- row.pendingDelete.equals(false),
- ))
- .get();
- final count = tasks.where((task) => task.status != 'completed').length;
- if (count <= 0) {
- return;
- }
-
- await ref.read(desktopNotificationServiceProvider).notifyDueToday(count);
- await ref
- .read(appSettingsControllerProvider.notifier)
- .markDueTodayNotified(today);
-}
diff --git a/lib/src/app/app_settings.dart b/lib/src/app/app_settings.dart
index 36512f7e..78d959c3 100644
--- a/lib/src/app/app_settings.dart
+++ b/lib/src/app/app_settings.dart
@@ -5,10 +5,15 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import '../l10n/app_locale.dart';
+import '../l10n/time_format.dart';
+
+export '../l10n/time_format.dart' show BusyMaxTimeFormatPreference;
+import '../core/logging/redacting_logger.dart';
import '../schedule/schedule_sidebar_order.dart';
import '../schedule/schedule_view_mode.dart';
@@ -37,6 +42,7 @@ class AppSettings {
required this.themeFamily,
required this.themeModePreference,
required this.localeTag,
+ this.timeFormatPreference = BusyMaxTimeFormatPreference.system,
required this.notifySyncFailures,
required this.notifyConflicts,
required this.notifyDueToday,
@@ -64,6 +70,7 @@ class AppSettings {
themeFamily: BusyMaxThemeFamily.yaru,
themeModePreference: BusyMaxThemeModePreference.system,
localeTag: null,
+ timeFormatPreference: BusyMaxTimeFormatPreference.system,
notifySyncFailures: true,
notifyConflicts: true,
notifyDueToday: false,
@@ -150,6 +157,11 @@ class AppSettings {
defaults.themeModePreference,
),
localeTag: normalizeBusyMaxLocaleTag(json['localeTag']?.toString()),
+ timeFormatPreference: _enumFromName(
+ BusyMaxTimeFormatPreference.values,
+ json['timeFormatPreference'],
+ defaults.timeFormatPreference,
+ ),
notifySyncFailures:
json['notifySyncFailures'] as bool? ?? defaults.notifySyncFailures,
notifyConflicts:
@@ -191,6 +203,7 @@ class AppSettings {
final BusyMaxThemeFamily themeFamily;
final BusyMaxThemeModePreference themeModePreference;
final String? localeTag;
+ final BusyMaxTimeFormatPreference timeFormatPreference;
final bool notifySyncFailures;
final bool notifyConflicts;
final bool notifyDueToday;
@@ -221,6 +234,7 @@ class AppSettings {
'themeFamily': themeFamily.name,
'themeModePreference': themeModePreference.name,
'localeTag': localeTag,
+ 'timeFormatPreference': timeFormatPreference.name,
'notifySyncFailures': notifySyncFailures,
'notifyConflicts': notifyConflicts,
'notifyDueToday': notifyDueToday,
@@ -248,6 +262,7 @@ class AppSettings {
BusyMaxThemeFamily? themeFamily,
BusyMaxThemeModePreference? themeModePreference,
Object? localeTag = _unset,
+ BusyMaxTimeFormatPreference? timeFormatPreference,
bool? notifySyncFailures,
bool? notifyConflicts,
bool? notifyDueToday,
@@ -281,6 +296,7 @@ class AppSettings {
);
return AppSettings(
themeFamily: themeFamily ?? this.themeFamily,
+ timeFormatPreference: timeFormatPreference ?? this.timeFormatPreference,
themeModePreference: themeModePreference ?? this.themeModePreference,
localeTag: identical(localeTag, _unset)
? this.localeTag
@@ -360,8 +376,11 @@ class JsonFileLocalSettingsStore implements LocalSettingsStore {
typedef _AppSettingsMutation = AppSettings Function(AppSettings current);
class AppSettingsController extends StateNotifier {
- AppSettingsController(this._store, {AppSettings? initialSettings})
- : super(initialSettings ?? AppSettings.defaults()) {
+ AppSettingsController(
+ this._store, {
+ AppSettings? initialSettings,
+ this.onPersistenceChanged,
+ }) : super(initialSettings ?? AppSettings.defaults()) {
_persistenceState = state;
if (initialSettings == null) {
_loadFuture = _load();
@@ -373,6 +392,8 @@ class AppSettingsController extends StateNotifier {
}
final LocalSettingsStore _store;
+ final void Function(bool failed)? onPersistenceChanged;
+ final _logger = RedactingLogger(Logger('AppSettings'));
final List<_AppSettingsMutation> _mutationsDuringLoad =
<_AppSettingsMutation>[];
late AppSettings _persistenceState;
@@ -383,6 +404,11 @@ class AppSettingsController extends StateNotifier {
Future get ready => _loadFuture;
+ Future retrySave() => _mutate((settings) => settings);
+
+ Future setTimeFormatPreference(BusyMaxTimeFormatPreference value) =>
+ _mutate((current) => current.copyWith(timeFormatPreference: value));
+
Future registerSidebarIds(
SidebarOrderSection section,
Iterable ids, {
@@ -625,8 +651,10 @@ class AppSettingsController extends StateNotifier {
_persistenceState = next;
try {
await _store.save(next.toJson());
- } on Object {
- // Keep the in-memory preference even when local persistence is unavailable.
+ if (!_disposed) onPersistenceChanged?.call(false);
+ } on Object catch (error) {
+ _logger.warning('Could not save settings: ${redactForLog(error)}');
+ if (!_disposed) onPersistenceChanged?.call(true);
}
}
@@ -653,11 +681,18 @@ final localSettingsStoreProvider = Provider(
final initialAppSettingsProvider = Provider((ref) => null);
+final appSettingsPersistenceFailedProvider = StateProvider(
+ (ref) => false,
+);
+
final appSettingsControllerProvider =
StateNotifierProvider((ref) {
return AppSettingsController(
ref.watch(localSettingsStoreProvider),
initialSettings: ref.watch(initialAppSettingsProvider),
+ onPersistenceChanged: (failed) =>
+ ref.read(appSettingsPersistenceFailedProvider.notifier).state =
+ failed,
);
});
diff --git a/lib/src/app/busymax_app.dart b/lib/src/app/busymax_app.dart
index b99eb057..a5078ce0 100644
--- a/lib/src/app/busymax_app.dart
+++ b/lib/src/app/busymax_app.dart
@@ -14,8 +14,10 @@ import '../platform/linux_header_bar_provider.dart';
import '../platform/linux_header_bar_service.dart';
import '../platform/common/desktop_services.dart';
import '../l10n/locale_resolution.dart';
+import '../l10n/time_format_scope.dart';
import '../schedule/schedule_commands.dart';
import 'app_bootstrap.dart';
+import 'desktop_startup_policy.dart';
import 'app_router.dart';
import 'busymax_keyboard_shortcuts_dialog.dart';
import 'busymax_shortcuts.dart';
@@ -88,7 +90,11 @@ class _BusyMaxAppState extends ConsumerState {
BusyMaxTrayService? _trayService;
bool? _lastHideOnClose;
bool? _lastTrayEnabled;
- bool _startMinimizedHandled = false;
+ late final _startupPolicy = DesktopStartupPolicy(
+ startMinimizedAtLaunch: widget.startMinimizedAtLaunch,
+ );
+ bool _backgroundConfigurationRunning = false;
+ bool _backgroundConfigurationPending = false;
bool _settingsReady = false;
var _scheduleCommandSequence = 0;
BusyMaxTrayPresentationFormatter? _trayPresentationFormatter;
@@ -252,9 +258,12 @@ class _BusyMaxAppState extends ConsumerState {
supportedLocales: busyMaxSupportedLocales,
builder: (context, child) {
final l10n = AppLocalizations.of(context);
- final material = MaterialLocalizations.of(context);
- final alwaysUse24HourFormat = MediaQuery.alwaysUse24HourFormatOf(
- context,
+ final clock = BusyMaxTimeFormatter(
+ locale: Localizations.localeOf(context).toLanguageTag(),
+ use24Hour: resolveBusyMax24HourClock(
+ settings.timeFormatPreference,
+ systemUses24Hour: MediaQuery.alwaysUse24HourFormatOf(context),
+ ),
);
final trayFormatter = BusyMaxTrayPresentationFormatter(
BusyMaxTrayPresentationStrings(
@@ -276,10 +285,7 @@ class _BusyMaxAppState extends ConsumerState {
quitBusyMax: l10n.trayQuitBusyMax,
offline: l10n.networkOffline,
offlineDescription: l10n.networkOfflineDescription,
- formatTime: (value) => material.formatTimeOfDay(
- TimeOfDay.fromDateTime(value),
- alwaysUse24HourFormat: alwaysUse24HourFormat,
- ),
+ formatTime: clock.format,
tasksDueToday: l10n.trayTasksDueToday,
lastSyncedJustNow: l10n.trayLastSyncedJustNow,
lastSyncedMinutesAgo: l10n.trayLastSyncedMinutesAgo,
@@ -327,7 +333,15 @@ class _BusyMaxAppState extends ConsumerState {
},
child: ColoredBox(
color: BusyMaxSurfaceColors.of(context).window,
- child: child ?? const SizedBox.shrink(),
+ child: BusyMaxTimeFormatScope(
+ formatter: clock,
+ child: MediaQuery(
+ data: MediaQuery.of(
+ context,
+ ).copyWith(alwaysUse24HourFormat: clock.use24Hour),
+ child: child ?? const SizedBox.shrink(),
+ ),
+ ),
),
),
);
@@ -400,44 +414,64 @@ class _BusyMaxAppState extends ConsumerState {
if (!_settingsReady) {
return;
}
-
- final windowService = ref.read(desktopWindowServiceProvider);
- final trayEnabled =
- settings.showTrayIcon ||
- settings.runInBackgroundWhenClosed ||
- settings.startMinimizedToTray ||
- widget.startMinimizedAtLaunch;
- _setHideOnClose(
- windowService,
- settings.runInBackgroundWhenClosed &&
- trayEnabled &&
- (_trayService?.available ?? false),
- );
- if (_trayService != null) {
- unawaited(_trayService!.refreshPresentation());
- }
- if (_lastTrayEnabled == trayEnabled) {
+ if (_backgroundConfigurationRunning) {
+ _backgroundConfigurationPending = true;
return;
}
- _lastTrayEnabled = trayEnabled;
- final tray = _trayService ??= _createTrayService(
- windowService: windowService,
- formatter: trayFormatter,
- settings: settings,
- );
- unawaited(tray.refreshPresentation());
- if (trayEnabled) {
- unawaited(
- _startTray(
+ unawaited(_applyBackgroundServices(settings, trayFormatter));
+ }
+
+ Future _applyBackgroundServices(
+ AppSettings settings,
+ BusyMaxTrayPresentationFormatter trayFormatter,
+ ) async {
+ _backgroundConfigurationRunning = true;
+ try {
+ final windowService = ref.read(desktopWindowServiceProvider);
+ final trayEnabled = _startupPolicy.needsTray(settings);
+ final startMinimized = _startupPolicy.takeStartMinimized(settings);
+ _setHideOnClose(
+ windowService,
+ settings.runInBackgroundWhenClosed &&
+ trayEnabled &&
+ (_trayService?.available ?? false),
+ );
+ if (_trayService != null) {
+ unawaited(_trayService!.refreshPresentation());
+ }
+ if (_lastTrayEnabled == trayEnabled) {
+ return;
+ }
+ _lastTrayEnabled = trayEnabled;
+ final tray = _trayService ??= _createTrayService(
+ windowService: windowService,
+ formatter: trayFormatter,
+ settings: settings,
+ );
+ unawaited(tray.refreshPresentation());
+ if (trayEnabled) {
+ await _startTray(
tray,
windowService,
- startMinimizedToTray:
- settings.startMinimizedToTray || widget.startMinimizedAtLaunch,
- ),
- );
- } else {
- _setHideOnClose(windowService, false);
- unawaited(tray.stop());
+ startMinimizedToTray: startMinimized,
+ );
+ } else {
+ _setHideOnClose(windowService, false);
+ if (!await windowService.isWindowVisible()) {
+ await windowService.showWindow();
+ }
+ await tray.stop();
+ }
+ } finally {
+ _backgroundConfigurationRunning = false;
+ if (_backgroundConfigurationPending && mounted) {
+ _backgroundConfigurationPending = false;
+ _configureBackgroundServices(
+ ref,
+ ref.read(appSettingsControllerProvider),
+ trayFormatter,
+ );
+ }
}
}
@@ -483,12 +517,11 @@ class _BusyMaxAppState extends ConsumerState {
}
Future _loadTrayPresentation() async {
- final formatter = _trayPresentationFormatter;
- if (formatter == null) {
+ if (_trayPresentationFormatter == null) {
throw StateError('Tray localization is not ready.');
}
final presentation = await ref.read(trayPresentationServiceProvider).load();
- return formatter.format(presentation);
+ return _trayPresentationFormatter!.format(presentation);
}
Future _openTrayNewEvent(DesktopWindowService windowService) async {
@@ -582,13 +615,10 @@ class _BusyMaxAppState extends ConsumerState {
}
final latestSettings = ref.read(appSettingsControllerProvider);
- final trayStillEnabled =
- latestSettings.showTrayIcon ||
- latestSettings.runInBackgroundWhenClosed ||
- latestSettings.startMinimizedToTray ||
- widget.startMinimizedAtLaunch;
+ final trayStillEnabled = _startupPolicy.needsTray(latestSettings);
if (!trayStillEnabled) {
_setHideOnClose(windowService, false);
+ await windowService.showWindow();
await tray.stop();
return;
}
@@ -596,10 +626,9 @@ class _BusyMaxAppState extends ConsumerState {
windowService,
latestSettings.runInBackgroundWhenClosed && tray.available,
);
- if (!startMinimizedToTray || _startMinimizedHandled) {
+ if (!startMinimizedToTray) {
return;
}
- _startMinimizedHandled = true;
if (tray.available) {
await windowService.hideWindow();
} else {
diff --git a/lib/src/app/desktop_startup_policy.dart b/lib/src/app/desktop_startup_policy.dart
new file mode 100644
index 00000000..c8a9d7f0
--- /dev/null
+++ b/lib/src/app/desktop_startup_policy.dart
@@ -0,0 +1,35 @@
+import 'app_settings.dart';
+
+/// The launch argument keeps a hidden startup reachable until the user changes
+/// tray preferences. Window presentation is decided only once per session.
+class DesktopStartupPolicy {
+ DesktopStartupPolicy({required bool startMinimizedAtLaunch})
+ : _launchTrayOverride = startMinimizedAtLaunch;
+
+ bool _launchTrayOverride;
+ bool _startupHandled = false;
+ (bool, bool, bool)? _previousTrayPreferences;
+
+ bool needsTray(AppSettings settings) {
+ final preferences = (
+ settings.showTrayIcon,
+ settings.runInBackgroundWhenClosed,
+ settings.startMinimizedToTray,
+ );
+ if (_previousTrayPreferences != null &&
+ _previousTrayPreferences != preferences) {
+ _launchTrayOverride = false;
+ }
+ _previousTrayPreferences = preferences;
+ return settings.showTrayIcon ||
+ settings.runInBackgroundWhenClosed ||
+ settings.startMinimizedToTray ||
+ _launchTrayOverride;
+ }
+
+ bool takeStartMinimized(AppSettings settings) {
+ if (_startupHandled) return false;
+ _startupHandled = true;
+ return _launchTrayOverride || settings.startMinimizedToTray;
+ }
+}
diff --git a/lib/src/app/windows/windows_busymax_app.dart b/lib/src/app/windows/windows_busymax_app.dart
index 01e83597..dde90afa 100644
--- a/lib/src/app/windows/windows_busymax_app.dart
+++ b/lib/src/app/windows/windows_busymax_app.dart
@@ -7,6 +7,8 @@ import 'package:go_router/go_router.dart';
import '../../../l10n/generated/app_localizations.dart';
import '../../features/auth/data/auth_repository.dart';
import '../../l10n/locale_resolution.dart';
+import '../../l10n/time_format_scope.dart';
+import '../../platform/windows/windows_clock_preference.dart';
import '../../platform/common/desktop_services.dart';
import '../../ui/windows/windows_schedule_page.dart';
import '../../ui/windows/windows_settings_page.dart';
@@ -68,12 +70,13 @@ class _WindowsBusyMaxAppState extends ConsumerState
StreamSubscription? _activationSubscription;
StreamSubscription? _navigationSubscription;
StreamSubscription? _appearanceSubscription;
- int _platformChangeSequence = 0;
+ late final WindowsClockPreference _clockPreference;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
+ _clockPreference = WindowsClockPreference()..addListener(_platformChanged);
_activationSubscription = ref
.read(desktopActivationServiceProvider)
.activations
@@ -86,9 +89,6 @@ class _WindowsBusyMaxAppState extends ConsumerState
.read(systemAppearanceSourceProvider)
.changes
.listen((_) => _platformChanged());
- // Starts in-process reminder scheduling. Explicit Quit disposes it before
- // asking the runner to terminate.
- ref.read(notificationSchedulerProvider);
}
@override
@@ -98,10 +98,11 @@ class _WindowsBusyMaxAppState extends ConsumerState
void didChangePlatformBrightness() => _platformChanged();
void _platformChanged() {
- if (mounted) setState(() => _platformChangeSequence++);
+ if (mounted) setState(() {});
}
Future _handleActivation(DesktopActivation activation) async {
+ if (!activation.isValid) return;
if (activation.requiresVisibleWindow) {
await ref.read(desktopWindowServiceProvider).showWindow();
}
@@ -133,12 +134,19 @@ class _WindowsBusyMaxAppState extends ConsumerState
);
}
case DesktopActivationKind.notification:
+ final destination = activation.notificationDestination;
+ if (destination != null) {
+ _handleNavigation(DesktopNavigationRequest(destination));
+ return;
+ }
final scheduleId = activation.payload?['notificationScheduleId'];
if (scheduleId == null) return;
await ref
.read(notificationSchedulerProvider)
.handleActivation(
notificationScheduleId: scheduleId,
+ notificationGeneration:
+ activation.payload?['notificationGeneration'] ?? 'legacy',
action: activation.action!,
);
if (activation.action == 'default' || activation.action == 'open') {
@@ -159,6 +167,7 @@ class _WindowsBusyMaxAppState extends ConsumerState
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
+ _clockPreference.dispose();
unawaited(_activationSubscription?.cancel());
unawaited(_navigationSubscription?.cancel());
unawaited(_appearanceSubscription?.cancel());
@@ -167,6 +176,8 @@ class _WindowsBusyMaxAppState extends ConsumerState
@override
Widget build(BuildContext context) {
+ ref.watch(notificationSchedulerProvider);
+ ref.watch(dueTodayNotificationProvider);
final settings = ref.watch(appSettingsControllerProvider);
final dispatcher = WidgetsBinding.instance.platformDispatcher;
final appearance = ref.watch(systemAppearanceSourceProvider);
@@ -198,12 +209,6 @@ class _WindowsBusyMaxAppState extends ConsumerState
}
return FluentApp.router(
- key: ValueKey((
- _platformChangeSequence,
- appearance.brightness,
- highContrast,
- reducedMotion,
- )),
title: 'BusyMax',
debugShowCheckedModeBanner: false,
routerConfig: windowsAppRouter,
@@ -216,10 +221,29 @@ class _WindowsBusyMaxAppState extends ConsumerState
localizationsDelegates: const [AppLocalizations.delegate],
localeListResolutionCallback: resolveBusyMaxLocales,
supportedLocales: busyMaxSupportedLocales,
- builder: (context, child) => WindowsDesktopRuntime(
- startMinimizedAtLaunch: widget.startMinimizedAtLaunch,
- child: child ?? const SizedBox.shrink(),
- ),
+ builder: (context, child) {
+ final clock = BusyMaxTimeFormatter(
+ locale: Localizations.localeOf(context).toLanguageTag(),
+ use24Hour: resolveBusyMax24HourClock(
+ settings.timeFormatPreference,
+ systemUses24Hour:
+ _clockPreference.value ??
+ MediaQuery.alwaysUse24HourFormatOf(context),
+ ),
+ );
+ return BusyMaxTimeFormatScope(
+ formatter: clock,
+ child: MediaQuery(
+ data: MediaQuery.of(
+ context,
+ ).copyWith(alwaysUse24HourFormat: clock.use24Hour),
+ child: WindowsDesktopRuntime(
+ startMinimizedAtLaunch: widget.startMinimizedAtLaunch,
+ child: child ?? const SizedBox.shrink(),
+ ),
+ ),
+ );
+ },
);
}
}
diff --git a/lib/src/calendar_providers/calendar_description.dart b/lib/src/calendar_providers/calendar_description.dart
index f3a9d347..edfbebb2 100644
--- a/lib/src/calendar_providers/calendar_description.dart
+++ b/lib/src/calendar_providers/calendar_description.dart
@@ -90,10 +90,14 @@ CalendarDescriptionDocument htmlCalendarDescriptionDocument(String html) {
}
}
- final tokenPattern = RegExp(r'<[^>]+>|[^<]+', multiLine: true, dotAll: true);
+ final tokenPattern = RegExp(
+ r'?[a-zA-Z][^>]*>|[^<]+|<',
+ multiLine: true,
+ dotAll: true,
+ );
for (final match in tokenPattern.allMatches(withoutHidden)) {
final token = match.group(0) ?? '';
- if (token.startsWith('<')) {
+ if (token.startsWith('<') && token.length > 1) {
final tag = _tagName(token);
final closing = RegExp(r'^').hasMatch(token);
final selfClosing = token.endsWith('/>');
@@ -207,27 +211,38 @@ String escapeHtml(String value) {
}
String decodeHtmlEntities(String value) {
- return value
- .replaceAll(' ', ' ')
- .replaceAll(' ', ' ')
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"')
- .replaceAll(''', "'")
- .replaceAll(''', "'")
- .replaceAllMapped(RegExp(r'([0-9a-fA-F]+);'), (match) {
- final codePoint = int.tryParse(match.group(1)!, radix: 16);
- return codePoint == null
- ? match.group(0)!
- : String.fromCharCode(codePoint);
- })
- .replaceAllMapped(RegExp(r'([0-9]+);'), (match) {
- final codePoint = int.tryParse(match.group(1)!);
- return codePoint == null
- ? match.group(0)!
- : String.fromCharCode(codePoint);
- });
+ // Decode once: < represents the literal text <, not a new tag.
+ return value.replaceAllMapped(
+ RegExp(r'&(?:#([xX][0-9a-fA-F]+|[0-9]+)|(nbsp|amp|lt|gt|quot|apos));'),
+ (match) {
+ final numeric = match.group(1);
+ if (numeric != null) {
+ final hex = numeric.startsWith(RegExp('[xX]'));
+ final codePoint = int.tryParse(
+ hex ? numeric.substring(1) : numeric,
+ radix: hex ? 16 : 10,
+ );
+ // Invalid numeric references (including overflow, NUL and surrogates)
+ // have a visible replacement instead of throwing during projection.
+ if (codePoint == null ||
+ codePoint <= 0 ||
+ codePoint > 0x10ffff ||
+ (codePoint >= 0xd800 && codePoint <= 0xdfff)) {
+ return '\uFFFD';
+ }
+ return codePoint == 160 ? ' ' : String.fromCharCode(codePoint);
+ }
+ return switch (match.group(2)) {
+ 'nbsp' => ' ',
+ 'amp' => '&',
+ 'lt' => '<',
+ 'gt' => '>',
+ 'quot' => '"',
+ 'apos' => "'",
+ _ => match.group(0)!,
+ };
+ },
+ );
}
bool isHtmlContentType(String? value) => _isHtmlContentType(value);
diff --git a/lib/src/core/time/stored_temporal_projection.dart b/lib/src/core/time/stored_temporal_projection.dart
new file mode 100644
index 00000000..48eb7ac0
--- /dev/null
+++ b/lib/src/core/time/stored_temporal_projection.dart
@@ -0,0 +1,70 @@
+import 'dart:convert';
+
+import '../../db/app_database.dart';
+import 'provider_date_time.dart';
+
+/// The local instant used by both the schedule and reminder navigation.
+/// CalDAV projections may resolve a resource's own VTIMEZONE definition.
+DateTime? calendarEventStartAsLocal(CalendarEvent event) => event.allDay
+ ? _date(event.startDate) ?? _date(event.startDateTime)
+ : projectedUtc(event.rawJson, 'startUtc')?.toLocal() ??
+ providerDateTimeAsLocal(event.startDateTime, event.startTimeZone);
+
+DateTime? calendarEventEndAsLocal(CalendarEvent event) => event.allDay
+ ? _date(event.endDate) ?? _date(event.endDateTime)
+ : projectedUtc(event.rawJson, 'endUtc')?.toLocal() ??
+ providerDateTimeAsLocal(event.endDateTime, event.endTimeZone);
+
+DateTime? projectedUtc(String? json, String key) {
+ final value = _metadata(json)?[key];
+ return value is String ? DateTime.tryParse(value)?.toUtc() : null;
+}
+
+/// Dates and floating times retain their written calendar day. Zoned times
+/// use the resolved instant, including custom CalDAV timezone definitions.
+DateTime? taskDueAsLocal(Task task) {
+ final metadata = _metadata(task.providerMetadataJson);
+ // CalDAV's dueUtc storage projection falls back to DTSTART without DUE.
+ final prefix = metadata?['nativeDue'] is Map ? 'due' : 'start';
+ final native = metadata?[prefix == 'due' ? 'nativeDue' : 'nativeStart'];
+ if (native is Map) {
+ final raw = native['raw'];
+ final kind = native['kind'];
+ if (raw is String) {
+ final match = RegExp(
+ r'^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})Z?)?$',
+ ).firstMatch(raw);
+ if (match != null) {
+ final date = '${match[1]}-${match[2]}-${match[3]}';
+ if (kind == 'date') return DateTime.tryParse(date);
+ final wall =
+ '$date'
+ 'T${match[4]}:${match[5]}:${match[6]}';
+ if (kind == 'floatingDateTime') return DateTime.tryParse(wall);
+ return projectedUtc(
+ task.providerMetadataJson,
+ '${prefix}Utc',
+ )?.toLocal() ??
+ providerDateTimeAsLocal(
+ wall,
+ kind == 'utcDateTime' ? 'UTC' : native['timeZoneId']?.toString(),
+ );
+ }
+ }
+ }
+ return providerDateTimeAsLocal(task.dueUtc, null);
+}
+
+Map