Достижения
75 отметок в девяти группах. Часть выдаётся сама, часть — за действие в приложении, часть отмечается руками. Открывается из настроек.

Зачем этот экран
Приложение считает время, но время само по себе не событие. Достижение — это способ сказать «вот здесь что-то произошло»: месяц без пропусков, тысяча часов, первый навык до ранга «Опытный».
Часть из них — про жизнь, а не про приложение («пробежал марафон»). Проверить такое приложение не может и не пытается: эти отмечаются вручную и на этом основании ничем не хуже автоматических.
Что на экране
- Счётчик в шапке — «Открыто N из 75».
- Фильтры: «Все», «Открытые», «Закрытые».
- Карточки по группам: иконка группы, название достижения, дата получения или «Ещё не открыто».
- Легенда внизу — три вида достижений одной строкой.
Как этим пользоваться
Посмотреть, за что дают

Нажмите на карточку. В шторке — условие целиком, дата получения и пометка о том, как достижение выдаётся.
Отметить то, что случилось в жизни

У достижений группы «Жизнь» в шторке есть кнопка «Отметить» (и «Снять отметку», если передумали). Подтверждение прямо говорит, что проверить это не может никто, кроме вас.
Поделиться
Кнопка «Поделиться карточкой» рисует картинку 1080×1350 и отдаёт её системному меню «Поделиться». Публичных страниц у приложения нет, поэтому «поделиться» здесь означает «получить картинку и отправить её самому».
Три вида достижений
| Вид | Сколько | Как выдаётся |
|---|---|---|
| Автоматические | 53 | Считаются по истории и выдаются сами, когда данные сойдутся |
| За действие | 9 | Событие произошло и исчезло — «сделал обои», «скачал копию». В журнале такого не остаётся, поэтому отметка ставится в момент действия |
| Вручную | 13 | Про жизнь, а не про приложение |
Полный список — в справочнике достижений.
Все семьдесят пять

Кадр снят на демо-профиле «Максимум»: на обычной истории почти все карточки заперты, и по ним не понять, что в реестре вообще бывает. У открытой карточки — цвет группы и дата выдачи, у запертой — приглушённый контур и «Ещё не открыто».
Пустое состояние

У фильтров разные пустые тексты: «Пока ничего не открыто. Отметьте первый блок — начнётся само» и «Здесь пока пусто — и это нормально».
Пределы и предупреждения
- Автоматические достижения не снимаются. Условие вроде «за 30 дней доля лидера не выше 40 %» иначе мигало бы каждый день вместе со скользящим окном.
- При выключенном модуле «Навыки» 13 достижений исчезают из знаменателя — счётчик показывает меньшее общее число, а не вечно недостижимый остаток.
- «Стереть историю» снимает автоматические достижения (их основания больше нет), но оставляет отметки за действия и ручные.
- Достижения из более новой версии приложения старая версия не стирает: неизвестный идентификатор из облака сохраняется как есть.
- Проверка запускается не на каждый тап, а через 0,7 секунды после изменения данных — с той же паузой, с какой данные пишутся в хранилище.
Чего здесь нельзя
- Снять автоматическое достижение. Только полный сброс истории.
- Добавить своё. Список зашит в приложение.
- Увидеть прогресс до следующего. Карточка показывает условие, а не «осталось 12 часов»: это превратило бы наблюдение в задание.
- Отметить вручную то, что выдаётся само. Кнопка есть только у группы «Жизнь».
Рядом
Справочник достижений · Модули · Сброс
Как это работает внутри
- Экран —
src/screens/AchievementsScreen.tsx, шторка —src/achievements/AchievementSheet.tsx, реестр —src/achievements/registry.ts. - Идентификатор — два символа и он никогда не меняется и не переиспользуется: переименование превратило бы чужой трофей в другой. Отставленные идентификаторы копятся в
RETIRED_IDS. - Тип
AchievementIdвыводится из ключейru.ts(src/achievements/types.ts), поэтому опечатка в идентификаторе или забытое название — ошибка компиляции, а не пустая карточка. - Условия — предикаты над
Derived(src/achievements/derive.ts): один проход по истории превращает ~75 проверок в сравнения чисел. - Выдачу ведёт невидимый компонент
src/achievements/Watcher.tsxс задержкойCHECK_DELAY_MS = 700. Он намеренно не подписан на сами награды, иначе выдача перезапускала бы проверку по кругу. evaluateсохраняет идентичность объекта наград, когда ничего не изменилось, — именно это и останавливает лишнюю запись в хранилище.stripAuto(сброс истории) убирает толькоkind === 'auto'и сохраняет незнакомые идентификаторы.- Карточка для «поделиться» рисуется на canvas в
src/achievements/card.ts; сохранение идёт через ту же лестницу, что и обои (src/wallpaper/save.ts). - Реестр покрыт
src/achievements/registry.test.ts: уникальность и длина идентификаторов, соответствиеauto⇄test, наличие названия и условия у каждого, и то, что весь реестр целиком влезает в предел хранилища.