From fcca6644137129c9e591e1efe0ad1fa16319aa7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 10:16:46 +0100 Subject: [PATCH 1/9] Add CLAUDE.md, skills and Claude Code settings (#576) --- .claude/hooks/format.dart | 22 ++++++++++ .claude/hooks/session-start.sh | 46 ++++++++++++++++++++ .claude/settings.json | 66 ++++++++++++++++++++++++++++ .claude/skills/check/SKILL.md | 25 +++++++++++ .claude/skills/database/SKILL.md | 35 +++++++++++++++ .claude/skills/l10n/SKILL.md | 17 ++++++++ .claude/skills/preference/SKILL.md | 22 ++++++++++ .claude/skills/release/SKILL.md | 21 +++++++++ CLAUDE.md | 70 ++++++++++++++++++++++++++++++ 9 files changed, 324 insertions(+) create mode 100644 .claude/hooks/format.dart create mode 100755 .claude/hooks/session-start.sh create mode 100644 .claude/settings.json create mode 100644 .claude/skills/check/SKILL.md create mode 100644 .claude/skills/database/SKILL.md create mode 100644 .claude/skills/l10n/SKILL.md create mode 100644 .claude/skills/preference/SKILL.md create mode 100644 .claude/skills/release/SKILL.md create mode 100644 CLAUDE.md diff --git a/.claude/hooks/format.dart b/.claude/hooks/format.dart new file mode 100644 index 00000000..28577211 --- /dev/null +++ b/.claude/hooks/format.dart @@ -0,0 +1,22 @@ +// Formats the Dart file that Claude just edited (PostToolUse hook). +import 'dart:convert'; +import 'dart:io'; + +Future main() async { + final input = jsonDecode(await stdin.transform(utf8.decoder).join()); + final toolInput = input is Map ? input['tool_input'] : null; + final filePath = toolInput is Map ? toolInput['file_path'] : null; + + if (filePath is! String || !filePath.endsWith('.dart') || filePath.endsWith('.g.dart')) { + return; + } + + final environment = Platform.environment; + final root = environment['CLAUDE_PROJECT_DIR'] ?? Directory.current.path; + final absoluteRoot = Directory(root).absolute.path; + if (!File(filePath).absolute.path.startsWith(absoluteRoot)) { + return; + } + + await Process.run('dart', ['format', filePath], workingDirectory: root, runInShell: true); +} diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100755 index 00000000..38313397 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Prepares a Claude Code cloud session: Flutter SDK, dependencies and generated code. +set -euo pipefail + +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +cd "${CLAUDE_PROJECT_DIR:-.}" + +# Flutter SDK, at the version pinned in .fvmrc +version=$(sed -n 's/.*"flutter": *"\([^"]*\)".*/\1/p' .fvmrc) +sdk="$HOME/flutter-$version" +if [ ! -x "$sdk/bin/flutter" ]; then + rm -rf "$sdk.tmp" + mkdir -p "$sdk.tmp" + curl -fsSL "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_$version-stable.tar.xz" \ + | tar -xJ -C "$sdk.tmp" + mv "$sdk.tmp/flutter" "$sdk" + rm -rf "$sdk.tmp" +fi +export PATH="$sdk/bin:$PATH" +git config --global --get-all safe.directory | grep -qx "$sdk" || git config --global --add safe.directory "$sdk" +if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + echo "export PATH=\"$sdk/bin:\$PATH\"" >> "$CLAUDE_ENV_FILE" +fi +flutter --version >/dev/null 2>&1 + +# Dependencies, when the pubspec changed +mkdir -p .dart_tool +hashes=.dart_tool/.session-start-pub.sha256 +if ! sha256sum --check --status "$hashes" 2>/dev/null; then + rm -f "$hashes" + flutter pub get >/dev/null + sha256sum .claude/hooks/session-start.sh .fvmrc pubspec.yaml pubspec.lock > "$hashes" +fi + +# Generated code (*.g.dart and localizations are not committed), when the sources changed +hashes=.dart_tool/.session-start-codegen.sha256 +sources() { find lib -name '*.dart' ! -name '*.g.dart' -o -name '*.arb'; echo build.yaml l10n.yaml pubspec.lock; } +if ! sha256sum --check --status "$hashes" 2>/dev/null; then + rm -f "$hashes" + dart run build_runner build >/dev/null + flutter gen-l10n >/dev/null + sources | sort | xargs sha256sum > "$hashes" +fi diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000..78b87047 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "attribution": { "pr": "" }, + "permissions": { + "allow": [ + "Bash(flutter pub get*)", + "Bash(flutter pub deps*)", + "Bash(flutter pub outdated*)", + "Bash(flutter gen-l10n*)", + "Bash(flutter analyze*)", + "Bash(flutter test*)", + "Bash(flutter --version)", + "Bash(fvm flutter pub get*)", + "Bash(fvm flutter gen-l10n*)", + "Bash(fvm flutter analyze*)", + "Bash(fvm dart run build_runner*)", + "Bash(fvm dart analyze*)", + "Bash(fvm dart format*)", + "Bash(dart run build_runner*)", + "Bash(dart analyze*)", + "Bash(dart format*)", + "Bash(git status*)", + "Bash(git diff*)", + "Bash(git log*)", + "Bash(git show*)", + "Bash(git fetch*)", + "Bash(git branch*)", + "Bash(git ls-files*)" + ], + "ask": ["Bash(git commit*)", "Bash(git push*)", "Bash(git reset*)", "Bash(git checkout*)", "Bash(bundle exec fastlane*)"], + "deny": [ + "Read(./android/key.properties)", + "Read(./android/*.jks)", + "Read(./fastlane/*supply_key.json)", + "Read(./build/**)", + "Read(./docs/data/export_1000_notes.json)", + "Edit(./lib/**/*.g.dart)", + "Edit(./lib/l10n/app_localizations/**)", + "Edit(./.isar/**)" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|MultiEdit|Write", + "hooks": [ + { + "type": "command", + "command": "dart \"$CLAUDE_PROJECT_DIR/.claude/hooks/format.dart\"" + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\"", + "timeout": 600 + } + ] + } + ] + } +} diff --git a/.claude/skills/check/SKILL.md b/.claude/skills/check/SKILL.md new file mode 100644 index 00000000..8c5e6bd5 --- /dev/null +++ b/.claude/skills/check/SKILL.md @@ -0,0 +1,25 @@ +--- +name: check +description: Run every check of Material Notes (code generation, format, analyze, build when native or dependency changes) and fix what fails. Use before saying a change is done. +--- + +# Checks + +Run each step and fix every failure before moving on. Report the result of each step. + +1. Generate: `dart run build_runner build`, then `flutter gen-l10n`. Both must succeed without warnings about the changed files. +2. Format the committed files: `dart format -l 120 $(git ls-files '*.dart')`, plus the new files not yet tracked. Never commit a `*.g.dart` file. +3. Analyze: `dart analyze --fatal-infos`. The CI fails on infos too. +4. When `pubspec.yaml`, `android/` or a native plugin changed, and the Android SDK is available: `flutter build apk --debug`. Without the SDK, say that the build was not checked. + +There are no automated tests: describe what to try by hand in the app (screens, note types, archived and deleted notes, backup and import) for the changed behavior. + +Then re-read the diff against `CLAUDE.md`: + +- every public member has a `///` doc comment; +- relative imports inside `lib`; +- no literal user-facing string (`l10n` skill); +- the four note types and the three note statuses are all handled; +- a change to a stored or exported model follows the `database` skill. + +Never add `// ignore:` unless the existing code already does it in the same situation. diff --git a/.claude/skills/database/SKILL.md b/.claude/skills/database/SKILL.md new file mode 100644 index 00000000..ac7f7fe1 --- /dev/null +++ b/.claude/skills/database/SKILL.md @@ -0,0 +1,35 @@ +--- +name: database +description: Change a Material Notes model stored in Isar or exported in backups (notes, note types, labels), with its migration and the backward compatibility of the JSON import. +--- + +# Database and backup changes + +Users keep their notes for years and import old backups: data must never be lost. + +## Model + +- Notes: `models/note/note.dart` (common fields of the `sealed class Note`) and `models/note/types/*_note.dart` (one `part` and one Isar collection per type). Labels: `models/label/label.dart`. +- Isar annotations: `@Index()` for the fields used in `where()`, `@ignore` for the getters and the fields not stored. +- JSON (`json_serializable`, `field_rename: snake`, `explicit_to_json`): give a new field a `@JsonKey(defaultValue: ...)` so that older backups still import. Use `includeFromJson`/`includeToJson` for the fields not exported. +- Encrypted backups: a new text field of a note is encrypted too, in `encrypted(password)` and `fromJsonEncrypted(json, password)` of each type. +- The getters `contentAsText`, `contentAsMarkdown`, `contentPreview`, `isContentEmpty` must stay right for the Markdown export, the share and the tiles. +- After a change: `dart run build_runner build`. + +## Migration + +An added nullable field or a field with a default needs no migration. Anything else (moving data, new IDs, changing a type) needs one: + +1. Bump the default of `PreferenceKey.databaseVersion`. +2. Add `_migrateToVN()` to `services/migration/migration_service.dart` and call it from `migrateIfNeeded()` when `databaseVersion < N`. +3. Read, transform and put back the notes with their labels, set `databaseVersion` to N, then log with `logger.i`. + +Old models stay in `models/deprecated` while a migration reads them, and their schema stays in `Isar.open` in `database_service.dart`. A new collection is added to that list too. + +## Import + +`services/backup/backup_service.dart` imports every older format: exports without `type` (before 2.0.0, rich text only), encrypted or not, with or without labels and preferences. Keep each path working. Sample exports are in `docs/data` (`export.json`, `export_old.json`, `export_1000_notes.json`): update `export.json` when the format changes, and check by hand that the old ones still import. + +The converters of `docs/external_imports` produce this format too: update them when a required field changes. + +Then run the `check` skill. diff --git a/.claude/skills/l10n/SKILL.md b/.claude/skills/l10n/SKILL.md new file mode 100644 index 00000000..0fca8c24 --- /dev/null +++ b/.claude/skills/l10n/SKILL.md @@ -0,0 +1,17 @@ +--- +name: l10n +description: Add, rename or remove user-facing text in Material Notes (ARB files generated with flutter gen-l10n, translations from Crowdin). +--- + +# Localizations + +- `lib/l10n/translations/app_en.arb` is the source of every key. The other `app_.arb` files come from Crowdin: never edit them, unless asked, as the next Crowdin sync overwrites them. +- Each key has an `@key` entry with a `description` that says where the text is shown, and a `placeholders` entry for each placeholder, with its `type` and `description`. +- Keys are snake_case and start with their feature, like the existing ones (`settings_`, `dialog_`, `action_`, `tooltip_`, `snack_bar_`, `navigation_`...). Put a new key next to the keys of the same screen. +- Plurals use the ICU syntax: `"{count} {count, plural, one{note} other{notes}}"` with `count` of type `int`. +- `use-escaping` is on: write `''` for an apostrophe (`doesn''t`). +- Use the text with `context.l.` (`common/extensions/build_context_extension.dart`), and `context.fl` for the Flutter material localizations. +- After a change: `flutter gen-l10n`, then `dart analyze --fatal-infos`. `lib/l10n/untranslated.txt` lists the keys missing in each language; it is ignored by git. +- When renaming or removing a key, search `lib` for its old name. + +Languages are listed in `common/enums/supported_language.dart` with their completion. Adding a language also needs its Crowdin file and the README list; the maintainer does it at release time. diff --git a/.claude/skills/preference/SKILL.md b/.claude/skills/preference/SKILL.md new file mode 100644 index 00000000..9d06bc79 --- /dev/null +++ b/.claude/skills/preference/SKILL.md @@ -0,0 +1,22 @@ +--- +name: preference +description: Add or change a setting of Material Notes (PreferenceKey, preference enum, settings page tile, backup of the preferences). +--- + +# New or changed setting + +Read a similar setting first, for example `labelSwipeRightAction` in `settings_labels_page.dart`, and copy its shape. + +1. **Key**: add an entry to `common/preferences/preference_key.dart`, in its section, with its type and default value: `myPreference(false)`. + - `secure: true` for a secret (stored with `flutter_secure_storage`, read with `preferenceOrDefaultSecure`, only `String`). + - `backup: false` when the value must not be exported and imported with the notes (device-specific: paths, locks, passwords, dates). + - Stored types are `bool`, `int`, `double`, `String` and `List` only. +2. **Choices**: for a value with choices, add an enum in `common/preferences/enums` stored by its `name`, with a `fromPreference()` factory that resets a malformed value to the default (see `sort_method.dart`), and a `title(BuildContext)` for the UI. +3. **Tile**: add it to the settings page of its section (`pages/settings/pages/settings_*_page.dart`) with a `settings_tiles` widget: `SettingSwitchTile`, `SettingSingleOptionTile.detailed`, `SettingSliderTile`... with `SettingTileIcon`, a title, a description and a `SettingTileValue` when it shows a value. The handler awaits `PreferenceKey.x.set(value)` then calls `setState`. +4. **Whole-app effect**: if the setting changes the theme, the font, the scaling or the layout, add it to `WatchedPreferences` and update `preferencesProvider` from the handler. +5. **Texts**: title, description and choices with the `l10n` skill (`settings_`, `settings__description`). +6. **Read it** where it applies with `PreferenceKey.x.preferenceOrDefault`. + +Renaming or removing a key: the old value stays on the devices and in old backups. The import ignores unknown keys with a warning, so a removed key is safe; a renamed key loses the user's value unless you migrate it. + +Then run the `check` skill. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 00000000..52f699e9 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,21 @@ +--- +name: release +description: Prepare a new version of Material Notes (version bump, CHANGELOG, fastlane changelogs, languages, descriptions) following RELEASING.md. Use when asked to release or prepare vX.Y.Z. +--- + +# Prepare a version + +Follow `RELEASING.md`. Work on the version branch `vX.Y.Z` (by default the next minor version, created from `main`); the version is published when its `vX.Y.Z` pull request is merged into `main`. Never merge into `main` yourself, and never push to `main`. + +If the changes on the branch call for another version (a breaking change → `X`, a new feature after only fixes → `Y`), rename the branch and the pull request, and use the new number everywhere below. + +1. **Version**: in `pubspec.yaml`, bump `version: X.Y.Z+N` with semantic versioning and `N` the previous build number plus one. Bump the Flutter version in `.fvmrc` and `environment.flutter` together when asked. +2. **Dependencies**, when asked: `flutter pub upgrade --major-versions`, then fix the breaking changes and run the `check` skill. +3. **CHANGELOG.md**: a `## X.Y.Z - YYYY-MM-DD` section at the top, Keep a Changelog headings (`### Added`, `### Changed`, `### Fixed`, `### Removed`, `### Miscellaneous`), one short line per user-visible change, written for users. +4. **Fastlane changelogs**: `fastlane/metadata/android/en-US/changelogs/.txt` and `fr-FR/changelogs/.txt` (build 38 → `380.txt`, the fat APK version code of `android/app/build.gradle`). Upper-case headings (`ADDED`, `CHANGED`, `FIXED`, `REMOVED`, `MISCELLANEOUS`; in French `AJOUTÉ`, `MODIFIÉ`, `CORRIGÉ`, `SUPPRIMÉ`, `DIVERS`), `- ` lines, no trailing newline, at most 500 characters (`docs/google_play/changelogs_max_length.txt`). +5. **Descriptions**, when a feature changes them: `README.md` and `fastlane/metadata/android/*/full_description.yaml`, then `python scripts/generate_full_description.py` to regenerate the `full_description.txt` files. +6. **Languages**: after a Crowdin sync, update the completions in `lib/common/enums/supported_language.dart` and the README list of languages. +7. **Screenshots**: when the UI changed, update `common/constants/notes.dart` and `labels.dart`; the maintainer takes the screenshots with `--dart-define=SCREENSHOTS=true`. +8. Run the `check` skill, then open or update the `vX.Y.Z` pull request from the version branch into `main` with the CHANGELOG section as its description. + +The release workflow takes the version from `pubspec.yaml` and the GitHub release notes from the CHANGELOG section of that version: their versions must match. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2335b2ab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,70 @@ +# Material Notes + +Local, private note-taking app for Android, in Flutter (`com.maelchiotti.localmaterialnotes`, AGPL-3.0). The app has no internet permission: never add a feature that needs the network. + +Released on GitHub, the Play Store, F-Droid and IzzyOnDroid. Translations come from Crowdin. + +## Stack + +- Flutter and Dart pinned in `.fvmrc` and `pubspec.yaml` (`environment.flutter`). Locally, run Flutter through `fvm`. +- State: Riverpod 3 with code generation (`@riverpod`), plus global `ValueNotifier`s in `providers/notifiers/notifiers.dart`. +- Database: Isar (`isar_community`), one collection per note type. Native libraries are built from the `.isar` submodule for F-Droid (`scripts/isar`). +- Navigation: go_router (`navigation/router.dart`, routes in `navigation_routes.dart`). +- Editors: fleather (rich text), `flutter_markdown_plus` (Markdown), `flutter_checklist` (checklist), `TextField` (plain text). +- Settings UI: `settings_tiles`. `flag_secure` sets `FLAG_SECURE`. + +`settings_tiles`, `flutter_checklist` and `flag_secure` are the maintainer's own packages (`maelchiotti/`), used from pub.dev. To try a change of one of them in the app, add a `dependency_overrides` with a `path` to its checkout, and never commit it: publish the package, then bump its version in `pubspec.yaml`. + +## Commands + +The `*.g.dart` files and `lib/l10n/app_localizations/` are generated and not committed: generate them before analyzing or building. + +- Install: `flutter pub get` +- Generate code: `dart run build_runner build` (models, providers). Use `watch` while developing. +- Generate localizations: `flutter gen-l10n` +- Analyze: `dart analyze --fatal-infos` (CI, after both generations) +- Format: `dart format -l 120 ` (CI: `dart format -l 120 --set-exit-if-changed .` on a checkout without the generated files, so format only the committed files: `dart format -l 120 $(git ls-files '*.dart')`) +- Run: `flutter run` (debug), `--dart-define=SCREENSHOTS=true` to fill the database with the screenshot notes of `common/constants/notes.dart` and `labels.dart`. +- Build: `flutter build apk --release` (`--split-per-abi` for the per-ABI APKs). Needs `android/key.properties` and the keystore, which are only in the CI. + +There are no automated tests. The `check` skill runs every check. + +## Architecture + +`pages` (UI) → `common/actions` (user actions: confirmation, provider call, snack bar with undo) → `providers` (state) → `services` (database) → `models`. + +- `models/note/note.dart`: `sealed class Note` with the parts `types/plain_text_note.dart`, `markdown_note.dart`, `rich_text_note.dart`, `checklist_note.dart`. Each type is an Isar `@Collection()` and a `@JsonSerializable()`. A note is available, archived or deleted (`NoteStatus`), never archived and deleted at once. `id` is a UUID, `isarId` its hash. +- `models/label/label.dart`: labels, linked to the notes by `IsarLinks`. +- `services/*_service.dart`: singletons (`factory X() => _singleton`) with `ensureInitialized()`, called in order from `main.dart` and `DatabaseService`. `NotesService` queries the four collections and concatenates the results. +- `services/migration/migration_service.dart`: database migrations, keyed by `PreferenceKey.databaseVersion`. +- `services/backup`: JSON import and export (optionally encrypted with `EncryptionUtils`), Markdown export in a ZIP, automatic export. The JSON format must stay importable by every later version. +- `providers/notes/notes_provider.dart`: `notesProvider(status:, label:)`, `keepAlive`. A method updates the database through the service, then the state, then the other providers that show the same notes (unlabeled, labeled). It catches and logs the errors and returns `false`. +- `common/preferences`: `PreferenceKey` enum (default value, `secure`, `backup`), read with `preferenceOrDefault`, written with `set`. Preferences with choices are enums in `preferences/enums` with a `fromPreference()` factory that resets a malformed value. `WatchedPreferences` holds the ones that rebuild the whole app. +- `common/constants`: `Paddings`, `Sizes`, `Separator`; the global `logger` (`AppLogger`, console in debug, file in release); `rootNavigatorKey`. + +## Code style + +- Formatter width 120. Relative imports inside `lib` (`prefer_relative_imports`). +- Every public member has a `///` doc comment (`public_member_api_docs`). Enums of preferences and routes use `// ignore_for_file: public_member_api_docs`. +- Control bodies on their own line, always with braces. No unawaited future: `await` it or wrap it in `unawaited(...)`. +- Text shown to the user goes through `context.l.` (`BuildContextExtension`), never a literal string. See the `l10n` skill. +- Log caught exceptions with `logger.e(exception.toString(), exception, stackTrace)`; warnings with `logger.w`. +- Use the existing `askForConfirmation`, `SnackBarUtils().show` and placeholders (`EmptyPlaceholder`, `ErrorPlaceholder`, `LoadingPlaceholder`) instead of new ones. +- Use `EdgeInsetsDirectional` through `Paddings`, so that right-to-left languages are laid out correctly. + +## Git and releases + +- `main` only receives releases. Never push to `main`, and never merge into it a pull request other than a release. +- Work goes into a version branch named `vX.Y.Z`. By default it is the next minor version (`2.2.2` → `v2.3.0`); create it from `main` if it doesn't exist yet. When the changes grow (a breaking change, a major feature), rename the branch to bump `Y` or `X`, and keep `pubspec.yaml`, the CHANGELOG and the open pull requests in line with it. +- Each change has a GitHub issue, a branch `fix/_` or `feat/_` from the version branch, and a pull request into the version branch titled `[] `. +- Every issue and pull request has a label for its kind: `bug`, `enhancement`, `localization`, `documentation`, `tests` or `style`, and `release` for a version pull request. A pull request gets the label of its issue. +- The release is a pull request from the version branch into `main`, titled `vX.Y.Z` and squash-merged. Every push to `main` runs the release workflow (Play Store and GitHub release). The `release` skill prepares it. +- CI on each push: analyze and format (`.github/workflows/dart.yaml`). On each pull request: build the Isar libraries and the APK (`build.yaml`). + +## Skills + +- `check`: every check before saying a change is done. +- `l10n`: add or change user-facing text. +- `preference`: add a setting. +- `database`: change a model stored in Isar or exported in backups. +- `release`: prepare a new version. From 768271f0215e3634ce73836b64ef1414105246af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:15:05 +0100 Subject: [PATCH 2/9] [582] Fix editor and labels inconsistencies (#583) --- lib/common/actions/notes/add.dart | 16 +++++++++--- lib/common/actions/notes/delete.dart | 2 +- lib/common/actions/notes/pop.dart | 8 ++++-- lib/pages/editor/editor_page.dart | 13 ++++++++-- .../widgets/editors/markdown_editor.dart | 14 ++++++++++- .../widgets/editors/plain_text_editor.dart | 7 ++++++ .../widgets/editors/rich_text_editor.dart | 16 +++++++++++- .../editor/widgets/editors/title_editor.dart | 25 ++++++++++++++----- lib/pages/notes/notes_page.dart | 6 ++++- lib/pages/notes/widgets/add_note_fab.dart | 6 +++-- lib/providers/notes/notes_provider.dart | 10 ++++---- 11 files changed, 99 insertions(+), 24 deletions(-) diff --git a/lib/common/actions/notes/add.dart b/lib/common/actions/notes/add.dart index 78e7ee9d..95aa3890 100644 --- a/lib/common/actions/notes/add.dart +++ b/lib/common/actions/notes/add.dart @@ -45,9 +45,19 @@ Future<void> addNote(BuildContext context, WidgetRef ref, {required NoteType not note = ChecklistNote.empty(); } - // If some content was provided, then immediately save the note without waiting for changes in the editor - if (content != null) { - await ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + final label = currentLabelFilter; + final notesNotifier = ref.read(notesProvider(status: NoteStatus.available, label: label).notifier); + + // If some content was provided, or if the note is created from the page of a label, + // then immediately save the note without waiting for changes in the editor + // (the label can only be added to a note that is already saved in the database) + if (content != null || label != null) { + await notesNotifier.edit(note); + } + + // If the note is created from the page of a label, add that label to the note + if (label != null) { + await notesNotifier.editLabels(note, [label]); } currentNoteNotifier.value = note; diff --git a/lib/common/actions/notes/delete.dart b/lib/common/actions/notes/delete.dart index 53f0b4ab..26b2bd26 100644 --- a/lib/common/actions/notes/delete.dart +++ b/lib/common/actions/notes/delete.dart @@ -86,7 +86,7 @@ Future<bool> deleteNotes(BuildContext context, WidgetRef ref, {required List<Not context, text: context.l.snack_bar_deleted(notes.length), onCancel: (globalRef) async => wereArchived - ? await archiveNotes(rootNavigatorKey.currentContext!, ref, notes: notes) + ? await archiveNotes(rootNavigatorKey.currentContext!, globalRef, notes: notes, cancel: false) : await restoreNotes(rootNavigatorKey.currentContext!, globalRef, notes: notes, cancel: false), ); } diff --git a/lib/common/actions/notes/pop.dart b/lib/common/actions/notes/pop.dart index 7f9b3585..d6c39d85 100644 --- a/lib/common/actions/notes/pop.dart +++ b/lib/common/actions/notes/pop.dart @@ -1,8 +1,12 @@ import '../../constants/constants.dart'; /// Closes the expandable FAB to add a note if it is open. +/// +/// Handles both the FAB of the notes page and the one of the label pages. void closeAddNoteFabIfOpen() { - if (addNoteFabKey.currentState != null && addNoteFabKey.currentState!.isOpen) { - addNoteFabKey.currentState!.toggle(); + for (final fabKey in [addNoteFabKey, labeledAddNoteFabKey]) { + if (fabKey.currentState != null && fabKey.currentState!.isOpen) { + fabKey.currentState!.toggle(); + } } } diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart index bdb88a3f..dc769390 100644 --- a/lib/pages/editor/editor_page.dart +++ b/lib/pages/editor/editor_page.dart @@ -68,6 +68,13 @@ class _EditorState extends ConsumerState<EditorPage> { globalRef.read(notesProvider(status: NoteStatus.available).notifier).removeEmpty(); // ignore: avoid_ref_inside_state_dispose globalRef.read(notesProvider(status: NoteStatus.archived).notifier).removeEmpty(); + if (currentLabelFilter != null) { + // ignore: avoid_ref_inside_state_dispose + globalRef.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).removeEmpty(); + } + + fleatherControllerNotifier.value = null; + _fleatherController?.dispose(); super.dispose(); } @@ -84,7 +91,9 @@ class _EditorState extends ConsumerState<EditorPage> { } void save(Note note, [WidgetRef? widgetRef]) { - (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + // Archived notes are not filtered by label + final label = note.archived ? null : currentLabelFilter; + (widgetRef ?? ref).read(notesProvider(status: note.status, label: label).notifier).edit(note); pendingNote = null; } @@ -129,7 +138,7 @@ class _EditorState extends ConsumerState<EditorPage> { final enableLabels = PreferenceKey.enableLabels.preferenceOrDefault; final showLabelsListInEditorPage = PreferenceKey.showLabelsListInEditorPage.preferenceOrDefault; - final readOnly = !currentNote.deleted && showEditorModeButton && !isEditorInEditMode; + final readOnly = currentNote.deleted || (showEditorModeButton && !isEditorInEditMode); final autofocus = widget.isNewNote && !focusTitleOnNewNote; final showLabelsList = enableLabels && showLabelsListInEditorPage && currentNote.labelsVisibleSorted.isNotEmpty; diff --git a/lib/pages/editor/widgets/editors/markdown_editor.dart b/lib/pages/editor/widgets/editors/markdown_editor.dart index d287b42e..3b278020 100644 --- a/lib/pages/editor/widgets/editors/markdown_editor.dart +++ b/lib/pages/editor/widgets/editors/markdown_editor.dart @@ -47,6 +47,13 @@ class _MarkdownEditorState extends ConsumerState<MarkdownEditor> { contentTextController = TextEditingController(text: widget.note.content); } + @override + void dispose() { + contentTextController.dispose(); + + super.dispose(); + } + void onChanged() { final note = widget.note..content = contentTextController.text; @@ -58,7 +65,12 @@ class _MarkdownEditorState extends ConsumerState<MarkdownEditor> { return; } - Uri uri = Uri.parse(href); + final uri = Uri.tryParse(href); + if (uri == null) { + logger.w('The URL opened in the Markdown editor could not be parsed: $href'); + + return; + } launchUrl(uri); } diff --git a/lib/pages/editor/widgets/editors/plain_text_editor.dart b/lib/pages/editor/widgets/editors/plain_text_editor.dart index e62af973..32edbe50 100644 --- a/lib/pages/editor/widgets/editors/plain_text_editor.dart +++ b/lib/pages/editor/widgets/editors/plain_text_editor.dart @@ -44,6 +44,13 @@ class _PlainTextEditorState extends ConsumerState<PlainTextEditor> { contentTextController = TextEditingController(text: widget.note.content); } + @override + void dispose() { + contentTextController.dispose(); + + super.dispose(); + } + void onChanged() { final note = widget.note..content = contentTextController.text; diff --git a/lib/pages/editor/widgets/editors/rich_text_editor.dart b/lib/pages/editor/widgets/editors/rich_text_editor.dart index 1e32b90c..0fa0c7f9 100644 --- a/lib/pages/editor/widgets/editors/rich_text_editor.dart +++ b/lib/pages/editor/widgets/editors/rich_text_editor.dart @@ -50,6 +50,9 @@ class _RichTextEditorState extends ConsumerState<RichTextEditor> { late StreamSubscription<ParchmentChange> changes; Timer? saveDebounce; + /// Timer to update the undo and redo actions once fleather has updated its history. + Timer? undoRedoTimer; + @override void initState() { super.initState(); @@ -72,9 +75,19 @@ class _RichTextEditorState extends ConsumerState<RichTextEditor> { launchUrl(uri); } - void onChanged() { + /// Updates whether the undo and redo actions are available. + void updateUndoRedo() { fleatherControllerCanUndoNotifier.value = widget.fleatherController.canUndo; fleatherControllerCanRedoNotifier.value = widget.fleatherController.canRedo; + } + + void onChanged() { + updateUndoRedo(); + + // Fleather adds the changes to its history after a delay (its `throttleDuration` of 500 milliseconds), + // so the undo and redo actions must be updated again after that delay + undoRedoTimer?.cancel(); + undoRedoTimer = Timer(const Duration(milliseconds: 600), updateUndoRedo); final content = jsonEncode(widget.fleatherController.document.toJson()); final note = widget.note..content = content; @@ -85,6 +98,7 @@ class _RichTextEditorState extends ConsumerState<RichTextEditor> { @override void dispose() { changes.cancel(); + undoRedoTimer?.cancel(); super.dispose(); } diff --git a/lib/pages/editor/widgets/editors/title_editor.dart b/lib/pages/editor/widgets/editors/title_editor.dart index cbec5fc3..12ac7ebb 100644 --- a/lib/pages/editor/widgets/editors/title_editor.dart +++ b/lib/pages/editor/widgets/editors/title_editor.dart @@ -5,7 +5,6 @@ import '../../../../common/constants/paddings.dart'; import '../../../../common/extensions/build_context_extension.dart'; import '../../../../common/preferences/preference_key.dart'; import '../../../../models/note/note.dart'; -import '../../../../models/note/note_status.dart'; import '../../../../providers/notes/notes_provider.dart'; import '../../../../providers/notifiers/notifiers.dart'; @@ -29,7 +28,21 @@ class TitleEditor extends ConsumerStatefulWidget { class _TitleEditorState extends ConsumerState<TitleEditor> { /// Controller of the title of the note. - late TextEditingController titleController; + late final TextEditingController titleController; + + @override + void initState() { + super.initState(); + + titleController = TextEditingController(text: currentNoteNotifier.value?.title); + } + + @override + void dispose() { + titleController.dispose(); + + super.dispose(); + } /// Saves the [newTitle] of the [note] in the database. void onChanged(Note? note, String? newTitle) { @@ -39,7 +52,9 @@ class _TitleEditorState extends ConsumerState<TitleEditor> { note.title = newTitle; - ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + // Archived notes are not filtered by label + final label = note.archived ? null : currentLabelFilter; + ref.read(notesProvider(status: note.status, label: label).notifier).edit(note); } @override @@ -54,8 +69,6 @@ class _TitleEditorState extends ConsumerState<TitleEditor> { child: ValueListenableBuilder( valueListenable: currentNoteNotifier, builder: (context, currentNote, child) { - titleController = TextEditingController(text: currentNote?.title); - return TextField( readOnly: widget.readOnly, autofocus: widget.isNewNote && focusTitleOnNewNote, @@ -65,7 +78,7 @@ class _TitleEditorState extends ConsumerState<TitleEditor> { decoration: InputDecoration.collapsed(hintText: context.l.hint_title), controller: titleController, onChanged: (text) => onChanged(currentNote, text), - onSubmitted: (_) => widget.onSubmitted, + onSubmitted: (_) => widget.onSubmitted(), ); }, ), diff --git a/lib/pages/notes/notes_page.dart b/lib/pages/notes/notes_page.dart index 33bcc591..aa9e587b 100644 --- a/lib/pages/notes/notes_page.dart +++ b/lib/pages/notes/notes_page.dart @@ -44,7 +44,11 @@ class _NotesPageState extends ConsumerState<NotesPage> { @override void dispose() { - currentLabelFilter = null; + // When navigating from a label to another, the new page is created before this one is disposed: + // only reset the filter if it is still the one of this page + if (currentLabelFilter == widget.label) { + currentLabelFilter = null; + } super.dispose(); } diff --git a/lib/pages/notes/widgets/add_note_fab.dart b/lib/pages/notes/widgets/add_note_fab.dart index 9955e56b..8a53ede8 100644 --- a/lib/pages/notes/widgets/add_note_fab.dart +++ b/lib/pages/notes/widgets/add_note_fab.dart @@ -32,9 +32,11 @@ class _AddNoteFabState extends ConsumerState<AddNoteFab> { } void onPressed(NoteType noteType) { - addNote(context, ref, noteType: noteType); - + // Close the FAB before opening the editor, otherwise closing it during the transition removes the focus + // from the editor and closes the keyboard closeAddNoteFabIfOpen(); + + addNote(context, ref, noteType: noteType); } @override diff --git a/lib/providers/notes/notes_provider.dart b/lib/providers/notes/notes_provider.dart index a22fceb6..1058f7b9 100644 --- a/lib/providers/notes/notes_provider.dart +++ b/lib/providers/notes/notes_provider.dart @@ -164,8 +164,8 @@ class Notes extends _$Notes { Future<bool> toggleLock(List<Note> notesToToggle) async { _checkStatus([NoteStatus.available, NoteStatus.archived, NoteStatus.deleted]); - // Ignore empty notes - notesToToggle.removeWhere((note) => note.isEmpty); + // Ignore empty notes (in a copy, to leave the list of the caller unchanged) + notesToToggle = notesToToggle.where((note) => !note.isEmpty).toList(); for (final note in notesToToggle) { note.locked = !note.locked; @@ -193,8 +193,8 @@ class Notes extends _$Notes { Future<bool> setArchived(List<Note> notesToSet, bool archived) async { _checkStatus([NoteStatus.available, NoteStatus.archived]); - // Ignore empty notes - notesToSet.removeWhere((note) => note.isEmpty); + // Ignore empty notes (in a copy, to leave the list of the caller unchanged) + notesToSet = notesToSet.where((note) => !note.isEmpty).toList(); for (final note in notesToSet) { note.pinned = false; @@ -227,7 +227,7 @@ class Notes extends _$Notes { for (final note in notesToSet) { note.pinned = false; note.archived = false; - note.deleted = !note.deleted; + note.deleted = deleted; note.deletedTime = deleted ? DateTime.timestamp() : null; } From b624ff013f2e97c954294146e1a16b578b18a675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:15:14 +0100 Subject: [PATCH 3/9] [580] Fix locked notes and labels readable without authentication (#581) --- lib/common/widgets/notes/note_tile.dart | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/common/widgets/notes/note_tile.dart b/lib/common/widgets/notes/note_tile.dart index 0daf40b7..c3759931 100644 --- a/lib/common/widgets/notes/note_tile.dart +++ b/lib/common/widgets/notes/note_tile.dart @@ -174,10 +174,8 @@ class _NoteTileState extends ConsumerState<NoteTile> { // Close the expandable FAB to add a note if it is open closeAddNoteFabIfOpen(); - final lockNote = PreferenceKey.lockNote.preferenceOrDefault; - if (lockNote) { - lockNoteNotifier.value = widget.note.locked || widget.note.hasLockedLabel; - } + // Lock the editor if the note is locked or has a locked label, depending on the settings + lockNoteNotifier.value = widget.note.requiresAuthentication; final EditorPageExtra extra = (isNewNote: false); context.pushNamed(NavigationRoute.editor.name, extra: extra); @@ -211,15 +209,16 @@ class _NoteTileState extends ConsumerState<NoteTile> { final layout = ref.watch(preferencesProvider.select((preferences) => preferences.layout)); + // Do not show only the title and the preview content is not empty + final showPreview = !showTitlesOnly && !widget.note.isContentPreviewEmpty; + // Show only the title but the title is empty + final showContentInsteadOfEmptyTitle = showTitlesOnly && widget.note.isTitleEmpty; + // In search view, do not show only the title and the preview content is not empty + final showInSearchView = + widget.search != null && showTitlesOnlyDisableInSearchView && !widget.note.isContentPreviewEmpty; + // Never show the content of a note that is locked or has a locked label final showContent = - // The note is locked - !lockNote && - // Do not show only the title and the preview content is not empty - (!showTitlesOnly && !widget.note.isContentPreviewEmpty) || - // Show only the title and the title is not empty - (showTitlesOnly && widget.note.isTitleEmpty) || - // In search view, do not show only the title and the preview content is not empty - (widget.search != null && showTitlesOnlyDisableInSearchView && !widget.note.isContentPreviewEmpty); + !widget.note.requiresAuthentication && (showPreview || showContentInsteadOfEmptyTitle || showInSearchView); var titleStyle = biggerTitles ? Theme.of(context).textTheme.titleLarge : Theme.of(context).textTheme.titleMedium; titleStyle ??= TextStyle(); From 79d63126761c7052993f6d3ae8ed4b8dab76fc81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:15:21 +0100 Subject: [PATCH 4/9] [578] Fix notes and backups lost silently (#579) --- lib/app.dart | 6 ++--- lib/common/constants/constants.dart | 5 ++++ lib/common/system_utils.dart | 11 +++++++- lib/models/note/note.dart | 1 - lib/models/note/types/rich_text_note.dart | 11 +++++--- lib/pages/editor/editor_page.dart | 27 +++++++++++++++----- lib/services/backup/auto_backup_service.dart | 7 +++-- lib/services/backup/backup_service.dart | 11 ++++++-- lib/services/bin/bin_service.dart | 4 ++- 9 files changed, 62 insertions(+), 21 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 6972a28f..afaeb340 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -85,7 +85,7 @@ class _AppState extends ConsumerState<App> with AfterLayoutMixin<App> { if (lockApp) { final lockAppDelayPreference = PreferenceKey.lockAppDelay.preferenceOrDefault; - if (lockAppDelayPreference != -1) { + if (lockAppDelayPreference != neverDelay && lockAppDelayPreference != -1) { final lockAppDelay = Duration(seconds: lockAppDelayPreference); final timeSinceBackground = now.difference(lastForegroundTimestamp); if (timeSinceBackground > lockAppDelay) { @@ -100,8 +100,8 @@ class _AppState extends ConsumerState<App> with AfterLayoutMixin<App> { if (lockNote || lockLabel) { final lockNoteDelayPreference = PreferenceKey.lockNoteDelay.preferenceOrDefault; - if (lockNoteDelayPreference != -1) { - final lockNoteDelay = Duration(seconds: PreferenceKey.lockNoteDelay.preferenceOrDefault); + if (lockNoteDelayPreference != neverDelay && lockNoteDelayPreference != -1) { + final lockNoteDelay = Duration(seconds: lockNoteDelayPreference); final timeSinceBackground = now.difference(lastForegroundTimestamp); if (timeSinceBackground > lockNoteDelay) { lockNoteNotifier.lock(); diff --git a/lib/common/constants/constants.dart b/lib/common/constants/constants.dart index 970a2dcc..5b72e911 100644 --- a/lib/common/constants/constants.dart +++ b/lib/common/constants/constants.dart @@ -38,6 +38,11 @@ final safStream = SafStream(); /// The value to apply to the alpha channel to get a subdued color. const subduedAlpha = 150; +/// Value of the delay settings (automatic bin emptying, application and note locks) that means "never". +/// +/// It is the last value of [autoRemoveFromBinValues] and [lockDelayValues], shown as "Never" in the settings. +const neverDelay = 999; + /// Frequency of the automatic bin emptying. const autoRemoveFromBinValues = [7.0, 14.0, 30.0, 90.0, 180.0, 365.0, 999.0]; diff --git a/lib/common/system_utils.dart b/lib/common/system_utils.dart index 81af8d5a..d2868585 100644 --- a/lib/common/system_utils.dart +++ b/lib/common/system_utils.dart @@ -5,6 +5,7 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flag_secure/flag_secure.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:listen_sharing_intent/listen_sharing_intent.dart'; import 'package:local_auth/local_auth.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -121,9 +122,17 @@ class SystemUtils { } /// Sets `FLAG_SECURE` to `true` if the corresponding preference was enabled by the user. + /// + /// A failure is only logged, because this is called before the application starts. Future<void> setFlagSecureIfNeeded() async { - if (PreferenceKey.flagSecure.preferenceOrDefault) { + if (!PreferenceKey.flagSecure.preferenceOrDefault) { + return; + } + + try { await FlagSecure.set(); + } on PlatformException catch (exception, stackTrace) { + logger.e(exception.toString(), exception, stackTrace); } } diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index ce83537e..41013ca1 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -11,7 +11,6 @@ import '../../../common/constants/constants.dart'; import '../../common/files/encryption_utils.dart'; import '../../common/preferences/enums/sort_method.dart'; import '../../common/preferences/preference_key.dart'; -import '../../services/notes/notes_service.dart'; import '../label/label.dart'; import 'note_status.dart'; import 'types/note_type.dart'; diff --git a/lib/models/note/types/rich_text_note.dart b/lib/models/note/types/rich_text_note.dart index f11eb5c0..0e1e553d 100644 --- a/lib/models/note/types/rich_text_note.dart +++ b/lib/models/note/types/rich_text_note.dart @@ -81,16 +81,19 @@ class RichTextNote extends Note { } /// Document containing the fleather content representation. + /// + /// If the [content] can't be decoded, returns a document with the raw [content] as plain text, + /// so that it is still shown and never lost. @ignore ParchmentDocument get document { try { return ParchmentDocument.fromJson(jsonDecode(content) as List); } catch (exception, stackTrace) { - logger.e('Failed to decode the content of a rich text note, deleting it.', exception, stackTrace); - - NotesService().delete(this); + logger.e('Failed to decode the content of a rich text note, showing it as plain text', exception, stackTrace); - return ParchmentDocument(); + return ParchmentDocument.fromJson([ + {'insert': '$content\n'}, + ]); } } diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart index dc769390..d6185183 100644 --- a/lib/pages/editor/editor_page.dart +++ b/lib/pages/editor/editor_page.dart @@ -43,10 +43,15 @@ class _EditorState extends ConsumerState<EditorPage> { FleatherController? _fleatherController; RichTextNote? _fleatherControllerNote; + /// Saves the pending changes when the application goes to the background, where it can be killed. + late final AppLifecycleListener appLifecycleListener; + @override void initState() { super.initState(); + appLifecycleListener = AppLifecycleListener(onPause: savePendingNote); + // If this is a new note, force the editor to be in edit mode on first build if (widget.isNewNote) { isEditModeNotifier.value = true; @@ -55,14 +60,9 @@ class _EditorState extends ConsumerState<EditorPage> { @override void dispose() { - // If a save was waiting to happen, save immediately - if (saveDebounce?.isActive ?? false) { - saveDebounce!.cancel(); + appLifecycleListener.dispose(); - if (pendingNote != null) { - save(pendingNote!, globalRef); - } - } + savePendingNote(globalRef); // ignore: avoid_ref_inside_state_dispose globalRef.read(notesProvider(status: NoteStatus.available).notifier).removeEmpty(); @@ -83,6 +83,19 @@ class _EditorState extends ConsumerState<EditorPage> { editorFocusNode.requestFocus(); } + /// If a save is waiting to happen, performs it immediately. + void savePendingNote([WidgetRef? widgetRef]) { + if (!(saveDebounce?.isActive ?? false)) { + return; + } + + saveDebounce!.cancel(); + + if (pendingNote != null) { + save(pendingNote!, widgetRef); + } + } + void scheduleSave(Note note) { pendingNote = note; diff --git a/lib/services/backup/auto_backup_service.dart b/lib/services/backup/auto_backup_service.dart index e842755f..265d1547 100644 --- a/lib/services/backup/auto_backup_service.dart +++ b/lib/services/backup/auto_backup_service.dart @@ -122,8 +122,11 @@ class AutoExportUtils { final encrypt = PreferenceKey.autoExportEncryption.preferenceOrDefault; final password = await PreferenceKey.autoExportPassword.preferenceOrDefaultSecure; - unawaited(ManualBackupService().autoExportAsJson(encrypt, password)); + final exported = await ManualBackupService().autoExportAsJson(encrypt, password); - unawaited(PreferenceKey.lastAutoExportDate.set(DateTime.now().toIso8601String())); + // Only save the date of a successful export, so that a failed one is tried again at the next startup + if (exported) { + await PreferenceKey.lastAutoExportDate.set(DateTime.now().toIso8601String()); + } } } diff --git a/lib/services/backup/backup_service.dart b/lib/services/backup/backup_service.dart index 4e3415f7..4c42a9db 100644 --- a/lib/services/backup/backup_service.dart +++ b/lib/services/backup/backup_service.dart @@ -245,12 +245,19 @@ class ManualBackupService { required String directory, required String fileName, }) async { + // Never write the notes in plain text when the encryption was requested + if (encrypt && (password == null || password.isEmpty)) { + logger.w('The export was not performed because the encryption was enabled without a password'); + + return false; + } + final version = SystemUtils().appVersion; final buildNumber = SystemUtils().buildNumber; var notes = await _notesService.getAll(); - if (encrypt && password != null && password.isNotEmpty) { - notes = notes.map((note) => note.encrypted(password)).toList(); + if (encrypt) { + notes = notes.map((note) => note.encrypted(password!)).toList(); } final labels = await _labelsService.getAll(); diff --git a/lib/services/bin/bin_service.dart b/lib/services/bin/bin_service.dart index 2a4429e9..420fafae 100644 --- a/lib/services/bin/bin_service.dart +++ b/lib/services/bin/bin_service.dart @@ -1,3 +1,4 @@ +import '../../common/constants/constants.dart'; import '../../common/preferences/preference_key.dart'; import '../notes/notes_service.dart'; @@ -8,7 +9,8 @@ class BinService { /// Removes the notes from the bin if they have been deleted longer than the corresponding setting, it enabled. Future<void> removeNotesFromBinIfNeeded() async { final autoRemoveFromBinDelayPreference = PreferenceKey.autoRemoveFromBinDelay.preferenceOrDefault; - if (autoRemoveFromBinDelayPreference == -1) { + // The "Never" value is stored as 999, older versions used -1 + if (autoRemoveFromBinDelayPreference == neverDelay || autoRemoveFromBinDelayPreference == -1) { return; } From 65d4c67e0dbdfd634fdac8917c95d96a2fd182ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:28:31 +0100 Subject: [PATCH 5/9] [584] Fix partial backup restore (#585) --- lib/models/note/note.dart | 2 +- .../settings/pages/settings_backup_page.dart | 1 + lib/services/backup/backup_service.dart | 84 ++++++++----------- lib/services/labels/labels_service.dart | 10 --- lib/services/notes/notes_service.dart | 31 +++++++ 5 files changed, 69 insertions(+), 59 deletions(-) diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index 41013ca1..8da8d2f6 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -57,7 +57,7 @@ sealed class Note implements Comparable<Note> { bool pinned; /// Whether the note is locked. - @JsonKey(includeFromJson: false, includeToJson: false) + @JsonKey(defaultValue: false) bool locked; /// The time when the note was created. diff --git a/lib/pages/settings/pages/settings_backup_page.dart b/lib/pages/settings/pages/settings_backup_page.dart index 82d093cb..12609b7c 100644 --- a/lib/pages/settings/pages/settings_backup_page.dart +++ b/lib/pages/settings/pages/settings_backup_page.dart @@ -49,6 +49,7 @@ class _SettingsBackupPageState extends ConsumerState<SettingsBackupPage> { await ref.read(labelsListProvider.notifier).get(); await ref.read(labelsNavigationProvider.notifier).get(); await ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).get(); + await ref.read(notesProvider(status: NoteStatus.archived).notifier).get(); await ref.read(notesProvider(status: NoteStatus.deleted).notifier).get(); ref.read(preferencesProvider.notifier).reset(); diff --git a/lib/services/backup/backup_service.dart b/lib/services/backup/backup_service.dart index 4c42a9db..00dde281 100644 --- a/lib/services/backup/backup_service.dart +++ b/lib/services/backup/backup_service.dart @@ -55,24 +55,16 @@ class ManualBackupService { importedJson = importedJson as Map<String, dynamic>; - // Import the labels - final importLabels = importedJson.containsKey("labels") && importedJson.getList<dynamic>("labels").isNotEmpty; - if (importLabels) { - final labelsAsJson = importedJson.getList<dynamic>("labels"); - final importedLabels = await _importLabels(labelsAsJson); - if (!importedLabels) { - return false; - } - } - if (!context.mounted) { return false; } - // Import the notes + // Import the labels and the notes + final labelsAsJson = (importedJson['labels'] as List?) ?? []; + final labels = labelsAsJson.map((labelAsJson) => Label.fromJson(labelAsJson)).toList(); final notesAsJson = importedJson.getList<dynamic>("notes"); final encrypted = importedJson.tryGetBool("encrypted") ?? false; - final importedNotes = await _importNotes(context, notesAsJson, encrypted, importLabels); + final importedNotes = await _importNotes(context, notesAsJson, encrypted, labels); if (!importedNotes) { return false; } @@ -89,17 +81,12 @@ class ManualBackupService { return true; } - Future<bool> _importLabels(List<dynamic> labelsAsJson) async { - final labels = labelsAsJson.map((labelAsJson) => Label.fromJson(labelAsJson)).toList(); - - await _labelsService.putAllNew(labels); - - return true; - } - - Future<bool> _importNotes(BuildContext context, List<dynamic> notesAsJson, bool encrypted, bool importLabels) async { + Future<bool> _importNotes(BuildContext context, List<dynamic> notesAsJson, bool encrypted, List<Label> labels) async { List<Note> notes = []; - List<List<Label>> notesLabels = []; + + for (final noteAsJson in notesAsJson) { + _fixStatus(noteAsJson); + } if (encrypted && context.mounted) { final password = await showAdaptiveDialog<String>( @@ -181,27 +168,30 @@ class ManualBackupService { } } - // Handle notes labels - if (importLabels) { - List<Label> databaseLabels = await _labelsService.getAll(); - - for (final noteAsJson in notesAsJson) { - final labelsString = noteAsJson['labels'] as List; - final labels = databaseLabels.where((label) => labelsString.contains(label.name)).toList(); - - notesLabels.add(labels); + // Link the notes to their labels only if the file contains labels + final notesLabelsNames = notesAsJson.map((noteAsJson) { + if (labels.isEmpty) { + return <String>[]; } - } - // Add notes and labels to the database - await _notesService.putAll(notes); - if (importLabels) { - await _notesService.putAllLabels(notes, notesLabels); - } + return ((noteAsJson['labels'] as List?) ?? []).cast<String>(); + }).toList(); + + // Add the labels, the notes and their links to the database at once + await _notesService.import(labels, notes, notesLabelsNames); return true; } + /// Unarchives the note of [noteAsJson] if it is also deleted, which can only happen if the file was edited by hand. + void _fixStatus(dynamic noteAsJson) { + if (noteAsJson['archived'] == true && noteAsJson['deleted'] == true) { + logger.w('Imported a note that was both archived and deleted as only deleted'); + + noteAsJson['archived'] = false; + } + } + void _addRichTextNote(dynamic noteAsJson, List<Note> notes, String warningMessage) { if (RichTextNote.isFleatherData(noteAsJson['content'] as String)) { notes.add(RichTextNote.fromJson(noteAsJson)); @@ -215,23 +205,21 @@ class ManualBackupService { Future<bool> _importPreferences(dynamic importedJson) async { final preferencesAndValues = importedJson['preferences'] as Map<String, dynamic>; - preferencesAndValues.forEach((preference, value) { + for (final MapEntry(key: preference, :value) in preferencesAndValues.entries) { final preferenceKey = PreferenceKey.values.byNameOrNull(preference); if (preferenceKey == null) { logger.w("While restoring the preferences from JSON, the preference '$preference' doesn't exist"); - } else { - if (preferenceKey.backup) { - // If the preference value is a list, then it's a list of String - // (the only type of list that can be stored in the preferences) - if (value is List) { - preferenceKey.set(value.cast<String>()); - } else { - preferenceKey.set(value); - } + } else if (preferenceKey.backup) { + // If the preference value is a list, then it's a list of String + // (the only type of list that can be stored in the preferences) + if (value is List) { + await preferenceKey.set(value.cast<String>()); + } else { + await preferenceKey.set(value); } } - }); + } return true; } diff --git a/lib/services/labels/labels_service.dart b/lib/services/labels/labels_service.dart index f32a82a7..fabbbf16 100644 --- a/lib/services/labels/labels_service.dart +++ b/lib/services/labels/labels_service.dart @@ -67,16 +67,6 @@ class LabelsService { }); } - /// Puts the [labels] in the database only if they do not exist already. - Future<void> putAllNew(List<Label> labels) async { - final databaseLabels = await getAll(); - final newLabels = labels.where((label) => !databaseLabels.contains(label)).toList(); - - await _database.writeTxn(() async { - await _labels.putAll(newLabels); - }); - } - /// Deletes the [label] from the database. Future<void> delete(Label label) async { await _database.writeTxn(() async { diff --git a/lib/services/notes/notes_service.dart b/lib/services/notes/notes_service.dart index 6c39f1c2..215652f6 100644 --- a/lib/services/notes/notes_service.dart +++ b/lib/services/notes/notes_service.dart @@ -261,6 +261,37 @@ class NotesService { }); } + /// Imports the [labels] and the [notes] in a single transaction, so that a failure leaves the database untouched. + /// + /// Only the labels whose name is not in the database yet are added. Each note is linked to the labels + /// whose names are in its [notesLabelsNames] entry. + Future<void> import(List<Label> labels, List<Note> notes, List<List<String>> notesLabelsNames) async { + assert( + notes.length == notesLabelsNames.length, + 'The list of labels names (${notesLabelsNames.length} items) should be the same length as the list of notes (${notes.length} notes)', + ); + + await _database.writeTxn(() async { + final databaseLabels = await _database.labels.where().findAll(); + final newLabels = labels.where((label) => !databaseLabels.contains(label)).toSet().toList(); + await _database.labels.putAll(newLabels); + final allLabels = [...databaseLabels, ...newLabels]; + + await _plainTextNotes.putAll(notes.whereType<PlainTextNote>().toList()); + await _markdownNotes.putAll(notes.whereType<MarkdownNote>().toList()); + await _richTextNotes.putAll(notes.whereType<RichTextNote>().toList()); + await _checklistNotes.putAll(notes.whereType<ChecklistNote>().toList()); + + for (var i = 0; i < notes.length; i++) { + final note = notes[i]; + final labelsNames = notesLabelsNames[i]; + + note.labels.addAll(allLabels.where((label) => labelsNames.contains(label.name))); + await note.labels.save(); + } + }); + } + /// Deletes the [note] from the database. Future<void> delete(Note note) async { await _database.writeTxn(() async { From 521048e6051b6827980474c5972ebcb3dd962442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:30:12 +0100 Subject: [PATCH 6/9] [586] Fix stale notes lists after renaming a label (#587) --- lib/models/label/label.dart | 7 +++++++ lib/pages/labels/dialogs/label_dialog.dart | 12 +++++++++--- .../labels/labels/labels_provider.dart | 17 ++++++++++++++--- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/lib/models/label/label.dart b/lib/models/label/label.dart index afac9b51..fcd9690b 100644 --- a/lib/models/label/label.dart +++ b/lib/models/label/label.dart @@ -81,6 +81,13 @@ class Label extends Equatable implements Comparable<Label> { /// Default constructor of a label. Label({required this.name, required this.colorHex}) : visible = true, pinned = false, locked = false; + /// Returns a copy of this label, with the same [id], and the [name] and the [colorHex] if they are given. + Label copyWith({String? name, int? colorHex}) => Label(name: name ?? this.name, colorHex: colorHex ?? this.colorHex) + ..id = id + ..visible = visible + ..pinned = pinned + ..locked = locked; + /// Label from [json] data. factory Label.fromJson(Map<String, dynamic> json) => _$LabelFromJson(json); diff --git a/lib/pages/labels/dialogs/label_dialog.dart b/lib/pages/labels/dialogs/label_dialog.dart index 0f11bc3c..819f577d 100644 --- a/lib/pages/labels/dialogs/label_dialog.dart +++ b/lib/pages/labels/dialogs/label_dialog.dart @@ -59,6 +59,13 @@ class _AddLabelDialogState extends ConsumerState<LabelDialog> { color = widget.label?.color ?? Theme.of(context).colorScheme.tertiaryContainer; } + @override + void dispose() { + nameController.dispose(); + + super.dispose(); + } + String? nameValidator(String? name) { if (name == null || name.isEmpty) { return context.l.dialog_label_name_cannot_be_empty; @@ -113,10 +120,9 @@ class _AddLabelDialogState extends ConsumerState<LabelDialog> { return; } + // Return a copy of the edited label, because the label is used as a key of the notes providers final label = widget.label != null - ? (widget.label! - ..name = nameController.text - ..colorHex = color.toARGB32()) + ? widget.label!.copyWith(name: nameController.text, colorHex: color.toARGB32()) : Label(name: nameController.text, colorHex: color.toARGB32()); Navigator.pop(context, label); diff --git a/lib/providers/labels/labels/labels_provider.dart b/lib/providers/labels/labels/labels_provider.dart index af6a44f4..c4e0f1e6 100644 --- a/lib/providers/labels/labels/labels_provider.dart +++ b/lib/providers/labels/labels/labels_provider.dart @@ -2,7 +2,6 @@ import 'package:collection/collection.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../common/constants/constants.dart'; -import '../../../common/extensions/list_extension.dart'; import '../../../models/label/label.dart'; import '../../../models/note/note_status.dart'; import '../../../pages/labels/enums/labels_filter.dart'; @@ -47,8 +46,10 @@ class Labels extends _$Labels { return labels.sorted(); } - /// Saves the [editedLabel] to the database. + /// Saves the [editedLabel] to the database, either a new label or a copy of an existing one with the same `id`. Future<bool> edit(Label editedLabel) async { + final oldLabel = state.value?.firstWhereOrNull((label) => label.id == editedLabel.id); + try { await _labelsService.put(editedLabel); } catch (exception, stackTrace) { @@ -57,10 +58,20 @@ class Labels extends _$Labels { return false; } - final labels = (state.value ?? [])..addOrUpdate(editedLabel); + // Replace the label by its ID, because the name, used for the equality, may have changed + final labels = [...?state.value] + ..removeWhere((label) => label.id == editedLabel.id) + ..add(editedLabel); state = AsyncData(labels.sorted()); + // The notes providers filtered by the label use its name as a key, so drop the ones of the old name + if (oldLabel != null && oldLabel.name != editedLabel.name) { + for (final status in NoteStatus.values) { + ref.invalidate(notesProvider(status: status, label: oldLabel)); + } + } + await _updateProviders(); return true; From 370e1da9d486a345f76f24e7224e5749e70c0d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:37:00 +0100 Subject: [PATCH 7/9] [588] Fix export frequency text, migrations and title sort (#589) --- lib/common/preferences/preference_key.dart | 3 -- lib/l10n/translations/app_en.arb | 2 +- lib/models/note/note.dart | 4 +- lib/services/migration/migration_service.dart | 46 +++++++++++++------ lib/services/notes/notes_service.dart | 19 -------- 5 files changed, 35 insertions(+), 39 deletions(-) diff --git a/lib/common/preferences/preference_key.dart b/lib/common/preferences/preference_key.dart index b6699507..8ed69bba 100644 --- a/lib/common/preferences/preference_key.dart +++ b/lib/common/preferences/preference_key.dart @@ -117,9 +117,6 @@ enum PreferenceKey<T extends Object> { /// Returns the value of this preference if set, or its default value otherwise. T get preferenceOrDefault => PreferencesWrapper().get<T>(this) ?? defaultValue; - /// Returns the value of this securely stored preference if set, or [null] otherwise. - T? get preferenceSecure => PreferencesWrapper().get<T>(this); - /// Returns the value of this securely stored preference if set, or its default value otherwise. Future<String> get preferenceOrDefaultSecure async => await PreferencesWrapper().getSecure(this) ?? defaultValue as String; diff --git a/lib/l10n/translations/app_en.arb b/lib/l10n/translations/app_en.arb index 5708e0c0..65e2885e 100644 --- a/lib/l10n/translations/app_en.arb +++ b/lib/l10n/translations/app_en.arb @@ -547,7 +547,7 @@ "@settings_auto_export_frequency": { "description": "Title of the settings tile to choose the frequency of the automatic exports." }, - "settings_auto_export_frequency_value": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{}}", + "settings_auto_export_frequency_value": "Every {frequency, select, 1{day} 3{3 days} 7{week} 14{2 weeks} 30{month} other{}}", "@settings_auto_export_frequency_value": { "description": "Frequency of the automatic exports.", "placeholders": { diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index 8da8d2f6..d6d7a9e9 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -210,7 +210,9 @@ sealed class Note implements Comparable<Note> { case SortMethod.editedDate: return sortAscending ? editedTime.compareTo(other.editedTime) : other.editedTime.compareTo(editedTime); case SortMethod.title: - return sortAscending ? title.compareTo(other.title) : other.title.compareTo(title); + final titleComparison = title.toLowerCase().compareTo(other.title.toLowerCase()); + + return sortAscending ? titleComparison : -titleComparison; default: throw Exception('The sort method is not valid: $sortMethod'); } diff --git a/lib/services/migration/migration_service.dart b/lib/services/migration/migration_service.dart index ac2e9859..b3730075 100644 --- a/lib/services/migration/migration_service.dart +++ b/lib/services/migration/migration_service.dart @@ -3,14 +3,13 @@ import 'package:isar_community/isar.dart'; import '../../common/constants/constants.dart'; import '../../common/preferences/preference_key.dart'; import '../../models/deprecated/note.dart'; +import '../../models/label/label.dart'; import '../../models/note/note.dart'; import '../database_service.dart'; -import '../notes/notes_service.dart'; /// Database migration service. class MigrationService { final _databaseService = DatabaseService(); - final _notesService = NotesService(); /// Migrates the database to the latest version if needed. Future<void> migrateIfNeeded() async { @@ -30,13 +29,12 @@ class MigrationService { /// Moves all legacy notes from the legacy `notes` collection to the new `richTextNotes` collection /// to allow adding different types of notes. Future<void> _migrateToV2() async { + final database = _databaseService.database; + // Get the notes and their labels - final notes = await _databaseService.database.notes.where().findAll(); + final notes = await database.notes.where().findAll(); final labels = notes.map((note) => note.labels.toList()).toList(); - // Remove the notes - await _databaseService.database.notes.clear(); - // Migrate the notes final migratedNotes = notes .map( @@ -53,9 +51,12 @@ class MigrationService { ) .toList(); - // Add the migrated notes to the new collection with their labels - await _notesService.putAll(migratedNotes); - await _notesService.putAllLabels(migratedNotes, labels); + // Replace the notes by the migrated ones with their labels, all at once so that a failure loses nothing + await database.writeTxn(() async { + await database.notes.clear(); + await database.richTextNotes.putAll(migratedNotes); + await _putLabels(migratedNotes, labels); + }); // Update the database version await PreferenceKey.databaseVersion.set(2); @@ -67,11 +68,12 @@ class MigrationService { /// /// Sets the new `id`, `archived` and `deletedTime` fields of each rich text note. Future<void> _migrateToV3() async { - // Get the rich text notes - final richTextNotes = await _databaseService.database.richTextNotes.where().findAll(); + final database = _databaseService.database; - // Remove the notes with the old IDs - await _notesService.deleteAll(richTextNotes); + // Get the rich text notes, their labels and their old IDs + final richTextNotes = await database.richTextNotes.where().findAll(); + final labels = richTextNotes.map((note) => note.labels.toList()).toList(); + final oldIds = richTextNotes.map((note) => note.isarId).toList(); // Migrate the notes final migratedNotes = richTextNotes @@ -83,12 +85,26 @@ class MigrationService { ) .toList(); - // Put back the migrated notes with the right IDs - await _notesService.putAll(migratedNotes); + // Replace the notes with the old IDs by the migrated ones with their labels, which are linked to the IDs, + // all at once so that a failure loses nothing + await database.writeTxn(() async { + await database.richTextNotes.deleteAll(oldIds); + await database.richTextNotes.putAll(migratedNotes); + await _putLabels(migratedNotes, labels); + }); // Update the database version await PreferenceKey.databaseVersion.set(3); logger.i('Migrated the database from version 2 to version 3'); } + + /// Links each of the [notes] to its [labels], inside a write transaction. + Future<void> _putLabels(List<RichTextNote> notes, List<List<Label>> labels) async { + for (var i = 0; i < notes.length; i++) { + await notes[i].labels.reset(); + notes[i].labels.addAll(labels[i]); + await notes[i].labels.save(); + } + } } diff --git a/lib/services/notes/notes_service.dart b/lib/services/notes/notes_service.dart index 215652f6..4d1cac93 100644 --- a/lib/services/notes/notes_service.dart +++ b/lib/services/notes/notes_service.dart @@ -242,25 +242,6 @@ class NotesService { }); } - /// Updates the [notes] with their corresponding [notesLabels] in the database. - Future<void> putAllLabels(List<Note> notes, List<List<Label>> notesLabels) async { - assert( - notes.length == notesLabels.length, - 'The list of labels (${notesLabels.length} items) should be the same length as the list of notes (${notes.length} notes)', - ); - - await _database.writeTxn(() async { - for (var i = 0; i < notes.length; i++) { - final note = notes[i]; - final labels = notesLabels[i]; - - await note.labels.reset(); - note.labels.addAll(labels); - await note.labels.save(); - } - }); - } - /// Imports the [labels] and the [notes] in a single transaction, so that a failure leaves the database untouched. /// /// Only the labels whose name is not in the database yet are added. Each note is linked to the labels From c91984ec9d88f6f4607ae9a8b5ddae66f8246bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:40:30 +0100 Subject: [PATCH 8/9] [566] Fix the backup location dialog interfering with shares (#590) Co-authored-by: Claude <noreply@anthropic.com> --- lib/app.dart | 17 ++++++++--- lib/common/actions/backup.dart | 6 ++-- lib/common/dialogs/require_backup_dialog.dart | 18 ++++++++++-- lib/common/system_utils.dart | 28 +++++++++++++------ lib/l10n/translations/app_en.arb | 4 +++ lib/services/backup/auto_backup_service.dart | 9 ++++++ 6 files changed, 65 insertions(+), 17 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index afaeb340..58ea9cbf 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -37,6 +37,9 @@ class _AppState extends ConsumerState<App> with AfterLayoutMixin<App> { /// Stream of data shared from other applications. late StreamSubscription _stream; + /// Whether the application was launched by another one sharing data, which opened a new note with it. + late final Future<bool> _launchedByShare; + @override void initState() { super.initState(); @@ -44,7 +47,7 @@ class _AppState extends ConsumerState<App> with AfterLayoutMixin<App> { globalRef = ref; // Read the potential data shared from other applications - SystemUtils().readSharedData(); + _launchedByShare = SystemUtils().readSharedData(); _stream = SystemUtils().listenSharedData(); // Eagerly get the labels for the full list and the navigation @@ -59,9 +62,15 @@ class _AppState extends ConsumerState<App> with AfterLayoutMixin<App> { // Using the context provided by afterFirstLayout doesn't work SystemUtils().setQuickActions(rootNavigatorKey.currentContext!); - // If the backup directory is still the default, ask to select it - if (await AutoExportUtils().isAutoExportDirectoryDefault) { - await requireBackupDirectory(rootNavigatorKey.currentContext!); + // Don't ask for the backup directory over the note opened with the shared data, it will be asked at the next launch + if (await _launchedByShare) { + return; + } + + // If the backup directory is still the default or is no longer accessible, ask to select it + final directoryLost = AutoExportUtils().isAutoExportDirectoryLost; + if (await AutoExportUtils().isAutoExportDirectoryDefault || directoryLost) { + await requireBackupDirectory(rootNavigatorKey.currentContext!, directoryLost: directoryLost); } } diff --git a/lib/common/actions/backup.dart b/lib/common/actions/backup.dart index 1fad28f8..3e6966da 100644 --- a/lib/common/actions/backup.dart +++ b/lib/common/actions/backup.dart @@ -7,7 +7,9 @@ import '../files/files_utils.dart'; import '../preferences/preference_key.dart'; /// Requires the user to select a backup directory. -Future<void> requireBackupDirectory(BuildContext context) async { +/// +/// If the [directoryLost] is enabled, also tells the user that the directory they selected is no longer accessible. +Future<void> requireBackupDirectory(BuildContext context, {bool directoryLost = false}) async { if (!kReleaseMode) { return; } @@ -15,7 +17,7 @@ Future<void> requireBackupDirectory(BuildContext context) async { final select = await showAdaptiveDialog<bool>( context: context, useRootNavigator: false, - builder: (context) => RequireBackupDialog(), + builder: (context) => RequireBackupDialog(directoryLost: directoryLost), ); if (select == null || !select) { diff --git a/lib/common/dialogs/require_backup_dialog.dart b/lib/common/dialogs/require_backup_dialog.dart index 75166bbc..ad735f14 100644 --- a/lib/common/dialogs/require_backup_dialog.dart +++ b/lib/common/dialogs/require_backup_dialog.dart @@ -5,13 +5,27 @@ import '../extensions/build_context_extension.dart'; /// Require backup dialog. class RequireBackupDialog extends StatelessWidget { /// Dialog to require the user to select a backup location. - const RequireBackupDialog({super.key}); + /// + /// If [directoryLost] is enabled, it starts by telling the user that the directory they selected is no longer + /// accessible. + const RequireBackupDialog({super.key, this.directoryLost = false}); + + /// Whether the backup directory selected by the user is no longer accessible. + final bool directoryLost; @override Widget build(BuildContext context) { return AlertDialog.adaptive( title: Text(context.l.dialog_require_backup_title), - content: SingleChildScrollView(child: Column(children: [Text(context.l.dialog_require_backup_description)])), + content: SingleChildScrollView( + child: Column( + spacing: 16.0, + children: [ + if (directoryLost) Text(context.l.dialog_require_backup_lost), + Text(context.l.dialog_require_backup_description), + ], + ), + ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), diff --git a/lib/common/system_utils.dart b/lib/common/system_utils.dart index d2868585..4596523b 100644 --- a/lib/common/system_utils.dart +++ b/lib/common/system_utils.dart @@ -188,24 +188,32 @@ class SystemUtils { }); } - /// Reads the data shared from other applications. - void readSharedData() { - ReceiveSharingIntent.instance.getInitialMedia().then((data) { - _processSharedData(data); + /// Reads the data shared from other applications when they launched this one, and returns whether a note was added + /// with it. + Future<bool> readSharedData() async { + try { + final data = await ReceiveSharingIntent.instance.getInitialMedia(); + final added = _processSharedData(data); - ReceiveSharingIntent.instance.reset(); - }); + await ReceiveSharingIntent.instance.reset(); + + return added; + } catch (exception, stackTrace) { + logger.e(exception.toString(), exception, stackTrace); + + return false; + } } - /// Processes the [data] shared from other applications. + /// Processes the [data] shared from other applications, and returns whether a note was added with it. /// /// If the [data] is text, it's added to a new note that is then opened. - void _processSharedData(List<SharedMediaFile> data) { + bool _processSharedData(List<SharedMediaFile> data) { if (rootNavigatorKey.currentContext == null || data.isEmpty || data.first.type != SharedMediaType.text || data.first.path.isEmpty) { - return; + return false; } final defaultShortcutNoteType = NoteType.defaultShare; @@ -213,6 +221,8 @@ class SystemUtils { final content = data.first.path; addNote(context, globalRef, noteType: defaultShortcutNoteType, content: content); + + return true; } /// Encodes the query [parameters] to be used in an URI. diff --git a/lib/l10n/translations/app_en.arb b/lib/l10n/translations/app_en.arb index 65e2885e..5cab67c6 100644 --- a/lib/l10n/translations/app_en.arb +++ b/lib/l10n/translations/app_en.arb @@ -843,6 +843,10 @@ "@dialog_require_backup_description": { "description": "Description of the dialog to select a backup location." }, + "dialog_require_backup_lost": "The backup location you selected is no longer accessible. Until you select a new one, the automatic exports are saved in the application folder, which is deleted when the application is uninstalled.", + "@dialog_require_backup_lost": { + "description": "Text shown first in the dialog to select a backup location when the location selected by the user is no longer accessible." + }, "dialog_require_backup_ignore": "Ignore", "@dialog_require_backup_ignore": { "description": "Button to ignore the dialog to select a backup location." diff --git a/lib/services/backup/auto_backup_service.dart b/lib/services/backup/auto_backup_service.dart index 265d1547..636a2bb6 100644 --- a/lib/services/backup/auto_backup_service.dart +++ b/lib/services/backup/auto_backup_service.dart @@ -26,6 +26,10 @@ class AutoExportUtils { /// Directory where automatic exports are saved. late String autoExportDirectory; + /// Whether the directory selected by the user for the automatic exports is no longer accessible, in which case + /// they are saved in the default directory. + bool isAutoExportDirectoryLost = false; + /// Ensures the utility is initialized. Future<void> ensureInitialized() async { await setAutoExportDirectory(); @@ -35,6 +39,8 @@ class AutoExportUtils { /// Sets the directory where automatic exports are located depending on the user preference and the available /// permissions. Future<void> setAutoExportDirectory() async { + isAutoExportDirectoryLost = false; + final autoExportDirectoryPreference = PreferenceKey.autoExportDirectory.preferenceOrDefault; // Set to default if the user didn't choose a directory @@ -47,6 +53,9 @@ class AutoExportUtils { // Set to default if the directory doesn't exist try { if (autoExportDirectorySaf == null || !(await doesDirectoryExist(autoExportDirectorySaf.uri))) { + logger.w('The automatic export directory selected by the user is no longer accessible, using the default one'); + isAutoExportDirectoryLost = true; + return setAutoExportDirectoryToDefault(); } } catch (exception, stackTrace) { From 8de339672b0f9aaaa2539eb327a898ab4c7e36d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:19:57 +0100 Subject: [PATCH 9/9] [591] Update Flutter, Dart, Android and the dependencies (#592) --- .fvmrc | 2 +- analysis_options.yaml | 7 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- android/settings.gradle | 4 +- lib/common/constants/notes.dart | 12 +- lib/common/extensions/color_extension.dart | 1 + .../extensions/text_style_extension.dart | 1 + .../editor/editor_deleted_menu_option.dart | 5 +- .../labels/selection_labels_menu_option.dart | 5 +- .../notes/selection_deleted_menu_option.dart | 5 +- .../swipe_actions/archived_swipe_action.dart | 5 +- .../swipe_actions/available_swipe_action.dart | 5 +- .../swipe_actions/label_swipe_action.dart | 5 +- .../preferences/preferences_wrapper.dart | 13 +- .../placeholders/empty_placeholder.dart | 5 +- lib/pages/editor/editor_page.dart | 6 +- lib/pages/notes/widgets/add_note_fab.dart | 38 +- .../preferences/preferences_provider.dart | 1 + lib/services/backup/auto_backup_service.dart | 4 +- pubspec.lock | 444 +++++++++--------- pubspec.yaml | 76 +-- 21 files changed, 343 insertions(+), 305 deletions(-) diff --git a/.fvmrc b/.fvmrc index 457360f8..4304de27 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.44.0" + "flutter": "3.47.9" } \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index bd10cb87..b8e11e1b 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,13 +1,16 @@ include: package:flutter_lints/flutter.yaml +plugins: + riverpod_lint: ^3.1.9 + formatter: page_width: 120 analyzer: exclude: - '**.g.dart' - plugins: - - custom_lint + - build/** + - android/** language: strict-inference: true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 52dc9895..7abf6a8e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip -distributionSha256Sum=fba8464465835e74f7270bbf43d6d8a8d7709ab0a43ce1aa3323f73e9aa0c612 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip +distributionSha256Sum=17f277867f6914d61b1aa02efab1ba7bb439ad652ca485cd8ca6842fccec6e43 diff --git a/android/settings.gradle b/android/settings.gradle index 5f1a19e6..71c67931 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.13.2' apply false - id "org.jetbrains.kotlin.android" version "2.1.0" apply false + id "com.android.application" version '9.1.0' apply false + id "org.jetbrains.kotlin.android" version "2.4.0" apply false } include ":app" diff --git a/lib/common/constants/notes.dart b/lib/common/constants/notes.dart index 3d013bea..30e06415 100644 --- a/lib/common/constants/notes.dart +++ b/lib/common/constants/notes.dart @@ -21,8 +21,7 @@ final screenshotNotes = [ createdTime: DateTime(2000, 01, 01, 12), editedTime: DateTime(2000, 01, 01, 12), title: "Welcome to Material Notes", - content: - '[{"insert":"Simple","attributes":{"b":true}},{"insert":", "},{"insert":"local","attributes":{"i":true}},{"insert":", "},{"insert":"material design","attributes":{"u":true}},{"insert":" notes\\n"}]', + content: '[{"insert":"Simple","attributes":{"b":true}},{"insert":", "},{"insert":"local","attributes":{"i":true}},{"insert":", "},{"insert":"material design","attributes":{"u":true}},{"insert":" notes\\n"}]', ), RichTextNote( archived: false, @@ -31,8 +30,7 @@ final screenshotNotes = [ createdTime: DateTime(2000, 01, 01, 11, 55), editedTime: DateTime(2000, 01, 01, 11, 55), title: "Take notes", - content: - '[{"insert":"Write text notes"},{"insert":"\\n","attributes":{"block":"cl","checked":true}},{"insert":"Use plain text, markdown, rich text or checklist notes"},{"insert":"\\n","attributes":{"block":"cl","checked":true}},{"insert":"Use quick action to add from your home screen"},{"insert":"\\n","attributes":{"block":"cl","checked":true}}]', + content: '[{"insert":"Write text notes"},{"insert":"\\n","attributes":{"block":"cl","checked":true}},{"insert":"Use plain text, markdown, rich text or checklist notes"},{"insert":"\\n","attributes":{"block":"cl","checked":true}},{"insert":"Use quick action to add from your home screen"},{"insert":"\\n","attributes":{"block":"cl","checked":true}}]', ), RichTextNote( archived: false, @@ -59,8 +57,7 @@ final screenshotNotes = [ createdTime: DateTime(2000, 01, 01, 11, 45), editedTime: DateTime(2000, 01, 01, 11, 45), title: "Share & backup", - content: - '[{"insert":"Create a note from shared text\\nShare notes as text and export as Markdown\\nBackup notes as JSON\\n"}]', + content: '[{"insert":"Create a note from shared text\\nShare notes as text and export as Markdown\\nBackup notes as JSON\\n"}]', ), RichTextNote( archived: false, @@ -79,8 +76,7 @@ final screenshotNotes = [ createdTime: DateTime(2000, 01, 01, 11, 40), editedTime: DateTime(2000, 01, 01, 11, 40), title: "Customize", - content: - "[{\"insert\":\"Choose your language\\nChoose your theme (including black and dynamic)\\nHide features you don't need\\n\"}]", + content: "[{\"insert\":\"Choose your language\\nChoose your theme (including black and dynamic)\\nHide features you don't need\\n\"}]", ), RichTextNote( archived: false, diff --git a/lib/common/extensions/color_extension.dart b/lib/common/extensions/color_extension.dart index 7fb62775..b78933c8 100644 --- a/lib/common/extensions/color_extension.dart +++ b/lib/common/extensions/color_extension.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../constants/constants.dart'; /// Extensions on the [Color] class. diff --git a/lib/common/extensions/text_style_extension.dart b/lib/common/extensions/text_style_extension.dart index f640b1b5..fe9b539d 100644 --- a/lib/common/extensions/text_style_extension.dart +++ b/lib/common/extensions/text_style_extension.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'color_extension.dart'; /// Extensions on the [TextStyle] class. diff --git a/lib/common/navigation/enums/editor/editor_deleted_menu_option.dart b/lib/common/navigation/enums/editor/editor_deleted_menu_option.dart index 72b51764..29bcfb2f 100644 --- a/lib/common/navigation/enums/editor/editor_deleted_menu_option.dart +++ b/lib/common/navigation/enums/editor/editor_deleted_menu_option.dart @@ -48,9 +48,8 @@ enum EditorDeletedMenuOption { leading: Icon(icon, color: dangerous ? Theme.of(context).colorScheme.error : null), title: Text( title(context), - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(color: dangerous ? Theme.of(context).colorScheme.error : null), + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(color: dangerous ? Theme.of(context).colorScheme.error : null), ), ), ); diff --git a/lib/common/navigation/enums/labels/selection_labels_menu_option.dart b/lib/common/navigation/enums/labels/selection_labels_menu_option.dart index ded584a8..b49b08e3 100644 --- a/lib/common/navigation/enums/labels/selection_labels_menu_option.dart +++ b/lib/common/navigation/enums/labels/selection_labels_menu_option.dart @@ -55,9 +55,8 @@ enum SelectionLabelsMenuOption { leading: Icon(icon, color: dangerous ? Theme.of(context).colorScheme.error : null), title: Text( title(context), - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(color: dangerous ? Theme.of(context).colorScheme.error : null), + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(color: dangerous ? Theme.of(context).colorScheme.error : null), ), ), ); diff --git a/lib/common/navigation/enums/notes/selection_deleted_menu_option.dart b/lib/common/navigation/enums/notes/selection_deleted_menu_option.dart index 0018da60..df4dad56 100644 --- a/lib/common/navigation/enums/notes/selection_deleted_menu_option.dart +++ b/lib/common/navigation/enums/notes/selection_deleted_menu_option.dart @@ -45,9 +45,8 @@ enum SelectionDeletedMenuOption { leading: Icon(icon, color: dangerous ? Theme.of(context).colorScheme.error : null), title: Text( title(context), - style: Theme.of( - context, - ).textTheme.titleSmall?.copyWith(color: dangerous ? Theme.of(context).colorScheme.error : null), + style: Theme.of(context).textTheme.titleSmall + ?.copyWith(color: dangerous ? Theme.of(context).colorScheme.error : null), ), ), ); diff --git a/lib/common/preferences/enums/swipe_actions/archived_swipe_action.dart b/lib/common/preferences/enums/swipe_actions/archived_swipe_action.dart index 5ec8d74d..68b4ae51 100644 --- a/lib/common/preferences/enums/swipe_actions/archived_swipe_action.dart +++ b/lib/common/preferences/enums/swipe_actions/archived_swipe_action.dart @@ -94,9 +94,8 @@ enum ArchivedSwipeAction { Widget titleWidget(BuildContext context) { return Text( title(context), - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onTertiaryContainer), + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.onTertiaryContainer), ); } diff --git a/lib/common/preferences/enums/swipe_actions/available_swipe_action.dart b/lib/common/preferences/enums/swipe_actions/available_swipe_action.dart index 7fa7866f..835dd04e 100644 --- a/lib/common/preferences/enums/swipe_actions/available_swipe_action.dart +++ b/lib/common/preferences/enums/swipe_actions/available_swipe_action.dart @@ -165,9 +165,8 @@ enum AvailableSwipeAction { Widget titleWidget(BuildContext context) { return Text( title(context), - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onTertiaryContainer), + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.onTertiaryContainer), ); } diff --git a/lib/common/preferences/enums/swipe_actions/label_swipe_action.dart b/lib/common/preferences/enums/swipe_actions/label_swipe_action.dart index 7b13972e..a5321e1c 100644 --- a/lib/common/preferences/enums/swipe_actions/label_swipe_action.dart +++ b/lib/common/preferences/enums/swipe_actions/label_swipe_action.dart @@ -181,9 +181,8 @@ enum LabelSwipeAction { Widget titleWidget(BuildContext context) { return Text( title(context), - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(color: Theme.of(context).colorScheme.onTertiaryContainer), + style: Theme.of(context).textTheme.titleMedium + ?.copyWith(color: Theme.of(context).colorScheme.onTertiaryContainer), ); } diff --git a/lib/common/preferences/preferences_wrapper.dart b/lib/common/preferences/preferences_wrapper.dart index 00388f41..e72aaa68 100644 --- a/lib/common/preferences/preferences_wrapper.dart +++ b/lib/common/preferences/preferences_wrapper.dart @@ -65,13 +65,12 @@ class PreferencesWrapper { } return switch (T) { - == bool => _preferences.getBool(preferenceKey.name), - == int => _preferences.getInt(preferenceKey.name), - == double => _preferences.getDouble(preferenceKey.name), - == String => _preferences.getString(preferenceKey.name), - _ => throw ArgumentError('Invalid preference type: $T'), - } - as T?; + == bool => _preferences.getBool(preferenceKey.name), + == int => _preferences.getInt(preferenceKey.name), + == double => _preferences.getDouble(preferenceKey.name), + == String => _preferences.getString(preferenceKey.name), + _ => throw ArgumentError('Invalid preference type: $T'), + } as T?; } // On type conversion error, reset the preference to its default value on TypeError catch (error) { diff --git a/lib/common/widgets/placeholders/empty_placeholder.dart b/lib/common/widgets/placeholders/empty_placeholder.dart index d7e08bfc..2d27b2fa 100644 --- a/lib/common/widgets/placeholders/empty_placeholder.dart +++ b/lib/common/widgets/placeholders/empty_placeholder.dart @@ -54,9 +54,8 @@ class EmptyPlaceholder extends StatelessWidget { Padding(padding: Paddings.vertical(16)), Text( text!, - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), + style: Theme.of(context).textTheme.headlineSmall + ?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant), ), ], ), diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart index d6185183..7d97856e 100644 --- a/lib/pages/editor/editor_page.dart +++ b/lib/pages/editor/editor_page.dart @@ -64,12 +64,12 @@ class _EditorState extends ConsumerState<EditorPage> { savePendingNote(globalRef); - // ignore: avoid_ref_inside_state_dispose + // ignore: riverpod_lint/avoid_ref_inside_state_dispose globalRef.read(notesProvider(status: NoteStatus.available).notifier).removeEmpty(); - // ignore: avoid_ref_inside_state_dispose + // ignore: riverpod_lint/avoid_ref_inside_state_dispose globalRef.read(notesProvider(status: NoteStatus.archived).notifier).removeEmpty(); if (currentLabelFilter != null) { - // ignore: avoid_ref_inside_state_dispose + // ignore: riverpod_lint/avoid_ref_inside_state_dispose globalRef.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).removeEmpty(); } diff --git a/lib/pages/notes/widgets/add_note_fab.dart b/lib/pages/notes/widgets/add_note_fab.dart index 8a53ede8..643e0caa 100644 --- a/lib/pages/notes/widgets/add_note_fab.dart +++ b/lib/pages/notes/widgets/add_note_fab.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -31,9 +33,31 @@ class _AddNoteFabState extends ConsumerState<AddNoteFab> { canPopNotifier.update(); } + /// Builds the button that opens or closes the FAB, rotated according to the [progress] of the animation. + /// + /// It replaces the `RotateFloatingActionButtonBuilder` of the package, whose buttons are built with + /// `autofocus: true`: when the FAB finishes closing, its button is built again and takes the focus, + /// which removes it from the editor that was opened in the meantime and closes the keyboard. + FloatingActionButtonBuilder buildToggleButton({required String heroTag, required IconData icon}) { + final themeSize = Theme.of(context).floatingActionButtonTheme.sizeConstraints?.maxWidth; + + return FloatingActionButtonBuilder( + size: themeSize ?? DefaultFloatingActionButtonBuilder().size, + builder: (context, onPressed, progress) { + return AnimatedBuilder( + animation: progress, + builder: (context, _) { + return Transform.rotate( + angle: progress.value * math.pi / 2, + child: FloatingActionButton(heroTag: heroTag, onPressed: onPressed, child: Icon(icon)), + ); + }, + ); + }, + ); + } + void onPressed(NoteType noteType) { - // Close the FAB before opening the editor, otherwise closing it during the transition removes the focus - // from the editor and closes the keyboard closeAddNoteFabIfOpen(); addNote(context, ref, noteType: noteType); @@ -54,14 +78,8 @@ class _AddNoteFabState extends ConsumerState<AddNoteFab> { type: ExpandableFabType.up, childrenAnimation: ExpandableFabAnimation.none, distance: 64, - openButtonBuilder: RotateFloatingActionButtonBuilder( - heroTag: '<open add note FAB hero tag>', - child: const Icon(Icons.add), - ), - closeButtonBuilder: RotateFloatingActionButtonBuilder( - heroTag: '<close add note FAB hero tag>', - child: const Icon(Icons.close), - ), + openButtonBuilder: buildToggleButton(heroTag: '<open add note FAB hero tag>', icon: Icons.add), + closeButtonBuilder: buildToggleButton(heroTag: '<close add note FAB hero tag>', icon: Icons.close), afterOpen: onOpen, afterClose: onClose, children: [ diff --git a/lib/providers/preferences/preferences_provider.dart b/lib/providers/preferences/preferences_provider.dart index 86a389d1..1530ce34 100644 --- a/lib/providers/preferences/preferences_provider.dart +++ b/lib/providers/preferences/preferences_provider.dart @@ -1,4 +1,5 @@ import '../../common/preferences/watched_preferences.dart'; + import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'preferences_provider.g.dart'; diff --git a/lib/services/backup/auto_backup_service.dart b/lib/services/backup/auto_backup_service.dart index 636a2bb6..b15095f5 100644 --- a/lib/services/backup/auto_backup_service.dart +++ b/lib/services/backup/auto_backup_service.dart @@ -56,7 +56,9 @@ class AutoExportUtils { logger.w('The automatic export directory selected by the user is no longer accessible, using the default one'); isAutoExportDirectoryLost = true; - return setAutoExportDirectoryToDefault(); + await setAutoExportDirectoryToDefault(); + + return; } } catch (exception, stackTrace) { logger.e(exception.toString(), exception, stackTrace); diff --git a/pubspec.lock b/pubspec.lock index 2bf295f7..ec954f5a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + sha256: "1b0e6a07425a3e460666e88bf1c949ccc7bb0116ad562ce94a1eca60fe820725" url: "https://pub.dev" source: hosted - version: "92.0.0" + version: "103.0.0" after_layout: dependency: "direct main" description: @@ -21,34 +21,34 @@ packages: dependency: transitive description: name: analysis_server_plugin - sha256: "44adba4d74a2541173bad4c11531d2a4d22810c29c5ddb458a38e9f4d0e5eac7" + sha256: c58b2a05b260a023eefd0848496ff6b113a318c6c95dad4215165a4bd54ae600 url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.18" analyzer: dependency: transitive description: name: analyzer - sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + sha256: "61c04d0c1bfed555c681ea079519933f071a5a026578ff73c4ff0df2d3462e5e" url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "13.3.0" analyzer_buffer: dependency: transitive description: name: analyzer_buffer - sha256: ff4bd291778c7417fe53fe24ee0d0a1f1ffe281a2d4ea887e7094f16e36eace7 + sha256: f890a2e708e38fbf4238ad2f9fd3d506538acdddb0cd8eb2c3e8c7bd17784b57 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.4" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: "6645a029da947ffd823d98118f385d4bd26b54eb069c006b22e0b94e451814b5" + sha256: c2a75baf1657171cc7872db96a12b07e385873cf7a73f2b4f7638e9d0701ee53 url: "https://pub.dev" source: hosted - version: "0.13.11" + version: "0.14.12" animated_reorderable_list: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: "direct main" description: name: archive - sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + sha256: de5ac590b8155e4e676088d714bd41ea620449a6adc0f7950c5073874b287ddb url: "https://pub.dev" source: hosted - version: "4.0.9" + version: "4.6.0" args: dependency: transitive description: @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: asn1lib - sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + sha256: cfb3cf6a53158918d52dda61bc26f0cc8612a0d3b9dc29e87eaa3423902ca636 url: "https://pub.dev" source: hosted - version: "1.6.5" + version: "1.6.8" async: dependency: transitive description: @@ -109,34 +109,34 @@ packages: dependency: transitive description: name: build - sha256: "45d14a0fb23e018d8287c32fc98d726ce466b231928ed9b9200f29bd3ccd39ae" + sha256: "8a5c5761af8e31748bba3c82f68925ace40f3225c3eea25be9beb57eca7cd7a8" url: "https://pub.dev" source: hosted - version: "4.0.7" + version: "4.0.11" build_config: dependency: transitive description: name: build_config - sha256: f2c223156a26eea323e6244b85141d76413a80aeee9fe0b380773789fabaf8ae + sha256: d466ed2dc9c6cd1d169948879b84ee061eb5e22c64a7c6089879c6296d272a8d url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" build_daemon: dependency: transitive description: name: build_daemon - sha256: fd754058c342243718d5171a95f352cfc9fcf0cba8cfa26df67cb13a5836db78 + sha256: "39ab65f7c60c19af64b9ef96e6a5d3ab48355f7dddfd98a9079d55a8cb6628fc" url: "https://pub.dev" source: hosted - version: "4.1.2" + version: "4.1.7" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "5367e521935b102bdf1e735d2aab461e36b2edca6517662d088dd04cc39f8d16" + sha256: "894c243f6bc32015fec466ce30a6925bd537a77a426ee5bf481120477eb3de67" url: "https://pub.dev" source: hosted - version: "2.15.1" + version: "2.16.1" build_verify: dependency: "direct dev" description: @@ -149,18 +149,18 @@ packages: dependency: transitive description: name: built_collection - sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + sha256: "44922ecebd8e3d0cef743dadd3b54df4e87e2a4c52a29465a1872aa9e192caa8" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.1.2" built_value: dependency: transitive description: name: built_value - sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + sha256: f87ea98192116f7093cb214551ce1929caae0681fdba282b3d8b4462adee7bb7 url: "https://pub.dev" source: hosted - version: "8.12.6" + version: "8.13.0" characters: dependency: transitive description: @@ -197,26 +197,26 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" code_assets: dependency: transitive description: name: code_assets - sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + sha256: "1ef22a0be3af9370bd9481ac76db114db92bfe75d5ca2b2d7bc911764478a4d9" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.2.0" code_builder: dependency: transitive description: name: code_builder - sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + sha256: "811709c1badd02fc054039a13f66e0db1961865f9a314175f68c3eaeebec786f" url: "https://pub.dev" source: hosted - version: "4.11.1" + version: "4.13.0" collection: dependency: "direct main" description: @@ -245,10 +245,10 @@ packages: dependency: transitive description: name: convert_object - sha256: "36d24e8bb88de1e7c1c2cbd757b7bfae21a0e31047a95a9b69672de394108b50" + sha256: a35faeb03782a26e23521a73e0d7e71e3dd4294edf9f31f2e62cc5dc28f12d93 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" coverage: dependency: transitive description: @@ -261,18 +261,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6 url: "https://pub.dev" source: hosted - version: "0.3.5+4" + version: "0.3.5+5" crypto: dependency: transitive description: name: crypto - sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + sha256: e3626ae6270383c9e3a549f34c9e4cd26c09ae6a9adaab2bf4737a00340efdc4 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.8" csslib: dependency: transitive description: @@ -285,18 +285,18 @@ packages: dependency: "direct main" description: name: dart_helper_utils - sha256: a2a51cc358e803e003c81850ccf76672d2a33cd5301d78ba62cc4dee13f4887d + sha256: "72aca9798ad726c354cf4147268e0b48ff07a21d8962569bade90ad585abae13" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "7.0.0" dart_style: dependency: transitive description: name: dart_style - sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + sha256: a0e4f65339b31eb6a9e6a12e14b4682a48d46fbc54a82f4a702396fbdbac63a7 url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.14" dartx: dependency: transitive description: @@ -309,10 +309,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442" + sha256: ade1c36f8355d6284c98e4a47992ea263887ed3d1acb281e37635dd45f04c4aa url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.3.0" device_info_plus_platform_interface: dependency: transitive description: @@ -333,10 +333,10 @@ packages: dependency: "direct main" description: name: dynamic_color - sha256: "43a5a6679649a7731ab860334a5812f2067c2d9ce6452cf069c5e0c25336c17c" + sha256: "4aeb76de323e8eb660bab5557d826ad7ebbf1434a598f0c6aa8a11a9696a6757" url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.9.0" encrypt: dependency: "direct main" description: @@ -349,10 +349,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" + sha256: ea36e500cbf0ec18361deabca755434f73e42fe53c5130021c7e2d6b5ebe1702 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.0" fake_async: dependency: transitive description: @@ -487,10 +487,10 @@ packages: dependency: "direct main" description: name: flutter_markdown_plus - sha256: "2e4b667a75c08e5710866ae010da392acf5af475f9ad0be9bf406525e9751b0c" + sha256: fce641d6c2106cc495de1cd603f97a4f9d615a97a64011928ded380dbdade935 url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.0.12" flutter_native_splash: dependency: "direct main" description: @@ -511,42 +511,42 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2" + sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.3" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" + sha256: d4e1fb6b2cb524868929e78dc0282fa000554b22060fb53789dc481c9fc95bb8 url: "https://pub.dev" source: hosted - version: "10.3.1" + version: "11.2.0" flutter_secure_storage_darwin: dependency: transitive description: name: flutter_secure_storage_darwin - sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" + sha256: a031ceac9b070e62ef183cd7f7d1a8798ebde402d5049f0fe6eb3a51bd797a21 url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.3" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + sha256: caa75bd78f017422912e3a904933113b2428eb79f981ac37acd2dd2a700458ea url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + sha256: "64951127f001f546891c86f414972bf49ef2c4c8a4c92c65f5790cc0c9100045" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.1" flutter_secure_storage_web: dependency: transitive description: @@ -593,34 +593,34 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + sha256: "2b56423afc7651a81da53ed3a4f9f51e92572293f54d0d54e19b759f13714849" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.2.0" gap: dependency: "direct main" description: name: gap - sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + sha256: ba305c9fb29d48b4ddfc59cbe4a437fc67b2e18824ec7729196289504d50570b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" glob: dependency: transitive description: name: glob - sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.2.0" go_router: dependency: "direct main" description: name: go_router - sha256: "5922b2861e2235a3504896f0d6fa07d84141b480cf52eecd2f42cd25585a9e8a" + sha256: d7a3576cb312649eaa51f2356450aed686085fb58fcdebda5b359aa951eef7ea url: "https://pub.dev" source: hosted - version: "17.3.0" + version: "17.5.0" graphs: dependency: transitive description: @@ -633,18 +633,18 @@ packages: dependency: transitive description: name: hooks - sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.2.0" html: dependency: transitive description: name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + sha256: "43b67b8f43321ab066817dfac5619596c98bb1b61624e77203bb4351785f9699" url: "https://pub.dev" source: hosted - version: "0.15.6" + version: "0.15.7" http: dependency: transitive description: @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: http_multi_server - sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + sha256: "361668bdd71f0ae2dc6d17b75a593520f3c414af6534f9eed1aa72049dd71d18" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" http_parser: dependency: transitive description: @@ -673,26 +673,26 @@ packages: dependency: transitive description: name: image - sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" + sha256: "89ed75b2f8231fe676808c5564f9590d728024ded7ac782a7ed8474f1c95b72a" url: "https://pub.dev" source: hosted - version: "4.9.1" + version: "4.12.0" intl: dependency: "direct main" description: name: intl - sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" url: "https://pub.dev" source: hosted - version: "0.20.2" + version: "0.20.3" io: dependency: transitive description: name: io - sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.1.0" is_first_run: dependency: "direct main" description: @@ -705,42 +705,50 @@ packages: dependency: "direct main" description: name: isar_community - sha256: "683fd093956eac3a660776afb70d65d50a3391b3587b40b89e3d12586b8cc47f" + sha256: "5046967047825b8699b66ad41f29c68c4eaefd54e236f6dea9e90e0b8063442a" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.1" isar_community_flutter_libs: dependency: "direct main" description: name: isar_community_flutter_libs - sha256: c44340fa38c81ef16d924202d443bbe799cde4826be9a31a9dc92ee612e1966f + sha256: "541b07c0de7b5bd28e76cafc9fb56fac521186937bf309231572bf16551ef2f1" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.1" isar_community_generator: dependency: "direct dev" description: name: isar_community_generator - sha256: fd5a2a5df2aac2fccba82425005bf47fd2a48f34190536d0c9fd732dc7303fb3 + sha256: "7db0aa41a2b03dc2d7c38b23ff2dfb1e06c7e43c2ac8831524595446d3f31808" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.1" jni: dependency: transitive description: name: jni - sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + sha256: "2e91e6087291affd10b31b6c872f3e3ac7dc5d93b51b5ba485d930c8f08bde6c" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.2" jni_flutter: dependency: transitive description: name: jni_flutter - sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + sha256: "0dd33d67797560f66e961a6053882d4df54bd345bf063b8fe7ea133f7ad8b191" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4+1" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" js: dependency: transitive description: @@ -753,18 +761,18 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.12.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0" + sha256: cc7822761b40be9237bcaae62b755ffe5453ceff96a0001f1f5f2439251c66f1 url: "https://pub.dev" source: hosted - version: "6.13.0" + version: "6.15.2" leak_tracker: dependency: transitive description: @@ -797,6 +805,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + listen: + dependency: transitive + description: + name: listen + sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.1" listen_sharing_intent: dependency: "direct main" description: @@ -817,18 +833,18 @@ packages: dependency: transitive description: name: local_auth_android - sha256: fdb936d59ab945c7af297defd67bd1ed87b11b6db1bc16d01e94677a8f1c38ec + sha256: e2ceb84cdecefe1bc0ddd4ac308b0f6525bbfa0a60d0e552082b2970e1fb38c2 url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.3.0" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: a8c3d4e17454111f7fd31ff72a31222359f6059f7fe956c2dcfe0f88f49826d4 + sha256: "45a9db01b57082bc9d6115cb25eb8fc0b72f43e5f330819bdd8352ecdae87589" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" local_auth_platform_interface: dependency: transitive description: @@ -841,10 +857,10 @@ packages: dependency: transitive description: name: local_auth_windows - sha256: be12c5b8ba5e64896983123655c5f67d2484ecfcc95e367952ad6e3bff94cb16 + sha256: d72b8a441e5e6a679902f87c51529fcce65f442d77598484cf35454c3d733eda url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" locale_names: dependency: "direct main" description: @@ -857,10 +873,10 @@ packages: dependency: "direct main" description: name: logger - sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + sha256: "2a0dc097e7b01d942475bdd552356db2d0f768b05540bd4b2b53f1840f2239a7" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.8.0" logging: dependency: transitive description: @@ -881,10 +897,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.20" material_color_utilities: dependency: transitive description: @@ -897,34 +913,34 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: "49c532dd0b74544e9d8d93ec0f821d52ec532e7c9263c889ebe71b1be4f34ba7" + sha256: bd513edc2bc9b034108d5518c48cf5cebbb67d0653728cc452368cb27ca80bb0 url: "https://pub.dev" source: hosted - version: "4.2951.0" + version: "4.2960.0" meta: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" mime: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" mockito: dependency: transitive description: name: mockito - sha256: eff30d002f0c8bf073b6f929df4483b543133fcafce056870163587b03f1d422 + sha256: "6c1970612452260366064d4df80fdb3eece1ec5e52610ae34a0e51f2f3d8e64f" url: "https://pub.dev" source: hosted - version: "5.6.4" + version: "5.8.1" node_preamble: dependency: transitive description: @@ -937,10 +953,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" + sha256: c965d264fd4b0b4dd402dce9d049b70656bf98d3ca108b9bc55d2544ceadbe5e url: "https://pub.dev" source: hosted - version: "9.4.1" + version: "9.6.4" package_config: dependency: transitive description: @@ -953,10 +969,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad + sha256: "73ef2cd5c11a7548f9b7f6fc2a4f6113de2e64aa2a70de63feede95548822908" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.2.2" package_info_plus_platform_interface: dependency: transitive description: @@ -1041,18 +1057,18 @@ packages: dependency: transitive description: name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + sha256: "2f1564aadb14215e478563b5406cb80729d022158c01dc50c96013c2f7eb4790" url: "https://pub.dev" source: hosted - version: "7.0.2" + version: "7.1.0" platform: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.2.0" plugin_platform_interface: dependency: transitive description: @@ -1073,58 +1089,58 @@ packages: dependency: transitive description: name: pool - sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.5.3" posix: dependency: transitive description: name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + sha256: "613646d076944f3acc125830af935b48d4b8a57a388691547bbfd25b434f4d63" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.5.4" pub_semver: dependency: transitive description: name: pub_semver - sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + sha256: "9ac81c01ffd6a9cf9b2713288834fbaaf6aebde05ffdf02d380e68b8e06f27d8" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.7.0" quick_actions: dependency: "direct main" description: name: quick_actions - sha256: "7e35dd6a21f5bbd21acf6899039eaf85001a5ac26d52cbd6a8a2814505b90798" + sha256: "1d3397c1ce5cd91b732c675bc82571d316d6de8241d774927b4ea3680c860e1a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" quick_actions_android: dependency: transitive description: name: quick_actions_android - sha256: af50d52d882a0f520c4be082636b5177b3de239bd468857d5da0b5a6eec61e07 + sha256: "7fb4852f791fd3da688103be2fa287ecb97608b9b73ee9def455b78fa5f18587" url: "https://pub.dev" source: hosted - version: "1.0.32" + version: "1.0.34" quick_actions_ios: dependency: transitive description: name: quick_actions_ios - sha256: be1496e7ca1debc86d9ea08e56325649fbc5abb2b6930690c97ba0dae59992b1 + sha256: fe090ae5b9673c7031ca307371f611061a731adda6a5c9574aed9439475218cf url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "1.2.5" quick_actions_platform_interface: dependency: transitive description: @@ -1145,10 +1161,10 @@ packages: dependency: transitive description: name: record_use - sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "1.1.1" remove_markdown: dependency: "direct main" description: @@ -1161,66 +1177,66 @@ packages: dependency: "direct main" description: name: restart_app - sha256: "400dd401b77b599e2defef55fd4c22af3ff80af48616a82f01c2a07070e3e368" + sha256: "398550c5c0438b15c6062c7cb7e5c234498584aa9ba1b28910e77d4ff43bfef6" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.10.2" riverpod: dependency: transitive description: name: riverpod - sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83" + sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.4.3" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: e55bc08c084a424e1bbdc303fe8ea75daafe4269b68fd0e0f6f1678413715b66 + sha256: bb13cb3985815840cc103eda1027158358c5fdc00f308fc6f76574d0594107af url: "https://pub.dev" source: hosted - version: "1.0.0-dev.9" + version: "1.0.0-dev.12" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "16471a1260b94e939394d78f1c63a9350936ac4a68c9fbdab40be47268c0b04f" + sha256: "322e56ad198928ef26ba240f85421bfff2493bd65665c887b1299e6386b6206d" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.7" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "6f9220534d7a353b53c875ea191a84d28cb4e52ac420a66a1bd7318329d977b0" + sha256: c94dcb8b60ff4805d2bb8cacca969c48237bcb563e2ab0b5afb50204470c7636 url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.0.9" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "64e8debf5b719a37d48b9785dd595d34133fdcd84b8fd07157a621c54ab2156f" + sha256: "215cb3cd54a97e09bc3512535fa4e8bedd3bf2a06dc2ae015ac8e0050fc71bd1" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.9" saf_stream: dependency: "direct main" description: name: saf_stream - sha256: "6fa5179bf5c8f5f99b183286df6236b23de33c1e874d69ce1fe565a63f95f62b" + sha256: "9a1b6f70527a52f7464c161f9cb05e8d471e71d460b987adbb94b15ed2b0e8cf" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.0.3" saf_util: dependency: "direct main" description: name: saf_util - sha256: "1e98cd63ddb78cbb3fa5cc0028283fc48381b867fa357f6b9417226eab464928" + sha256: c630df86aa412d3c5b715eb941ebefa9c97eefe31395bf45223ba5efb3862a43 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.1.1" sanitize_filename: dependency: "direct main" description: @@ -1241,42 +1257,42 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" + sha256: db40767f130189d53f3ae8a6393c217ff4a492da1baff718a9873679c0c00224 url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.3.1" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9" + sha256: "365ef7379fc22507256adda3385152942ffce08935452bc972c2e52a0bebae41" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + sha256: "1e85e0dccf5b4a016077fea7dfbb4a7cb39c759ac1596fcf60c06deadb7da91f" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.6" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" + sha256: "33b38b07d9ac7d16aff0426c61418752fd3e7a8c7dc8be786c875d6ce673b002" url: "https://pub.dev" source: hosted - version: "2.4.26" + version: "2.5.4" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + sha256: "4e396ac6f8751ab7d2d6e27f22f32c8350b07006028d534f5221503382f1fa69" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.6.1" shared_preferences_linux: dependency: transitive description: @@ -1313,10 +1329,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: e75e9cf311d7fd712048038cc17c25199c38536c20330d53e7db058b543d5a48 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.3" shelf_packages_handler: dependency: transitive description: @@ -1329,10 +1345,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + sha256: "480504af9e5323899c5be26a4f6c2fc4437d50954d39842f0290f4ae733d57b4" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.0" shelf_web_socket: dependency: transitive description: @@ -1358,18 +1374,18 @@ packages: dependency: transitive description: name: source_gen - sha256: ec37cc0e6694374cbef59ed79685572c870a54ede6fa30a3e420feb3adffea02 + sha256: a603f1fb984a7391ae5978d1b92bfaaa08b350dca5c825256f925818f7943bf5 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.4" source_helper: dependency: transitive description: name: source_helper - sha256: "4227d54ceefd0bb8ca4c8fcb96e1719dc53f1ee1b6e2ca9d7a6069da160e4eae" + sha256: "279126bce72de3f2b7d4756408a4157988cf6bb4815be551bf55851aa00e9984" url: "https://pub.dev" source: hosted - version: "1.3.12" + version: "1.4.0" source_map_stack_trace: dependency: transitive description: @@ -1382,10 +1398,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + sha256: "14c2945847669b44089bb1222f66873d7ff7103c58911917f2a63c5a62327898" url: "https://pub.dev" source: hosted - version: "0.10.13" + version: "0.10.14" source_span: dependency: transitive description: @@ -1398,10 +1414,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.2" state_notifier: dependency: transitive description: @@ -1422,10 +1438,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -1442,6 +1458,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + stringo: + dependency: transitive + description: + name: stringo + sha256: "95ad52452dbeafb77d811809163f8cae7e34e844d201baf605b391f37579993a" + url: "https://pub.dev" + source: hosted + version: "3.0.0" substring_highlight: dependency: "direct main" description: @@ -1462,26 +1486,26 @@ packages: dependency: transitive description: name: test - sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" + sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f url: "https://pub.dev" source: hosted - version: "1.31.0" + version: "1.31.1" test_api: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" test_core: dependency: transitive description: name: test_core - sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" + sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5 url: "https://pub.dev" source: hosted - version: "0.6.17" + version: "0.6.18" time: dependency: transitive description: @@ -1510,42 +1534,42 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + sha256: e8cc955c97e06df9879140a1fe301f3d493f01b7c62dec7f46834be31d7e9fda url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" url: "https://pub.dev" source: hosted - version: "6.3.32" + version: "6.3.33" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" url: "https://pub.dev" source: hosted - version: "6.4.1" + version: "6.4.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.2.3" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" url: "https://pub.dev" source: hosted - version: "3.2.5" + version: "3.2.6" url_launcher_platform_interface: dependency: transitive description: @@ -1566,34 +1590,34 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" uuid: dependency: "direct main" description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_math: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: fa49e1674cb84acb900141714e6c6e1f9ae099827c24d141430ff6819db4e1bc url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.4" vm_service: dependency: transitive description: name: vm_service - sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + sha256: "89b11d62e1040ee681af3b4f1c97a803ecbf010c6e8b9c5896ee5b08c3bf7b78" url: "https://pub.dev" source: hosted - version: "15.2.0" + version: "15.4.0" watcher: dependency: transitive description: @@ -1606,10 +1630,10 @@ packages: dependency: transitive description: name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + sha256: e119e82ac4795c7925103b7cf3768191f33941b99e7ccffbd6c645201d9f606b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web_socket: dependency: transitive description: @@ -1638,10 +1662,10 @@ packages: dependency: transitive description: name: win32 - sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.4.0" win32_registry: dependency: transitive description: @@ -1662,10 +1686,10 @@ packages: dependency: transitive description: name: xml - sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + sha256: "0fe8cc98946def31cbcbc8734aa4067031e6f65ea3a78a508060bfed2200bfeb" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.1.0" xxh3: dependency: transitive description: @@ -1678,10 +1702,10 @@ packages: dependency: transitive description: name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" yaml_edit: dependency: transitive description: @@ -1691,5 +1715,5 @@ packages: source: hosted version: "2.2.4" sdks: - dart: ">=3.12.0 <4.0.0" - flutter: "3.44.0" + dart: ">=3.13.5 <4.0.0" + flutter: "3.47.9" diff --git a/pubspec.yaml b/pubspec.yaml index 60f81d96..7d61f68f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,21 +7,21 @@ publish_to: none version: 2.2.2+38 environment: - sdk: ^3.12.0 - flutter: 3.44.0 + sdk: ^3.13.0 + flutter: 3.47.9 dependencies: after_layout: ^1.2.0 - archive: ^4.0.9 + archive: ^4.6.0 collection: ^1.19.1 const_date_time: ^1.2.1 - dart_helper_utils: ^6.0.2 - device_info_plus: ^13.1.0 - dynamic_color: ^1.8.1 + dart_helper_utils: ^7.0.0 + device_info_plus: ^13.3.0 + dynamic_color: ^1.9.0 encrypt: ^5.0.3 - equatable: ^2.0.8 + equatable: ^3.0.0 flag_secure: ^2.0.2 - fleather: ^1.27.0 + fleather: ">=1.27.0 <1.28.0" flex_color_picker: ^3.8.0 flutter: sdk: flutter @@ -31,54 +31,54 @@ dependencies: flutter_fgbg: ^0.8.0 flutter_localizations: sdk: flutter - flutter_markdown_plus: ^1.0.7 + flutter_markdown_plus: ^1.0.12 flutter_native_splash: ^2.4.8 - flutter_riverpod: ^3.3.1 - flutter_secure_storage: ^10.3.1 + flutter_riverpod: ">=3.4.3 <3.5.0" + flutter_secure_storage: ^11.2.0 flutter_staggered_grid_view: ^0.7.0 - gap: ^3.0.1 - go_router: ^17.3.0 - intl: ^0.20.2 + gap: ^3.1.0 + go_router: ^17.5.0 + intl: ^0.20.3 is_first_run: ^1.0.0 - isar_community: ^3.3.2 - isar_community_flutter_libs: ^3.3.2 - json_annotation: ^4.11.0 + isar_community: ^3.4.1 + isar_community_flutter_libs: ^3.4.1 + json_annotation: ^4.12.0 listen_sharing_intent: ^1.9.2 - local_auth: ^3.0.1 + local_auth: ^3.0.2 locale_names: ^1.1.1 - logger: ^2.7.0 + logger: ^2.8.0 markdown: ^7.3.1 - material_symbols_icons: ^4.2928.1 - package_info_plus: ^10.1.0 - parchment: any - parchment_delta: any + material_symbols_icons: ^4.2960.0 + package_info_plus: ^10.2.2 + parchment: ^1.27.0 + parchment_delta: ^1.0.0 path: ^1.9.1 - path_provider: ^2.1.5 - quick_actions: ^1.1.0 + path_provider: ^2.1.6 + quick_actions: ^1.1.1 remove_markdown: ^0.0.2 - restart_app: ^1.8.3 - riverpod_annotation: ^4.0.2 - saf_stream: ^2.0.0 - saf_util: ^2.2.0 + restart_app: ^1.10.2 + riverpod_annotation: ^4.0.7 + saf_stream: ^4.0.3 + saf_util: ^3.1.1 sanitize_filename: ^1.0.5 settings_tiles: ^6.0.1 - share_plus: ^13.1.0 - shared_preferences: ^2.5.5 + share_plus: ^13.3.1 + shared_preferences: ^2.5.6 simple_icons: ^16.23.0 string_validator: ^1.2.0 substring_highlight: ^1.0.33 - url_launcher: ^6.3.2 - uuid: ^4.5.3 + url_launcher: ^6.3.3 + uuid: ^4.6.0 dev_dependencies: - build_runner: ^2.15.0 + build_runner: ^2.16.1 build_verify: ^3.1.1 flutter_launcher_icons: ^0.14.4 flutter_lints: ^6.0.0 - isar_community_generator: ^3.3.2 - json_serializable: ^6.13.0 - riverpod_generator: ^4.0.3 - riverpod_lint: ^3.1.3 + isar_community_generator: ^3.4.1 + json_serializable: ^6.15.2 + riverpod_generator: ^4.0.9 + riverpod_lint: ^3.1.9 flutter: uses-material-design: true