xima.keeps - заметки, поиск по ним и системные напоминания на Android. Все работает на устройстве: без сервера, аккаунта, облачной синхронизации, аналитики и обязательного интернета. Не «оффлайн-первый», а просто оффлайн - сетевого кода в приложении нет вообще.
Стек
| Слой | Чем сделан |
|---|---|
| Оболочка | 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 заново отдает системе после перезагрузки телефона, не заглядывая
в базу.
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 намеренно минимален.
Сборка и проверки
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.
Подпись у всех релизов одна, поэтому обновление ставится поверх и данные сохраняются.
