xima.keeps - заметки, поиск по ним и системные напоминания на Android. Все работает на устройстве: без сервера, аккаунта, облачной синхронизации, аналитики и обязательного интернета. Не «оффлайн-первый», а просто оффлайн - сетевого кода в приложении нет вообще.

proxima812/notes-androidИсходники: Tauri 2, Rust, React 19, SQLite.

Стек

Слой Чем сделан
Оболочка Tauri 2
Ядро Rust: бизнес-логика, SQLite, поиск, валидация
Интерфейс React 19, TypeScript, Tailwind CSS v4
Хранилище локальная SQLite + FTS5
Напоминания Rust + Kotlin (AlarmManager, NotificationManager)
Голос Kotlin (SpeechRecognizer, только офлайн-модель устройства)

В цифрах: 54 файла на Rust против 31 компонента на React. Соотношение показательное - интерфейс тут тонкий, вся работа в ядре.

Границы слоев

Четыре правила, которые не нарушаются:

  • React не обращается к SQLite и не содержит бизнес-логики.
  • Kotlin содержит только вызовы Android API, без бизнес-логики.
  • SQL живет в репозиториях, а не в Tauri-командах.
  • Системные напоминания не используют setTimeout и setInterval.

Последнее - принципиальное. Таймер в JS живет ровно столько, сколько живет процесс приложения, а напоминание должно сработать через три дня на выключенном экране. Поэтому будильники ставит AlarmManager, а Kotlin помнит только те, что поставил сам (AlarmStore). Это не вторая копия напоминаний: это то, что BootReceiver заново отдает системе после перезагрузки телефона, не заглядывая в базу.

TEXT
src-tauri/
├── src/
│   ├── domain/          notes, reminders, tasks, organisation, backup,
│   │                    app_icons, settings, search, clock, ids
│   ├── application/     commands, use_cases, dto
│   ├── infrastructure/  sqlite: соединение, миграции, репозитории
│   └── platform/        мост к Android-плагинам
├── migrations/          версионируемые SQL-миграции
├── plugins/reminders/   Rust API + Kotlin (AlarmManager)
├── plugins/documents/   системный выбор файла
└── plugins/appicon/     переключение иконки лаунчера

Схема больше, чем продукт

Начальная миграция 0001_initial.sql заложена под весь продукт целиком: задачи, вложения, теги, повторения, бэкапы. Написана из этого пока меньшая часть, и в README это разведено честно - таблица «есть / еще нет» описывает написанное, а не схему.

Папки схема тоже несла. Их убрали целиком, миграцией 0002_drop_folders.sql, потому что теги делают то же самое одним способом вместо двух. Это редкий и правильный ход: удалить половину модели данных, когда стало видно, что она дублирует другую половину.

Голосовая заметка

Кнопка с микрофоном в библиотеке плюс ярлык «Продиктовать» по долгому нажатию на иконку приложения. Сказали «встреча, 15:00» - появилась заметка «Встреча» и напоминание в 14:30. Запас в полчаса настраивается.

Разбор фразы живет в src-tauri/src/domain/quick_notes/phrase.rs и понимает куда больше, чем цифры:

Сказано Что получится
в 15 30, в 15 часов 30 минут сегодня в 15:30, а если время прошло - завтра
в три часа, полвторого, без пятнадцати шесть, полдень числа словами, час читается как дневной
в 9 утра, в семь вечера, в час дня часть суток задает половину суток явно
завтра, послезавтра, в пятницу ближайший такой день

Распознавание - офлайн-модель самого устройства через SpeechRecognizer, ничего никуда не уходит.

Что уже работает

Заметки с форматированием, шаблонами и цветовыми градиентами. Библиотека в две колонки, и на карточке видно ближайшее напоминание. Полнотекстовый поиск на FTS5 с историей запросов. Чек-листы, где задачи - это строки, а не галочки внутри текста. Теги с фильтром в библиотеке. Резервная копия в файл и восстановление из него. Четыре темные палитры и восемь языков интерфейса, среди них татарский, башкирский, крымскотатарский и казахский.

Корзина устроена с оговоркой: на возврат дается час, дальше заметка стирается сама. Это не «удалить навсегда» кнопкой, а срок, после которого хранить уже нечего.

Напоминаний на одну заметку может быть несколько, со звуком и шаблонами времени. Повторы: ежедневно, по будням, еженедельно, ежемесячно, ежегодно. В шторке уведомления две кнопки, «Готово» и «Отложить».

Отдельная деталь: «Готово» убирает уведомление и больше ничего. Повтор - это серия, и одно отработавшее срабатывание ничего не говорит о следующем.

Еще одна: когда в настройках меняется срок откладывания, уже поставленные будильники перевзводятся. Срок едет внутри самого будильника, потому что уведомление показывает приемник, который может работать с мертвым процессом приложения.

Чего нет: вложений и голосовых заметок как файлов, шифрования базы, приоритетов и сроков у задач.

Приватность

Сетевого кода нет. CSP в tauri.conf.json запрещает любые внешние источники (default-src 'self'), а список разрешений Tauri в src-tauri/capabilities/default.json намеренно минимален.

Сборка и проверки

Bash
bun install
bun run dev                  # только Vite, в браузере, без Rust-команд
bun run android:dev          # на подключенном устройстве с HMR
bun run android:build        # release APK для arm64

Полная проверка перед сдачей этапа - это девять команд подряд: строгий TypeScript, сборка фронтенда, cargo fmt --check, clippy с -D warnings, тесты ядра и отдельно тесты каждого из трех плагинов, vitest и Kotlin-тесты.

CI гоняет на каждый push все, что можно проверить без устройства. Kotlin-тесты туда не входят: модуль плагина собирается против .tauri/tauri-api, который генерируется сборкой Tauri и в чистом клоне отсутствует. Их запускают локально после хотя бы одной Android-сборки.

Версии и релизы

Пути и пароли для подписи собираются из одного файла: .env в корне, он в .gitignore, а рядом лежит .env.example с пустыми значениями. Из четырех строк scripts/android-env.sh собирает keystore.properties, который ждет Gradle.

Версия одна и та же в трех местах: package.json, src-tauri/tauri.conf.json и src-tauri/Cargo.toml. Каждому релизу соответствует git-тег vX.Y.Z, запись в CHANGELOG.md в формате Keep a Changelog и подписанный APK в Releases.

Подпись у всех релизов одна, поэтому обновление ставится поверх и данные сохраняются.

Последний релизГотовый APK: скачать, открыть на телефоне, разрешить установку.