Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/hooks/format.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Formats the Dart file that Claude just edited (PostToolUse hook).
import 'dart:convert';
import 'dart:io';

Future<void> 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);
}
46 changes: 46 additions & 0 deletions .claude/hooks/session-start.sh
Original file line number Diff line number Diff line change
@@ -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
66 changes: 66 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
]
}
}
25 changes: 25 additions & 0 deletions .claude/skills/check/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 35 additions & 0 deletions .claude/skills/database/SKILL.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions .claude/skills/l10n/SKILL.md
Original file line number Diff line number Diff line change
@@ -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_<lang>.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.<key>` (`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.
22 changes: 22 additions & 0 deletions .claude/skills/preference/SKILL.md
Original file line number Diff line number Diff line change
@@ -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<bool>(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<String>` 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_<name>`, `settings_<name>_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.
21 changes: 21 additions & 0 deletions .claude/skills/release/SKILL.md
Original file line number Diff line number Diff line change
@@ -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/<N*10>.txt` and `fr-FR/changelogs/<N*10>.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.
2 changes: 1 addition & 1 deletion .fvmrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"flutter": "3.44.0"
"flutter": "3.47.9"
}
Loading
Loading