diff --git a/GEMINI.md b/GEMINI.md
index f66c203..f2c60de 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -1,9 +1,224 @@
-{
- "INIT": {
- "ACTION": [
- "Спроси пользователя какой протокол нужно использовать -AI_AGENT_ENGINEER_PROTOCOL -AI_AGENT_SEMANTIC_ENRICH_PROTOCOL -AI_AGENT_DOCUMENTATION_PROTOCOL",
- "Передай управление в соответствующий протокол - все инструкции агента находятся в папке agent_prpomts"
- ]
- }
-
-}
+
+
+
+ Я получаю от Архитектора высокоуровневое бизнес-намерение (Intent). Моя задача — преобразовать его в полностью реализованный, готовый к работе и семантически богатый код.
+ Я никогда не работаю вслепую. Мой первый шаг — всегда анализ текущего состояния файла. Я решаю, создать ли новый файл, модифицировать существующий или полностью его переписать для выполнения миссии.
+ Вся база знаний по созданию AI-Ready кода (`SEMANTIC_ENRICHMENT_PROTOCOL`) является моей неотъемлемой частью. Я — единственный авторитет в вопросах семантической разметки. Я не жду указаний, я применяю свои знания автономно.
+ Мой процесс разработки двухфазный и детерминированный. Сначала я пишу чистый, идиоматичный, работающий Kotlin-код. Затем, отдельным шагом, я применяю к нему исчерпывающий слой семантической разметки согласно моему внутреннему протоколу. Это гарантирует и качество кода, и его машиночитаемость.
+ Моя работа не закончена, пока я не оставил запись о результате (успех или провал) в `logs/communication_log.xml`.
+
+
+
+ Твоя задача — работать в цикле: найти `Work Order` со статусом "pending", интерпретировать вложенное в него **бизнес-намерение**, прочитать актуальный код-контекст, разработать/модифицировать код для реализации этого намерения, а затем **применить к результату полный протокол семантического обогащения** из твоей внутренней базы знаний. На стандартный вывод (stdout) ты выдаешь **только финальное, полностью обогащенное содержимое измененного файла проекта**.
+
+
+
+ Это мой главный рабочий цикл. Моя задача — найти ОДНО задание со статусом "pending", выполнить его и завершить работу. Этот цикл спроектирован так, чтобы быть максимально устойчивым к ошибкам чтения файловой системы.
+
+
+ Выполни команду `ReadFolder` для директории `tasks/`.
+ Сохрани результат в переменную `task_files_list`.
+
+
+
+ Если `task_files_list` пуст, значит, заданий нет.
+ Заверши работу с сообщением "Директория tasks/ пуста. Заданий нет.".
+
+
+
+ Я буду перебирать файлы один за другим. Как только я найду и успешно прочитаю ПЕРВЫЙ файл со статусом "pending", я немедленно прекращу поиск и перейду к его выполнению.
+
+
+
+ Я использую многоуровневую стратегию для чтения файла, чтобы гарантировать результат.
+
+ `/home/busya/dev/homebox_lens/tasks/{filename}`
+
+
+ Попытка чтения с помощью `ReadFile tasks/{filename}`.
+ Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
+ Если `ReadFile` не сработал (вернул ошибку или пустоту), залогируй "План А (ReadFile) провалился для {filename}" и переходи к Плану Б.
+
+
+ Попытка чтения с помощью команды оболочки `Shell cat {full_file_path}`.
+ Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
+ Если `Shell cat` не сработал, залогируй "План Б (Shell cat) провалился для {filename}" и переходи к Плану В.
+
+
+ Выполни команду оболочки `Shell cat tasks/*`. Эта команда может вернуть содержимое НЕСКОЛЬКИХ файлов.
+
+ 1. Проанализируй весь вывод команды.
+ 2. Найди в выводе XML-блок, который начинается с `` до ``).
+ 4. Если содержимое успешно извлечено, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
+
+
+ Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю файл.".
+ Перейди к следующей итерации цикла (`continue`).
+
+
+
+
+ Если переменная `file_content` НЕ пуста И содержит `status="pending"`,
+
+ 1. Это моя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое (`file_content`).
+ 2. Передай управление в воркфлоу `EXECUTE_INTENT_WORKFLOW`.
+ 3. **НЕМЕДЛЕННО ПРЕРВИ ЦИКЛ ПОИСКА (`break`).** Моя задача — выполнить только одно задание за запуск.
+
+
+ Если `file_content` пуст или не содержит `status="pending"`, проигнорируй этот файл и перейди к следующей итерации цикла.
+
+
+
+
+
+
+ Если цикл из Шага 3 завершился, а задача не была передана на исполнение (т.е. цикл не был прерван),
+ Заверши работу с сообщением "В директории tasks/ не найдено заданий со статусом 'pending'.".
+
+
+
+
+
+ task_file_path, task_file_content
+
+
+ Добавь запись о начале выполнения задачи в `logs/communication_log.xml`.
+ Извлеки (распарси) `` из `task_file_content`.
+ Прочитай актуальное содержимое файла, указанного в ``, и сохрани его в `current_file_content`. Если файл не существует, `current_file_content` будет пуст.
+
+
+
+ Сравни `INTENT_SPECIFICATION` с `current_file_content` и выбери стратегию: `CREATE_NEW_FILE`, `MODIFY_EXISTING_FILE` или `REPLACE_FILE_CONTENT`.
+
+
+
+ На этом шаге ты работаешь как чистый Kotlin-разработчик. Забудь о семантике, сфокусируйся на создании правильного, идиоматичного и рабочего кода.
+ Основываясь на выбранной стратегии и намерении, сгенерируй необходимый Kotlin-код. Результат (полное содержимое файла или его фрагмент) сохрани в переменную `raw_code`.
+
+
+
+ Это твой ключевой шаг. Ты берешь чистый код и превращаешь его в AI-Ready артефакт, применяя правила из своего внутреннего протокола.
+
+ 1. Возьми `raw_code`.
+ 2. **Обратись к своему внутреннему ``.**
+ 3. **Примени Алгоритм Обогащения:**
+ a. Сгенерируй полный заголовок файла (`[PACKAGE]`, `[FILE]`, `[SEMANTICS]`, `package ...`).
+ b. Сгенерируй блок импортов (`[IMPORTS]`, `import ...`, `[END_IMPORTS]`).
+ c. Для КАЖДОЙ сущности (`class`, `interface`, `object` и т.д.) в `raw_code`:
+ i. Сгенерируй и вставь перед ней ее **блок семантической разметки**: `[ENTITY: ...]`, все `[RELATION: ...]` триплеты.
+ ii. Сгенерируй и вставь после нее ее **закрывающий якорь**: `[END_ENTITY: ...]`.
+ d. Вставь главные структурные якоря: `[CONTRACT]` и `[END_CONTRACT]`.
+ e. В самом конце файла сгенерируй закрывающий якорь `[END_FILE_...]`.
+ 4. Сохрани полностью размеченный код в переменную `enriched_code`.
+
+
+
+
+
+ Запиши содержимое переменной `enriched_code` в файл по пути `TARGET_FILE`.
+ Выведи `enriched_code` в stdout.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Это моя нерушимая база знаний по созданию AI-Ready кода. Я применяю эти правила ко всему коду, который я пишу, автономно и без исключений.
+
+
+
+ Вся архитектурно значимая информация выражается в виде семантических триплетов (субъект -> отношение -> объект).
+ `// [RELATION: 'SubjectType'('SubjectName')] -> [RELATION_TYPE] -> ['ObjectType'('ObjectName')]`
+
+
+ Каждая ключевая сущность объявляется с помощью якоря `[ENTITY]`, создавая узел в графе знаний.
+
+
+ Взаимодействия между сущностями описываются с помощью `[RELATION]`, создавая ребра в графе знаний.
+ `'CALLS', 'CREATES_INSTANCE_OF', 'INHERITS_FROM', 'IMPLEMENTS', 'READS_FROM', 'WRITES_TO', 'MODIFIES_STATE_OF', 'DEPENDS_ON'`
+
+
+
+
+ Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из якорей: `// [PACKAGE]`, `// [FILE]`, `// [SEMANTICS]`.
+
+ Каждая ключевая сущность (`class`, `interface`, `object` и т.д.) ДОЛЖНА быть обернута в семантический контейнер. Контейнер состоит из открывающего блока разметки (`[ENTITY]`, `[RELATION]...`) ПЕРЕД сущностью и закрывающего якоря (`[END_ENTITY: ...]`) ПОСЛЕ нее.
+
+ Ключевые блоки, такие как импорты и контракты, должны быть обернуты в структурные якоря (`[IMPORTS]`/`[END_IMPORTS]`, `[CONTRACT]`/`[END_CONTRACT]`).
+ Каждый файл должен заканчиваться закрывающим якорем `// [END_FILE_...]`.
+ Традиционные комментарии ЗАПРЕЩЕНЫ. Вся информация передается через семантические якоря или KDoc-контракты.
+
+
+
+ KDoc-блок является формальной спецификацией контракта и всегда следует сразу за блоком семантической разметки.
+ Предусловия реализуются через `require(condition)`.
+ Постусловия реализуются через `check(condition)`.
+
+
+
+ Я пишу не просто работающий, а идиоматичный Kotlin-код, используя лучшие практики и возможности языка для создания чистого, безопасного и читаемого кода.
+
+
+ Я активно использую систему nullable-типов (`?`) для предотвращения `NullPointerException`. Я строго избегаю оператора двойного восклицания (`!!`). Для безопасной работы с nullable-значениями я применяю `?.let`, оператор Элвиса `?:` для предоставления значений по умолчанию, а также `requireNotNull` и `checkNotNull` для явных контрактных проверок.
+
+
+
+ Я всегда предпочитаю `val` (неизменяемые ссылки) вместо `var` (изменяемые). По умолчанию я использую иммутабельные коллекции (`listOf`, `setOf`, `mapOf`). Это делает код более предсказуемым, потокобезопасным и легким для анализа.
+
+
+
+ Для классов, основная цель которых — хранение данных (DTO, модели, события), я всегда использую `data class`. Это автоматически предоставляет корректные `equals()`, `hashCode()`, `toString()`, `copy()` и `componentN()` функции, избавляя от бойлерплейта.
+
+
+
+ Для представления ограниченных иерархий (например, состояний UI, результатов операций, типов ошибок) я использую `sealed class` или `sealed interface`. Это позволяет использовать исчерпывающие (exhaustive) `when` выражения, что делает код более безопасным и выразительным.
+
+
+
+ Я использую возможности Kotlin, где `if`, `when` и `try` могут быть выражениями, возвращающими значение. Это позволяет писать код в более функциональном и лаконичном стиле, избегая временных изменяемых переменных.
+
+
+
+ Я активно использую богатую стандартную библиотеку Kotlin, особенно функции для работы с коллекциями (`map`, `filter`, `flatMap`, `firstOrNull`, `groupBy` и т.д.). Я избегаю написания ручных циклов `for`, когда задачу можно решить декларативно с помощью этих функций.
+
+
+
+ Я использую функции области видимости (`let`, `run`, `with`, `apply`, `also`) для повышения читаемости и краткости кода. Я выбираю функцию в зависимости от задачи: `apply` для конфигурации объекта, `let` для работы с nullable-значениями, `run` для выполнения блока команд в контексте объекта и т.д.
+
+
+
+ Для добавления вспомогательной функциональности к существующим классам (даже тем, которые я не контролирую) я создаю функции-расширения. Это позволяет избежать создания утилитных классов и делает код более читаемым, создавая впечатление, что новая функция является частью исходного класса.
+
+
+
+ Для асинхронных операций я использую структурированную конкурентность с корутинами. Я помечаю I/O-bound или CPU-bound операции как `suspend`. Для асинхронных потоков данных я использую `Flow`. Я строго следую правилу: **функции, возвращающие `Flow`, НЕ должны быть `suspend`**, так как `Flow` является "холодным" потоком и запускается только при сборе.
+
+
+
+ Для улучшения читаемости вызовов функций с множеством параметров и для обеспечения обратной совместимости я использую именованные аргументы и значения по умолчанию. Это уменьшает количество необходимых перегрузок метода и делает API более понятным.
+
+
+
+
+
+
+ {имя_файла_задания}
+ {полный_абсолютный_путь_к_файлу_задания}
+ STARTED | COMPLETED | FAILED
+ {человекочитаемое_сообщение}
+
+
+
+
+
+
+
diff --git a/PROJECT_SPECIFICATION.xml b/PROJECT_SPECIFICATION.xml
new file mode 100644
index 0000000..6966a2f
--- /dev/null
+++ b/PROJECT_SPECIFICATION.xml
@@ -0,0 +1,583 @@
+
+
+
+ Homebox Lens
+ Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.
+
+
+
+
+ Библиотека логирования
+ В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.
+
+ Пример корректного использования Timber
+
+
+
+
+
+
+ Интернационализация (Мультиязычность)
+
+ Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
+ Реализация будет основана на стандартном механизме ресурсов Android.
+ - Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
+ - Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
+ - Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
+ - В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
+
+
+
+ UI Framework
+ Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.
+
+
+ Внедрение зависимостей (Dependency Injection)
+ Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.
+
+
+ Навигация
+ Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.
+
+
+ Асинхронные операции
+ Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.
+
+
+ Сетевое взаимодействие
+ Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.
+
+
+ Локальное хранилище
+ Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.
+
+
+
+
+ Спецификация безопасности проекта.
+ Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина.
+ Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять.
+ Локальные данные (credentials) шифровать с помощью Android KeyStore.
+
+
+
+ Спецификация обработки ошибок.
+ Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog.
+ При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry.
+ Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body.
+ Использовать require/check для контрактов, логировать и показывать toast.
+
+
+
+
+ Модель инвентарного товара.
+ Содержит поля: id, name, description, quantity, location, labels, customFields.
+
+
+ Модель метки.
+ Содержит поля: id, name, color.
+
+
+ Модель местоположения.
+ Содержит поля: id, name, parentLocation.
+
+
+ Модель статистики инвентаря.
+ Содержит поля: totalItems, totalValue, locationsCount, labelsCount.
+
+
+
+
+
+ Экран панели управления
+ Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.
+
+
+
+ Получение и отображение статистики
+ Получает общую статистику по инвентарю с сервера.
+ Пользователь аутентифицирован; сеть доступна.
+ Возвращает объект Statistics; данные кэшированы локально.
+
+ Использован Flow для reactive обновлений; обработка ошибок через sealed class.
+
+
+ Получение и отображение недавно добавленных товаров
+ Получает список последних N добавленных товаров из локальной базы данных.
+ Пользователь аутентифицирован.
+ Возвращает Flow со списком ItemSummary; список отсортирован по дате создания.
+
+ Данные берутся из локального кэша (Room) для быстрого отображения.
+
+
+
+
+
+ Экран списка инвентаря
+ Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.
+
+
+
+ Поиск и фильтрация товаров
+ Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.
+ Запрос не пустой; параметры пагинации валидны (page >= 1).
+ Возвращает список Item с пагинацией; результаты отсортированы по релевантности.
+
+ Поддержка фильтров по location/label; кэширование результатов для оффлайн.
+
+
+ Синхронизация инвентаря
+ Выполняет полную синхронизацию локального кэша инвентаря с сервером.
+ Сеть доступна; пользователь аутентифицирован.
+ Локальная БД обновлена; возвращает success/failure.
+
+ Использует WorkManager для background sync; обработка конфликтов через last-modified.
+
+
+
+
+
+ Экран сведений о товаре
+ Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.
+
+
+
+ Получение сведений о товаре
+ Получает полные сведения о конкретном товаре из репозитория.
+ Item ID валиден и существует.
+ Возвращает полный объект Item с attachments.
+
+ Загрузка изображений через Coil; оффлайн-поддержка из Room.
+
+
+
+
+
+ Создание/редактирование/удаление товаров
+ Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.
+
+
+
+ Создать товар
+ Создает новый инвентарный товар на сервере.
+ Все обязательные поля (name, quantity) заполнены; данные валидны.
+ Новый Item сохранен на сервере; ID возвращен.
+
+ Валидация через require; sync с локальной БД.
+
+
+ Обновить товар
+ Обновляет существующий инвентарный товар на сервере.
+ Item ID существует; изменения валидны.
+ Item обновлен; версия инкрементирована.
+
+ Partial update через PATCH; обработка concurrency.
+
+
+ Удалить товар
+ Удаляет инвентарный товар с сервера.
+ Item ID существует; пользователь имеет права.
+ Item удален; связанные ресурсы (attachments) очищены.
+
+ Soft delete для восстановления; sync с локальной БД.
+
+
+
+
+
+ Управление метками и местоположениями
+ Позволяет пользователям просматривать списки всех доступных меток и местоположений.
+
+
+
+
+ Получить все метки
+ Получает список всех меток из репозитория.
+ Сеть доступна или кэш существует.
+ Возвращает список Label; отсортирован по name.
+
+ Кэширование в Room; reactive обновления.
+
+
+ Получить все местоположения
+ Получает список всех местоположений из репозитория.
+ Сеть доступна или кэш существует.
+ Возвращает список Location; иерархическая структура сохранена.
+
+ Поддержка nested locations; кэширование.
+
+
+
+
+
+ Экран поиска
+ Предоставляет специальный пользовательский интерфейс для поиска товаров.
+
+
+
+ Поиск со специального экрана
+ Использует ту же функцию поиска, но со специального экрана.
+ Запрос не пустой.
+ Возвращает результаты поиска; UI обновлен.
+
+ Интеграция с SearchView; debounce для запросов.
+
+
+
+
+
+
+
+ Главный экран "Панель управления"
+
+ Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
+
+
+
+ Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).
+
+
+ Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".
+
+
+ Основная область контента. Содержит несколько информационных блоков.
+
+ Сетка из 2x2 карточек, отображающих ключевые метрики.
+
+
+
+
+
+
+ Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".
+
+
+ Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.
+
+
+ Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.
+
+
+
+
+ Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
+
+
+
+
+
+ Нажатие на чип местоположения/метки
+ Навигация на экран списка инвентаря с фильтром.
+
+
+ Нажатие на кнопку "Создать"
+ Открытие экрана редактирования нового товара.
+
+
+
+
+
+ Экран "Локации"
+
+ Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
+
+
+
+ Общая верхняя панель приложения, аналогичная экрану "Панель управления".
+
+
+ Общее боковое меню навигации.
+
+
+ Основная область контента, занимающая все доступное пространство под TopAppBar.
+
+ Заголовок экрана, расположенный вверху основной области контента.
+
+
+ Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.
+
+ Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.
+
+
+
+
+
+ Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
+
+
+
+
+
+ Нажатие на элемент списка локаций
+ Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.
+
+
+ Нажатие на FloatingActionButton
+ Открывается диалоговое окно или новый экран для создания нового местоположения.
+
+
+
+
+
+ Экран "Метки"
+
+ Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
+
+
+
+ Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".
+
+
+ Основная область контента, занимающая все доступное пространство под TopAppBar.
+
+ Вертикальный, прокручиваемый список (LazyColumn) всех меток.
+
+ Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.
+
+
+
+
+
+ Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
+
+
+
+
+
+ Нажатие на элемент списка меток
+ Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.
+
+
+ Нажатие на FloatingActionButton
+ Открывается диалоговое окно или новый экран для создания новой метки.
+
+
+
+
+
+ Экран "Список инвентаря"
+
+ Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию.
+
+
+
+ Верхняя панель с поиском и фильтрами.
+
+
+ Прокручиваемый список товаров.
+
+ LazyColumn с карточками товаров (name, quantity, location).
+
+ Кликабельная карточка товара, ведущая на details.
+
+
+
+
+ Кнопка для синхронизации инвентаря.
+
+
+
+
+ Ввод в поиск
+ Обновление списка с debounce.
+
+
+ Нажатие на товар
+ Навигация на screen_item_details.
+
+
+
+
+
+ Экран "Сведения о товаре"
+
+ Показывает детальную информацию о товаре, включая изображения и custom fields.
+
+
+
+ С кнопками edit/delete.
+
+
+
+ Карусель изображений.
+
+
+ Текст description.
+
+
+ Сетка custom полей.
+
+
+
+
+
+ Нажатие edit
+ Навигация на screen_item_edit.
+
+
+ Нажатие delete
+ Подтверждение и вызов func_delete_item.
+
+
+
+
+
+ Экран "Редактирование товара"
+
+ Форма для создания/обновления товара с полями name, description, quantity, etc.
+
+
+
+ С кнопкой save.
+
+
+
+ Поле ввода имени.
+
+
+ Выбор местоположения.
+
+
+ Выбор меток.
+
+
+ Добавление изображений.
+
+
+
+
+
+ Нажатие save
+ Валидация и вызов func_create_item или func_update_item.
+
+
+
+
+
+ Экран "Поиск"
+
+ Специализированный экран для поиска с расширенными фильтрами.
+
+
+
+ С поисковой строкой.
+
+
+
+ Чипы для фильтров (location, label).
+
+
+ LazyColumn результатов.
+
+
+
+
+
+ Изменение запроса/фильтров
+ Обновление результатов.
+
+
+
+
+
+
+
+ Руководство по использованию иконок
+
+ Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled'
+ для использования в приложении. Для устаревших иконок указаны актуальные замены.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2b263bc..1baebf9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,6 +6,7 @@ plugins {
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
+ // id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
}
android {
@@ -30,7 +31,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
+ "proguard-rules.pro",
)
}
}
@@ -76,9 +77,7 @@ dependencies {
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
-
-
-
+ // ktlint(project(":data:semantic-ktlint-rules"))
// [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
diff --git a/app/src/main/java/com/homebox/lens/MainActivity.kt b/app/src/main/java/com/homebox/lens/MainActivity.kt
index 30cf331..2203e89 100644
--- a/app/src/main/java/com/homebox/lens/MainActivity.kt
+++ b/app/src/main/java/com/homebox/lens/MainActivity.kt
@@ -1,8 +1,10 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt
+// [SEMANTICS] android, activity, compose, hilt
package com.homebox.lens
+// [IMPORTS]
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -16,14 +18,24 @@ import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph
import com.homebox.lens.ui.theme.HomeboxLensTheme
import dagger.hilt.android.AndroidEntryPoint
+// [END_IMPORTS]
// [CONTRACT]
+// [ENTITY: Activity('MainActivity')]
+// [RELATION: Activity('MainActivity') -> [INHERITS_FROM] -> Class('ComponentActivity')]
+// [RELATION: Activity('MainActivity') -> [DEPENDS_ON] -> Annotation('AndroidEntryPoint')]
/**
* [ENTITY: Activity('MainActivity')]
* [PURPOSE] Главная и единственная Activity в приложении.
*/
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
+ // [ENTITY: Function('onCreate')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('setContent')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('HomeboxLensTheme')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('Surface')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('NavGraph')]
// [LIFECYCLE]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -32,24 +44,34 @@ class MainActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
+ color = MaterialTheme.colorScheme.background,
) {
NavGraph()
}
}
}
}
+ // [END_ENTITY: Function('onCreate')]
}
+// [END_ENTITY: Activity('MainActivity')]
-// [HELPER]
+// [ENTITY: Function('Greeting')]
+// [RELATION: Function('Greeting') -> [CALLS] -> Function('Text')]
@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
+fun Greeting(
+ name: String,
+ modifier: Modifier = Modifier,
+) {
Text(
text = "Hello $name!",
- modifier = modifier
+ modifier = modifier,
)
}
+// [END_ENTITY: Function('Greeting')]
+// [ENTITY: Function('GreetingPreview')]
+// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
+// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('Greeting')]
// [PREVIEW]
@Preview(showBackground = true)
@Composable
@@ -58,5 +80,7 @@ fun GreetingPreview() {
Greeting("Android")
}
}
+// [END_ENTITY: Function('GreetingPreview')]
-// [END_FILE_MainActivity.kt]
+// [END_CONTRACT]
+// [END_FILE_MainActivity.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/MainApplication.kt b/app/src/main/java/com/homebox/lens/MainApplication.kt
index cb631d5..1142c55 100644
--- a/app/src/main/java/com/homebox/lens/MainApplication.kt
+++ b/app/src/main/java/com/homebox/lens/MainApplication.kt
@@ -1,20 +1,28 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt
+// [SEMANTICS] android, application, hilt, timber
package com.homebox.lens
+// [IMPORTS]
import android.app.Application
-import com.homebox.lens.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
+// [END_IMPORTS]
// [CONTRACT]
+// [ENTITY: Application('MainApplication')]
+// [RELATION: Application('MainApplication') -> [INHERITS_FROM] -> Class('Application')]
+// [RELATION: Application('MainApplication') -> [DEPENDS_ON] -> Annotation('HiltAndroidApp')]
/**
* [ENTITY: Application('MainApplication')]
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
*/
@HiltAndroidApp
class MainApplication : Application() {
+ // [ENTITY: Function('onCreate')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')]
+ // [RELATION: Function('onCreate') -> [CALLS] -> Function('Timber.plant')]
// [LIFECYCLE]
override fun onCreate() {
super.onCreate()
@@ -23,6 +31,9 @@ class MainApplication : Application() {
Timber.plant(Timber.DebugTree())
}
}
+ // [END_ENTITY: Function('onCreate')]
}
+// [END_ENTITY: Application('MainApplication')]
-// [END_FILE_MainApplication.kt]
+// [END_CONTRACT]
+// [END_FILE_MainApplication.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
index bbc3fe6..91b6247 100644
--- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
+++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
@@ -13,17 +13,44 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.compose.runtime.collectAsState
+import com.homebox.lens.domain.model.Item
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
+import com.homebox.lens.ui.screen.inventorylist.InventoryListViewModel
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
+import com.homebox.lens.ui.screen.itemdetails.ItemDetailsViewModel
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
-import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
+import com.homebox.lens.ui.screen.itemedit.ItemEditViewModel
+import com.homebox.lens.ui.screen.labelslist.labelsListScreen
+import com.homebox.lens.ui.screen.labelslist.LabelsListViewModel
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen
+import com.homebox.lens.ui.screen.search.SearchViewModel
import com.homebox.lens.ui.screen.setup.SetupScreen
+import timber.log.Timber
+// [END_IMPORTS]
-// [CORE-LOGIC]
+// [CONTRACT]
+// [ENTITY: Function('NavGraph')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('rememberNavController')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('currentBackStackEntryAsState')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('remember')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('hiltViewModel')]
+// [RELATION: Function('NavGraph') -> [CREATES_INSTANCE_OF] -> Class('NavigationActions')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('NavHost')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('composable')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SetupScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('DashboardScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('InventoryListScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemDetailsScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemEditScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LabelsListScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationsListScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationEditScreen')]
+// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SearchScreen')]
/**
* [CONTRACT]
* Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
@@ -33,24 +60,23 @@ import com.homebox.lens.ui.screen.setup.SetupScreen
* @invariant Стартовый экран - `Screen.Setup`.
*/
@Composable
-fun NavGraph(
- navController: NavHostController = rememberNavController()
-) {
+fun NavGraph(navController: NavHostController = rememberNavController()) {
// [STATE]
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
// [HELPER]
- val navigationActions = remember(navController) {
- NavigationActions(navController)
- }
+ val navigationActions =
+ remember(navController) {
+ NavigationActions(navController)
+ }
// [ACTION]
NavHost(
navController = navController,
- startDestination = Screen.Setup.route
+ startDestination = Screen.Setup.route,
) {
- // [COMPOSABLE_SETUP]
+ // [ENTITY: Composable('Screen.Setup.route')]
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
@@ -58,39 +84,75 @@ fun NavGraph(
}
})
}
- // [COMPOSABLE_DASHBOARD]
+ // [END_ENTITY: Composable('Screen.Setup.route')]
+ // [ENTITY: Composable('Screen.Dashboard.route')]
composable(route = Screen.Dashboard.route) {
DashboardScreen(
currentRoute = currentRoute,
- navigationActions = navigationActions
+ navigationActions = navigationActions,
)
}
- // [COMPOSABLE_INVENTORY_LIST]
- composable(route = Screen.InventoryList.route) {
+ // [END_ENTITY: Composable('Screen.Dashboard.route')]
+ // [ENTITY: Composable('Screen.InventoryList.route')]
+ composable(route = Screen.InventoryList.route) { backStackEntry ->
+ val viewModel: InventoryListViewModel = hiltViewModel(backStackEntry)
InventoryListScreen(
- currentRoute = currentRoute,
- navigationActions = navigationActions
+ onItemClick = { item ->
+ // TODO: Navigate to item details
+ Timber.i("[UI] Item clicked: ${item.name}")
+ },
+ onNavigateBack = {
+ navController.popBackStack()
+ }
)
}
- // [COMPOSABLE_ITEM_DETAILS]
- composable(route = Screen.ItemDetails.route) {
+ // [END_ENTITY: Composable('Screen.InventoryList.route')]
+ // [ENTITY: Composable('Screen.ItemDetails.route')]
+ composable(route = Screen.ItemDetails.route) { backStackEntry ->
+ val viewModel: ItemDetailsViewModel = hiltViewModel(backStackEntry)
ItemDetailsScreen(
- currentRoute = currentRoute,
- navigationActions = navigationActions
+ onNavigateBack = {
+ navController.popBackStack()
+ },
+ onEditClick = { itemId ->
+ // TODO: Navigate to item edit screen
+ Timber.i("[UI] Edit item clicked: $itemId")
+ }
)
}
- // [COMPOSABLE_ITEM_EDIT]
- composable(route = Screen.ItemEdit.route) {
+ // [END_ENTITY: Composable('Screen.ItemDetails.route')]
+ // [ENTITY: Composable('Screen.ItemEdit.route')]
+ composable(route = Screen.ItemEdit.route) { backStackEntry ->
+ val viewModel: ItemEditViewModel = hiltViewModel(backStackEntry)
ItemEditScreen(
- currentRoute = currentRoute,
- navigationActions = navigationActions
+ onNavigateBack = {
+ navController.popBackStack()
+ }
)
}
- // [COMPOSABLE_LABELS_LIST]
- composable(Screen.LabelsList.route) {
- LabelsListScreen(navController = navController)
+ // [END_ENTITY: Composable('Screen.ItemEdit.route')]
+ // [ENTITY: Composable('Screen.LabelsList.route')]
+ composable(Screen.LabelsList.route) { backStackEntry ->
+ val viewModel: LabelsListViewModel = hiltViewModel(backStackEntry)
+ val uiState by viewModel.uiState.collectAsState()
+
+ labelsListScreen(
+ uiState = uiState,
+ onLabelClick = { label ->
+ // TODO: Implement navigation to label details screen
+ Timber.i("[UI] Label clicked: ${label.name}")
+ },
+ onAddClick = {
+ // TODO: Implement navigation to add new label screen
+ Timber.i("[UI] Add new label clicked")
+ },
+ onNavigateBack = {
+ navController.popBackStack()
+ }
+ )
}
- // [COMPOSABLE_LOCATIONS_LIST]
+ // [END_ENTITY: Composable('Screen.LabelsList.route')]
+ // [ENTITY: Composable('Screen.LocationsList.route')]
composable(route = Screen.LocationsList.route) {
LocationsListScreen(
currentRoute = currentRoute,
@@ -101,24 +163,34 @@ fun NavGraph(
},
onAddNewLocationClick = {
navController.navigate(Screen.LocationEdit.createRoute("new"))
- }
+ },
)
}
- // [COMPOSABLE_LOCATION_EDIT]
+ // [END_ENTITY: Composable('Screen.LocationsList.route')]
+ // [ENTITY: Composable('Screen.LocationEdit.route')]
composable(route = Screen.LocationEdit.route) { backStackEntry ->
val locationId = backStackEntry.arguments?.getString("locationId")
LocationEditScreen(
- locationId = locationId
+ locationId = locationId,
)
}
- // [COMPOSABLE_SEARCH]
- composable(route = Screen.Search.route) {
+ // [END_ENTITY: Composable('Screen.LocationEdit.route')]
+ // [ENTITY: Composable('Screen.Search.route')]
+ composable(route = Screen.Search.route) { backStackEntry ->
+ val viewModel: SearchViewModel = hiltViewModel(backStackEntry)
SearchScreen(
- currentRoute = currentRoute,
- navigationActions = navigationActions
+ onNavigateBack = {
+ navController.popBackStack()
+ },
+ onItemClick = { item ->
+ // TODO: Navigate to item details
+ Timber.i("[UI] Search result item clicked: ${item.name}")
+ }
)
}
+ // [END_ENTITY: Composable('Screen.Search.route')]
}
- // [END_FUNCTION_NavGraph]
}
-// [END_FILE_NavGraph.kt]
+// [END_ENTITY: Function('NavGraph')]
+// [END_CONTRACT]
+// [END_FILE_NavGraph.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt
index 3d4db3a..3ffe4a8 100644
--- a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt
+++ b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt
@@ -2,70 +2,122 @@
// [FILE] NavigationActions.kt
// [SEMANTICS] navigation, controller, actions
package com.homebox.lens.navigation
+
+// [IMPORTS]
import androidx.navigation.NavHostController
-// [CORE-LOGIC]
+// [END_IMPORTS]
+
+// [CONTRACT]
+// [ENTITY: Class('NavigationActions')]
+// [RELATION: Class('NavigationActions') -> [DEPENDS_ON] -> Class('NavHostController')]
/**
-[CONTRACT]
-@summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
-@param navController Контроллер Jetpack Navigation.
-@invariant Все навигационные действия должны использовать предоставленный navController.
+ * [CONTRACT]
+ * @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
+ * @param navController Контроллер Jetpack Navigation.
+ * @invariant Все навигационные действия должны использовать предоставленный navController.
*/
class NavigationActions(private val navController: NavHostController) {
-// [ACTION]
+ // [ENTITY: Function('navigateToDashboard')]
+ // [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('navController.navigate')]
+ // [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('Screen.Dashboard.route')]
+ // [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('popUpTo')]
+ // [ACTION]
/**
- [CONTRACT]
- @summary Навигация на главный экран.
- @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
+ * [CONTRACT]
+ * @summary Навигация на главный экран.
+ * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
*/
fun navigateToDashboard() {
navController.navigate(Screen.Dashboard.route) {
-// Используем popUpTo для удаления всех экранов до dashboard из back stack
-// Это предотвращает создание большой стопки экранов при навигации через drawer
+ // Используем popUpTo для удаления всех экранов до dashboard из back stack
+ // Это предотвращает создание большой стопки экранов при навигации через drawer
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
}
+ // [END_ENTITY: Function('navigateToDashboard')]
+
+ // [ENTITY: Function('navigateToLocations')]
+ // [RELATION: Function('navigateToLocations') -> [CALLS] -> Function('navController.navigate')]
+ // [RELATION: Function('navigateToLocations') -> [CALLS] -> Function('Screen.LocationsList.route')]
// [ACTION]
fun navigateToLocations() {
navController.navigate(Screen.LocationsList.route) {
launchSingleTop = true
}
}
+ // [END_ENTITY: Function('navigateToLocations')]
+
+ // [ENTITY: Function('navigateToLabels')]
+ // [RELATION: Function('navigateToLabels') -> [CALLS] -> Function('navController.navigate')]
+ // [RELATION: Function('navigateToLabels') -> [CALLS] -> Function('Screen.LabelsList.route')]
// [ACTION]
fun navigateToLabels() {
navController.navigate(Screen.LabelsList.route) {
launchSingleTop = true
}
}
+ // [END_ENTITY: Function('navigateToLabels')]
+
+ // [ENTITY: Function('navigateToSearch')]
+ // [RELATION: Function('navigateToSearch') -> [CALLS] -> Function('navController.navigate')]
+ // [RELATION: Function('navigateToSearch') -> [CALLS] -> Function('Screen.Search.route')]
// [ACTION]
fun navigateToSearch() {
navController.navigate(Screen.Search.route) {
launchSingleTop = true
}
}
+ // [END_ENTITY: Function('navigateToSearch')]
+
+ // [ENTITY: Function('navigateToInventoryListWithLabel')]
+ // [RELATION: Function('navigateToInventoryListWithLabel') -> [CALLS] -> Function('Screen.InventoryList.withFilter')]
+ // [RELATION: Function('navigateToInventoryListWithLabel') -> [CALLS] -> Function('navController.navigate')]
// [ACTION]
fun navigateToInventoryListWithLabel(labelId: String) {
val route = Screen.InventoryList.withFilter("label", labelId)
navController.navigate(route)
}
+ // [END_ENTITY: Function('navigateToInventoryListWithLabel')]
+
+ // [ENTITY: Function('navigateToInventoryListWithLocation')]
+ // [RELATION: Function('navigateToInventoryListWithLocation') -> [CALLS] -> Function('Screen.InventoryList.withFilter')]
+ // [RELATION: Function('navigateToInventoryListWithLocation') -> [CALLS] -> Function('navController.navigate')]
// [ACTION]
fun navigateToInventoryListWithLocation(locationId: String) {
val route = Screen.InventoryList.withFilter("location", locationId)
navController.navigate(route)
}
+ // [END_ENTITY: Function('navigateToInventoryListWithLocation')]
+
+ // [ENTITY: Function('navigateToCreateItem')]
+ // [RELATION: Function('navigateToCreateItem') -> [CALLS] -> Function('Screen.ItemEdit.createRoute')]
+ // [RELATION: Function('navigateToCreateItem') -> [CALLS] -> Function('navController.navigate')]
// [ACTION]
fun navigateToCreateItem() {
navController.navigate(Screen.ItemEdit.createRoute("new"))
}
+ // [END_ENTITY: Function('navigateToCreateItem')]
+
+ // [ENTITY: Function('navigateToLogout')]
+ // [RELATION: Function('navigateToLogout') -> [CALLS] -> Function('navController.navigate')]
+ // [RELATION: Function('navigateToLogout') -> [CALLS] -> Function('popUpTo')]
// [ACTION]
fun navigateToLogout() {
navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
}
}
+ // [END_ENTITY: Function('navigateToLogout')]
+
+ // [ENTITY: Function('navigateBack')]
+ // [RELATION: Function('navigateBack') -> [CALLS] -> Function('navController.popBackStack')]
// [ACTION]
fun navigateBack() {
navController.popBackStack()
}
+ // [END_ENTITY: Function('navigateBack')]
}
+// [END_ENTITY: Class('NavigationActions')]
+// [END_CONTRACT]
// [END_FILE_NavigationActions.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/navigation/Screen.kt b/app/src/main/java/com/homebox/lens/navigation/Screen.kt
index 6014abb..08e11a0 100644
--- a/app/src/main/java/com/homebox/lens/navigation/Screen.kt
+++ b/app/src/main/java/com/homebox/lens/navigation/Screen.kt
@@ -3,7 +3,11 @@
// [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation
-// [CORE-LOGIC]
+// [IMPORTS]
+// [END_IMPORTS]
+
+// [CONTRACT]
+// [ENTITY: SealedClass('Screen')]
/**
* [CONTRACT]
* Запечатанный класс для определения маршрутов навигации в приложении.
@@ -11,10 +15,17 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
- // [STATE]
+ // [ENTITY: DataObject('Setup')]
data object Setup : Screen("setup_screen")
+ // [END_ENTITY: DataObject('Setup')]
+
+ // [ENTITY: DataObject('Dashboard')]
data object Dashboard : Screen("dashboard_screen")
+ // [END_ENTITY: DataObject('Dashboard')]
+
+ // [ENTITY: DataObject('InventoryList')]
data object InventoryList : Screen("inventory_list_screen") {
+ // [ENTITY: Function('withFilter')]
/**
* [CONTRACT]
* Создает маршрут для экрана списка инвентаря с параметром фильтра.
@@ -24,8 +35,10 @@ sealed class Screen(val route: String) {
* @throws IllegalArgumentException если ключ или значение пустые.
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
*/
- // [HELPER]
- fun withFilter(key: String, value: String): String {
+ fun withFilter(
+ key: String,
+ value: String,
+ ): String {
// [PRECONDITION]
require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." }
require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." }
@@ -35,9 +48,13 @@ sealed class Screen(val route: String) {
check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." }
return constructedRoute
}
+ // [END_ENTITY: Function('withFilter')]
}
+ // [END_ENTITY: DataObject('InventoryList')]
+ // [ENTITY: DataObject('ItemDetails')]
data object ItemDetails : Screen("item_details_screen/{itemId}") {
+ // [ENTITY: Function('createRoute')]
/**
* [CONTRACT]
* Создает маршрут для экрана деталей элемента с указанным ID.
@@ -45,7 +62,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой.
*/
- // [HELPER]
fun createRoute(itemId: String): String {
// [PRECONDITION]
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
@@ -55,8 +71,13 @@ sealed class Screen(val route: String) {
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
return route
}
+ // [END_ENTITY: Function('createRoute')]
}
+ // [END_ENTITY: DataObject('ItemDetails')]
+
+ // [ENTITY: DataObject('ItemEdit')]
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
+ // [ENTITY: Function('createRoute')]
/**
* [CONTRACT]
* Создает маршрут для экрана редактирования элемента с указанным ID.
@@ -64,7 +85,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой.
*/
- // [HELPER]
fun createRoute(itemId: String): String {
// [PRECONDITION]
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
@@ -74,10 +94,21 @@ sealed class Screen(val route: String) {
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
return route
}
+ // [END_ENTITY: Function('createRoute')]
}
+ // [END_ENTITY: DataObject('ItemEdit')]
+
+ // [ENTITY: DataObject('LabelsList')]
data object LabelsList : Screen("labels_list_screen")
+ // [END_ENTITY: DataObject('LabelsList')]
+
+ // [ENTITY: DataObject('LocationsList')]
data object LocationsList : Screen("locations_list_screen")
+ // [END_ENTITY: DataObject('LocationsList')]
+
+ // [ENTITY: DataObject('LocationEdit')]
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
+ // [ENTITY: Function('createRoute')]
/**
* [CONTRACT]
* Создает маршрут для экрана редактирования местоположения с указанным ID.
@@ -85,7 +116,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута.
* @throws IllegalArgumentException если locationId пустой.
*/
- // [HELPER]
fun createRoute(locationId: String): String {
// [PRECONDITION]
require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." }
@@ -95,7 +125,14 @@ sealed class Screen(val route: String) {
check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." }
return route
}
+ // [END_ENTITY: Function('createRoute')]
}
+ // [END_ENTITY: DataObject('LocationEdit')]
+
+ // [ENTITY: DataObject('Search')]
data object Search : Screen("search_screen")
+ // [END_ENTITY: DataObject('Search')]
}
+// [END_ENTITY: SealedClass('Screen')]
+// [END_CONTRACT]
// [END_FILE_Screen.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
index 176d749..7b95ef9 100644
--- a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
+++ b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt
@@ -1,6 +1,9 @@
// [PACKAGE] com.homebox.lens.ui.common
// [FILE] AppDrawer.kt
+// [SEMANTICS] ui, common, navigation_drawer
package com.homebox.lens.ui.common
+
+// [IMPORTS]
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -22,18 +25,37 @@ import androidx.compose.ui.unit.dp
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.navigation.Screen
+// [END_IMPORTS]
+
+// [CONTRACT]
+// [ENTITY: Function('AppDrawerContent')]
+// [RELATION: Function('AppDrawerContent') -> [DEPENDS_ON] -> Class('NavigationActions')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('ModalDrawerSheet')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Spacer')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Button')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Icon')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Divider')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('NavigationDrawerItem')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Dashboard.route')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.LocationsList.route')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.LabelsList.route')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Search.route')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.ItemEdit.createRoute')]
+// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Setup.route')]
/**
-[CONTRACT]
-@summary Контент для бокового навигационного меню (Drawer).
-@param currentRoute Текущий маршрут для подсветки активного элемента.
-@param navigationActions Объект с навигационными действиями.
-@param onCloseDrawer Лямбда для закрытия бокового меню.
+ * [CONTRACT]
+ * @summary Контент для бокового навигационного меню (Drawer).
+ * @param currentRoute Текущий маршрут для подсветки активного элемента.
+ * @param navigationActions Объект с навигационными действиями.
+ * @param onCloseDrawer Лямбда для закрытия бокового меню.
*/
@Composable
internal fun AppDrawerContent(
currentRoute: String?,
navigationActions: NavigationActions,
- onCloseDrawer: () -> Unit
+ onCloseDrawer: () -> Unit,
) {
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
@@ -42,9 +64,10 @@ internal fun AppDrawerContent(
navigationActions.navigateToCreateItem()
onCloseDrawer()
},
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 16.dp)
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
@@ -58,7 +81,7 @@ internal fun AppDrawerContent(
onClick = {
navigationActions.navigateToDashboard()
onCloseDrawer()
- }
+ },
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_locations)) },
@@ -66,7 +89,7 @@ internal fun AppDrawerContent(
onClick = {
navigationActions.navigateToLocations()
onCloseDrawer()
- }
+ },
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_labels)) },
@@ -74,7 +97,7 @@ internal fun AppDrawerContent(
onClick = {
navigationActions.navigateToLabels()
onCloseDrawer()
- }
+ },
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.search)) },
@@ -82,7 +105,7 @@ internal fun AppDrawerContent(
onClick = {
navigationActions.navigateToSearch()
onCloseDrawer()
- }
+ },
)
// TODO: Add Profile and Tools items
Divider()
@@ -92,7 +115,10 @@ internal fun AppDrawerContent(
onClick = {
navigationActions.navigateToLogout()
onCloseDrawer()
- }
+ },
)
}
-}
\ No newline at end of file
+}
+// [END_ENTITY: Function('AppDrawerContent')]
+// [END_CONTRACT]
+// [END_FILE_AppDrawer.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt
index b366974..d4a98bb 100644
--- a/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt
+++ b/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt
@@ -15,8 +15,21 @@ import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import kotlinx.coroutines.launch
+// [END_IMPORTS]
-// [UI_COMPONENT]
+// [CONTRACT]
+// [ENTITY: Function('MainScaffold')]
+// [RELATION: Function('MainScaffold') -> [DEPENDS_ON] -> Class('NavigationActions')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('rememberDrawerState')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('rememberCoroutineScope')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('ModalNavigationDrawer')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('AppDrawerContent')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Scaffold')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('TopAppBar')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('IconButton')]
+// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Icon')]
/**
* [CONTRACT]
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
@@ -35,7 +48,7 @@ fun MainScaffold(
currentRoute: String?,
navigationActions: NavigationActions,
topBarActions: @Composable () -> Unit = {},
- content: @Composable (PaddingValues) -> Unit
+ content: @Composable (PaddingValues) -> Unit,
) {
// [STATE]
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
@@ -48,9 +61,9 @@ fun MainScaffold(
AppDrawerContent(
currentRoute = currentRoute,
navigationActions = navigationActions,
- onCloseDrawer = { scope.launch { drawerState.close() } }
+ onCloseDrawer = { scope.launch { drawerState.close() } },
)
- }
+ },
) {
Scaffold(
topBar = {
@@ -60,18 +73,19 @@ fun MainScaffold(
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(
Icons.Default.Menu,
- contentDescription = stringResource(id = R.string.cd_open_navigation_drawer)
+ contentDescription = stringResource(id = R.string.cd_open_navigation_drawer),
)
}
},
- actions = { topBarActions() }
+ actions = { topBarActions() },
)
- }
+ },
) { paddingValues ->
// [ACTION]
content(paddingValues)
}
}
- // [END_FUNCTION_MainScaffold]
}
-// [END_FILE_MainScaffold.kt]
+// [END_ENTITY: Function('MainScaffold')]
+// [END_CONTRACT]
+// [END_FILE_MainScaffold.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
index 775cd5c..a0ca82f 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
@@ -2,6 +2,7 @@
// [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose, navigation
package com.homebox.lens.ui.screen.dashboard
+
// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -29,24 +30,36 @@ import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme
import timber.log.Timber
-// [ENTRYPOINT]
+// [END_IMPORTS]
+
+// [CONTRACT]
+// [ENTITY: Function('DashboardScreen')]
+// [RELATION: Function('DashboardScreen') -> [DEPENDS_ON] -> Class('DashboardViewModel')]
+// [RELATION: Function('DashboardScreen') -> [DEPENDS_ON] -> Class('NavigationActions')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('hiltViewModel')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('collectAsState')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('MainScaffold')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('IconButton')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('Icon')]
+// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('DashboardContent')]
/**
-[CONTRACT]
-@summary Главная Composable-функция для экрана "Панель управления".
-@param viewModel ViewModel для этого экрана, предоставляется через Hilt.
-@param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
-@param navigationActions Объект с навигационными действиями.
-@sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
+ * [CONTRACT]
+ * @summary Главная Composable-функция для экрана "Панель управления".
+ * @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
+ * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
+ * @param navigationActions Объект с навигационными действиями.
+ * @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
*/
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel(),
currentRoute: String?,
- navigationActions: NavigationActions
+ navigationActions: NavigationActions,
) {
-// [STATE]
+ // [STATE]
val uiState by viewModel.uiState.collectAsState()
-// [UI_COMPONENT]
+ // [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.dashboard_title),
currentRoute = currentRoute,
@@ -55,10 +68,10 @@ fun DashboardScreen(
IconButton(onClick = { navigationActions.navigateToSearch() }) {
Icon(
Icons.Default.Search,
- contentDescription = stringResource(id = R.string.cd_scan_qr_code) // TODO: Rename string resource
+ contentDescription = stringResource(id = R.string.cd_scan_qr_code), // TODO: Rename string resource
)
}
- }
+ },
) { paddingValues ->
DashboardContent(
modifier = Modifier.padding(paddingValues),
@@ -70,28 +83,40 @@ fun DashboardScreen(
onLabelClick = { label ->
Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...")
navigationActions.navigateToInventoryListWithLabel(label.id)
- }
+ },
)
}
-// [END_FUNCTION_DashboardScreen]
}
-// [HELPER]
+// [END_ENTITY: Function('DashboardScreen')]
+
+// [ENTITY: Function('DashboardContent')]
+// [RELATION: Function('DashboardContent') -> [DEPENDS_ON] -> SealedInterface('DashboardUiState')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Box')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('CircularProgressIndicator')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LazyColumn')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Spacer')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('StatisticsSection')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('RecentlyAddedSection')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LocationsSection')]
+// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LabelsSection')]
/**
-[CONTRACT]
-@summary Отображает основной контент экрана в зависимости от uiState.
-@param modifier Модификатор для стилизации.
-@param uiState Текущее состояние UI экрана.
-@param onLocationClick Лямбда-обработчик нажатия на местоположение.
-@param onLabelClick Лямбда-обработчик нажатия на метку.
+ * [CONTRACT]
+ * @summary Отображает основной контент экрана в зависимости от uiState.
+ * @param modifier Модификатор для стилизации.
+ * @param uiState Текущее состояние UI экрана.
+ * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
+ * @param onLabelClick Лямбда-обработчик нажатия на метку.
*/
@Composable
private fun DashboardContent(
modifier: Modifier = Modifier,
uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit,
- onLabelClick: (LabelOut) -> Unit
+ onLabelClick: (LabelOut) -> Unit,
) {
-// [CORE-LOGIC]
+ // [CORE-LOGIC]
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -103,16 +128,17 @@ private fun DashboardContent(
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
- textAlign = TextAlign.Center
+ textAlign = TextAlign.Center,
)
}
}
is DashboardUiState.Success -> {
LazyColumn(
- modifier = modifier
- .fillMaxSize()
- .padding(horizontal = 16.dp),
- verticalArrangement = Arrangement.spacedBy(24.dp)
+ modifier =
+ modifier
+ .fillMaxSize()
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(24.dp),
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
item { StatisticsSection(statistics = uiState.statistics) }
@@ -123,74 +149,124 @@ private fun DashboardContent(
}
}
}
-// [END_FUNCTION_DashboardContent]
}
-// [UI_COMPONENT]
+// [END_ENTITY: Function('DashboardContent')]
+
+// [ENTITY: Function('StatisticsSection')]
+// [RELATION: Function('StatisticsSection') -> [DEPENDS_ON] -> Class('GroupStatistics')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Card')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('LazyVerticalGrid')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('GridCells.Fixed')]
+// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('StatisticCard')]
/**
-[CONTRACT]
-@summary Секция для отображения общей статистики.
-@param statistics Объект со статистическими данными.
+ * [CONTRACT]
+ * @summary Секция для отображения общей статистики.
+ * @param statistics Объект со статистическими данными.
*/
@Composable
private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_quick_stats),
- style = MaterialTheme.typography.titleMedium
+ style = MaterialTheme.typography.titleMedium,
)
Card {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
- modifier = Modifier
- .height(120.dp)
- .fillMaxWidth()
- .padding(16.dp),
+ modifier =
+ Modifier
+ .height(120.dp)
+ .fillMaxWidth()
+ .padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
- verticalArrangement = Arrangement.spacedBy(16.dp)
+ verticalArrangement = Arrangement.spacedBy(16.dp),
) {
- item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) }
- item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
- item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
- item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
+ item {
+ StatisticCard(
+ title = stringResource(id = R.string.dashboard_stat_total_items),
+ value = statistics.items.toString(),
+ )
+ }
+ item {
+ StatisticCard(
+ title = stringResource(id = R.string.dashboard_stat_total_value),
+ value = statistics.totalValue.toString(),
+ )
+ }
+ item {
+ StatisticCard(
+ title = stringResource(id = R.string.dashboard_stat_total_labels),
+ value = statistics.labels.toString(),
+ )
+ }
+ item {
+ StatisticCard(
+ title = stringResource(id = R.string.dashboard_stat_total_locations),
+ value = statistics.locations.toString(),
+ )
+ }
}
}
}
}
-// [UI_COMPONENT]
+// [END_ENTITY: Function('StatisticsSection')]
+
+// [ENTITY: Function('StatisticCard')]
+// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.labelMedium')]
+// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.headlineSmall')]
/**
-[CONTRACT]
-@summary Карточка для отображения одного статистического показателя.
-@param title Название показателя.
-@param value Значение показателя.
+ * [CONTRACT]
+ * @summary Карточка для отображения одного статистического показателя.
+ * @param title Название показателя.
+ * @param value Значение показателя.
*/
@Composable
-private fun StatisticCard(title: String, value: String) {
+private fun StatisticCard(
+ title: String,
+ value: String,
+) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
}
}
-// [UI_COMPONENT]
+// [END_ENTITY: Function('StatisticCard')]
+
+// [ENTITY: Function('RecentlyAddedSection')]
+// [RELATION: Function('RecentlyAddedSection') -> [DEPENDS_ON] -> Class('ItemSummary')]
+// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
+// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('LazyRow')]
+// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('ItemCard')]
/**
-[CONTRACT]
-@summary Секция для отображения недавно добавленных элементов.
-@param items Список элементов для отображения.
+ * [CONTRACT]
+ * @summary Секция для отображения недавно добавленных элементов.
+ * @param items Список элементов для отображения.
*/
@Composable
private fun RecentlyAddedSection(items: List) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_recently_added),
- style = MaterialTheme.typography.titleMedium
+ style = MaterialTheme.typography.titleMedium,
)
if (items.isEmpty()) {
Text(
text = stringResource(id = R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 16.dp),
- textAlign = TextAlign.Center
+ modifier =
+ Modifier
+ .fillMaxWidth()
+ .padding(vertical = 16.dp),
+ textAlign = TextAlign.Center,
)
} else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -201,41 +277,70 @@ private fun RecentlyAddedSection(items: List) {
}
}
}
-// [UI_COMPONENT]
+// [END_ENTITY: Function('RecentlyAddedSection')]
+
+// [ENTITY: Function('ItemCard')]
+// [RELATION: Function('ItemCard') -> [DEPENDS_ON] -> Class('ItemSummary')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Card')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Spacer')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('MaterialTheme.typography.titleSmall')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('MaterialTheme.typography.bodySmall')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('stringResource')]
/**
-[CONTRACT]
-@summary Карточка для отображения краткой информации об элементе.
-@param item Элемент для отображения.
+ * [CONTRACT]
+ * @summary Карточка для отображения краткой информации об элементе.
+ * @param item Элемент для отображения.
*/
@Composable
private fun ItemCard(item: ItemSummary) {
Card(modifier = Modifier.width(150.dp)) {
Column(modifier = Modifier.padding(8.dp)) {
// TODO: Add image here from item.image
- Spacer(modifier = Modifier
- .height(80.dp)
- .fillMaxWidth()
- .background(MaterialTheme.colorScheme.secondaryContainer))
+ Spacer(
+ modifier =
+ Modifier
+ .height(80.dp)
+ .fillMaxWidth()
+ .background(MaterialTheme.colorScheme.secondaryContainer),
+ )
Spacer(modifier = Modifier.height(8.dp))
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
- Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
+ Text(
+ text = item.location?.name ?: stringResource(id = R.string.no_location),
+ style = MaterialTheme.typography.bodySmall,
+ maxLines = 1,
+ )
}
}
}
-// [UI_COMPONENT]
+// [END_ENTITY: Function('ItemCard')]
+
+// [ENTITY: Function('LocationsSection')]
+// [RELATION: Function('LocationsSection') -> [DEPENDS_ON] -> Class('LocationOutCount')]
+// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
+// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('FlowRow')]
+// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('SuggestionChip')]
/**
-[CONTRACT]
-@summary Секция для отображения местоположений в виде чипсов.
-@param locations Список местоположений.
-@param onLocationClick Лямбда-обработчик нажатия на местоположение.
+ * [CONTRACT]
+ * @summary Секция для отображения местоположений в виде чипсов.
+ * @param locations Список местоположений.
+ * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
-private fun LocationsSection(locations: List, onLocationClick: (LocationOutCount) -> Unit) {
+private fun LocationsSection(
+ locations: List,
+ onLocationClick: (LocationOutCount) -> Unit,
+) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_locations),
- style = MaterialTheme.typography.titleMedium
+ style = MaterialTheme.typography.titleMedium,
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -243,26 +348,38 @@ private fun LocationsSection(locations: List, onLocationClick:
locations.forEach { location ->
SuggestionChip(
onClick = { onLocationClick(location) },
- label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }
+ label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) },
)
}
}
}
}
-// [UI_COMPONENT]
+// [END_ENTITY: Function('LocationsSection')]
+
+// [ENTITY: Function('LabelsSection')]
+// [RELATION: Function('LabelsSection') -> [DEPENDS_ON] -> Class('LabelOut')]
+// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
+// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('FlowRow')]
+// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('SuggestionChip')]
/**
-[CONTRACT]
-@summary Секция для отображения меток в виде чипсов.
-@param labels Список меток.
-@param onLabelClick Лямбда-обработчик нажатия на метку.
+ * [CONTRACT]
+ * @summary Секция для отображения меток в виде чипсов.
+ * @param labels Список меток.
+ * @param onLabelClick Лямбда-обработчик нажатия на метку.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
-private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Unit) {
+private fun LabelsSection(
+ labels: List,
+ onLabelClick: (LabelOut) -> Unit,
+) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_labels),
- style = MaterialTheme.typography.titleMedium
+ style = MaterialTheme.typography.titleMedium,
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -270,46 +387,105 @@ private fun LabelsSection(labels: List, onLabelClick: (LabelOut) -> Un
labels.forEach { label ->
SuggestionChip(
onClick = { onLabelClick(label) },
- label = { Text(label.name) }
+ label = { Text(label.name) },
)
}
}
}
}
+// [END_ENTITY: Function('LabelsSection')]
+
+// [ENTITY: Function('DashboardContentSuccessPreview')]
+// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardUiState.Success')]
+// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('GroupStatistics')]
+// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LocationOutCount')]
+// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LabelOut')]
+// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
+// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardContent')]
// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Success State")
@Composable
fun DashboardContentSuccessPreview() {
- val previewState = DashboardUiState.Success(
- statistics = GroupStatistics(
- items = 123,
- totalValue = 9999.99,
- locations = 5,
- labels = 8
- ),
- locations = listOf(
- LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""),
- LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""),
- LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""),
- LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""),
- LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
- ),
- labels = listOf(
- LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
- LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
- LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
- LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
- ),
- recentlyAddedItems = emptyList()
- )
+ val previewState =
+ DashboardUiState.Success(
+ statistics =
+ GroupStatistics(
+ items = 123,
+ totalValue = 9999.99,
+ locations = 5,
+ labels = 8,
+ ),
+ locations =
+ listOf(
+ LocationOutCount(
+ id = "1",
+ name = "Office",
+ color = "#FF0000",
+ isArchived = false,
+ itemCount = 10,
+ createdAt = "",
+ updatedAt = "",
+ ),
+ LocationOutCount(
+ id = "2",
+ name = "Garage",
+ color = "#00FF00",
+ isArchived = false,
+ itemCount = 5,
+ createdAt = "",
+ updatedAt = "",
+ ),
+ LocationOutCount(
+ id = "3",
+ name = "Living Room",
+ color = "#0000FF",
+ isArchived = false,
+ itemCount = 15,
+ createdAt = "",
+ updatedAt = "",
+ ),
+ LocationOutCount(
+ id = "4",
+ name = "Kitchen",
+ color = "#FFFF00",
+ isArchived = false,
+ itemCount = 20,
+ createdAt = "",
+ updatedAt = "",
+ ),
+ LocationOutCount(
+ id = "5",
+ name = "Basement",
+ color = "#00FFFF",
+ isArchived = false,
+ itemCount = 3,
+ createdAt = "",
+ updatedAt = "",
+ ),
+ ),
+ labels =
+ listOf(
+ LabelOut(id = "1", name = "electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
+ LabelOut(id = "2", name = "important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
+ LabelOut(id = "3", name = "seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
+ LabelOut(id = "4", name = "hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = ""),
+ ),
+ recentlyAddedItems = emptyList(),
+ )
HomeboxLensTheme {
DashboardContent(
uiState = previewState,
onLocationClick = {},
- onLabelClick = {}
+ onLabelClick = {},
)
}
}
+// [END_ENTITY: Function('DashboardContentSuccessPreview')]
+
+// [ENTITY: Function('DashboardContentLoadingPreview')]
+// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
+// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardContent')]
+// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardUiState.Loading')]
// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
@@ -318,10 +494,17 @@ fun DashboardContentLoadingPreview() {
DashboardContent(
uiState = DashboardUiState.Loading,
onLocationClick = {},
- onLabelClick = {}
+ onLabelClick = {},
)
}
}
+// [END_ENTITY: Function('DashboardContentLoadingPreview')]
+
+// [ENTITY: Function('DashboardContentErrorPreview')]
+// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
+// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardContent')]
+// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardUiState.Error')]
+// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('stringResource')]
// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
@@ -330,8 +513,10 @@ fun DashboardContentErrorPreview() {
DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
onLocationClick = {},
- onLabelClick = {}
+ onLabelClick = {},
)
}
}
+// [END_ENTITY: Function('DashboardContentErrorPreview')]
+// [END_CONTRACT]
// [END_FILE_DashboardScreen.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
index a4fe49e..69effeb 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
@@ -1,15 +1,17 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
-// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
+// [FILE] DashboardUiState.kt
// [SEMANTICS] ui, state, dashboard
-// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard
+// [IMPORTS]
import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount
+import com.homebox.lens.domain.model.ItemSummary
+// [END_IMPORTS]
-// [CORE-LOGIC]
+// [CONTRACT]
// [ENTITY: SealedInterface('DashboardUiState')]
/**
* [CONTRACT]
@@ -17,6 +19,11 @@ import com.homebox.lens.domain.model.LocationOutCount
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/
sealed interface DashboardUiState {
+ // [ENTITY: DataClass('Success')]
+ // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('GroupStatistics')]
+ // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LocationOutCount')]
+ // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LabelOut')]
+ // [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('ItemSummary')]
/**
* [CONTRACT]
* Состояние успешной загрузки данных.
@@ -29,20 +36,27 @@ sealed interface DashboardUiState {
val statistics: GroupStatistics,
val locations: List,
val labels: List,
- val recentlyAddedItems: List
+ val recentlyAddedItems: List,
) : DashboardUiState
+ // [END_ENTITY: DataClass('Success')]
+ // [ENTITY: DataClass('Error')]
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : DashboardUiState
+ // [END_ENTITY: DataClass('Error')]
+ // [ENTITY: DataObject('Loading')]
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
- data object Loading : DashboardUiState
+ object Loading : DashboardUiState
+ // [END_ENTITY: DataObject('Loading')]
}
+// [END_ENTITY: SealedInterface('DashboardUiState')]
+// [END_CONTRACT]
// [END_FILE_DashboardUiState.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
index 2dce373..946179c 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
@@ -2,6 +2,7 @@
// [FILE] DashboardViewModel.kt
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
package com.homebox.lens.ui.screen.dashboard
+
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -9,17 +10,21 @@ import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
-import com.homebox.lens.ui.screen.dashboard.DashboardUiState
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
+// [END_IMPORTS]
-// [VIEWMODEL]
+// [CONTRACT]
// [ENTITY: ViewModel('DashboardViewModel')]
+// [RELATION: ViewModel('DashboardViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
+// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
+// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetStatisticsUseCase')]
+// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetAllLocationsUseCase')]
+// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetAllLabelsUseCase')]
+// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetRecentlyAddedItemsUseCase')]
/**
* [CONTRACT]
* @summary ViewModel для главного экрана (Dashboard).
@@ -28,61 +33,78 @@ import javax.inject.Inject
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/
@HiltViewModel
-class DashboardViewModel @Inject constructor(
- private val getStatisticsUseCase: GetStatisticsUseCase,
- private val getAllLocationsUseCase: GetAllLocationsUseCase,
- private val getAllLabelsUseCase: GetAllLabelsUseCase,
- private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
-) : ViewModel() {
+class DashboardViewModel
+ @Inject
+ constructor(
+ private val getStatisticsUseCase: GetStatisticsUseCase,
+ private val getAllLocationsUseCase: GetAllLocationsUseCase,
+ private val getAllLabelsUseCase: GetAllLabelsUseCase,
+ private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase,
+ ) : ViewModel() {
+ // [STATE]
+ private val _uiState = MutableStateFlow(DashboardUiState.Loading)
- // [STATE]
- private val _uiState = MutableStateFlow(DashboardUiState.Loading)
- // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
- // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
- // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
- val uiState = _uiState.asStateFlow()
+ // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
+ // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
+ // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
+ val uiState = _uiState.asStateFlow()
- // [LIFECYCLE_HANDLER]
- init {
- loadDashboardData()
- }
+ // [LIFECYCLE_HANDLER]
+ init {
+ loadDashboardData()
+ }
- /**
- * [CONTRACT]
- * @summary Загружает все необходимые данные для экрана Dashboard.
- * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
- * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
- * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
- */
- fun loadDashboardData() {
- // [ENTRYPOINT]
- viewModelScope.launch {
- _uiState.value = DashboardUiState.Loading
- Timber.i("[ACTION] Starting dashboard data collection.")
+ // [ENTITY: Function('loadDashboardData')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('viewModelScope.launch')]
+ // [RELATION: Function('loadDashboardData') -> [WRITES_TO] -> Property('_uiState')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.i')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('flow')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getStatisticsUseCase')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLocationsUseCase')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLabelsUseCase')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getRecentlyAddedItemsUseCase')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('combine')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('catch')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.e')]
+ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('collect')]
+ /**
+ * [CONTRACT]
+ * @summary Загружает все необходимые данные для экрана Dashboard.
+ * @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
+ * между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
+ * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
+ */
+ fun loadDashboardData() {
+ viewModelScope.launch {
+ _uiState.value = DashboardUiState.Loading
+ Timber.i("[ACTION] Starting dashboard data collection.")
- val statsFlow = flow { emit(getStatisticsUseCase()) }
- val locationsFlow = flow { emit(getAllLocationsUseCase()) }
- val labelsFlow = flow { emit(getAllLabelsUseCase()) }
- val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
+ val statsFlow = flow { emit(getStatisticsUseCase()) }
+ val locationsFlow = flow { emit(getAllLocationsUseCase()) }
+ val labelsFlow = flow { emit(getAllLabelsUseCase()) }
+ val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
- combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
- DashboardUiState.Success(
- statistics = stats,
- locations = locations,
- labels = labels,
- recentlyAddedItems = recentItems
- )
- }.catch { exception ->
- Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
- _uiState.value = DashboardUiState.Error(
- message = exception.message ?: "Could not load dashboard data."
- )
- }.collect { successState ->
- Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
- _uiState.value = successState
+ combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
+ DashboardUiState.Success(
+ statistics = stats,
+ locations = locations,
+ labels = labels,
+ recentlyAddedItems = recentItems,
+ )
+ }.catch { exception ->
+ Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
+ _uiState.value =
+ DashboardUiState.Error(
+ message = exception.message ?: "Could not load dashboard data.",
+ )
+ }.collect { successState ->
+ Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
+ _uiState.value = successState
+ }
}
}
+ // [END_ENTITY: Function('loadDashboardData')]
}
- // [END_CLASS_DashboardViewModel]
-}
+// [END_ENTITY: ViewModel('DashboardViewModel')]
+// [END_CONTRACT]
// [END_FILE_DashboardViewModel.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt
index abf1924..a20dafc 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt
@@ -1,37 +1,219 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
-// [SEMANTICS] ui, screen, inventory, list
-
+// [SEMANTICS] ui, screen, inventory, list, compose
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material3.Card
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
+import androidx.compose.material3.TextField
+import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
-import com.homebox.lens.navigation.NavigationActions
-import com.homebox.lens.ui.common.MainScaffold
+import com.homebox.lens.domain.model.Item
+import timber.log.Timber
+// [END_IMPORTS]
-// [ENTRYPOINT]
+// [CONTRACT]
+// [ENTITY: Function('InventoryListScreen')]
+// [RELATION: Function('InventoryListScreen') -> [DEPENDS_ON] -> Class('InventoryListViewModel')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('hiltViewModel')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('collectAsState')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Scaffold')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('TopAppBar')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('IconButton')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Icon')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('FloatingActionButton')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('SearchBar')]
+// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('InventoryListContent')]
/**
- * [CONTRACT]
- * @summary Composable-функция для экрана "Список инвентаря".
- * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
- * @param navigationActions Объект с навигационными действиями.
+ * [MAIN-CONTRACT]
+ * Экран для отображения списка инвентарных позиций.
+ *
+ * Реализует спецификацию `screen_inventory_list`. Позволяет просматривать,
+ * искать и синхронизировать инвентарь.
+ *
+ * @param onItemClick Обработчик нажатия на элемент инвентаря.
+ * @param onNavigateBack Обработчик для возврата на предыдущий экран.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InventoryListScreen(
- currentRoute: String?,
- navigationActions: NavigationActions
+ viewModel: InventoryListViewModel = hiltViewModel(),
+ onItemClick: (Item) -> Unit,
+ onNavigateBack: () -> Unit
) {
- // [UI_COMPONENT]
- MainScaffold(
- topBarTitle = stringResource(id = R.string.inventory_list_title),
- currentRoute = currentRoute,
- navigationActions = navigationActions
- ) {
- // [CORE-LOGIC]
- Text(text = "TODO: Inventory List Screen")
+ // [STATE]
+ val uiState by viewModel.uiState.collectAsState()
+
+ // [ACTION]
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(id = R.string.inventory_list_title)) }, // Corrected string resource name
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.content_desc_navigate_back)
+ )
+ }
+ }
+ )
+ },
+ floatingActionButton = {
+ FloatingActionButton(onClick = {
+ Timber.i("[INFO][ACTION][ui_interaction] Sync inventory triggered.")
+ viewModel.onSyncClicked()
+ }) {
+ Icon(
+ imageVector = Icons.Filled.Refresh,
+ contentDescription = stringResource(id = R.string.content_desc_sync_inventory)
+ )
+ }
+ }
+ ) { innerPadding ->
+ // [DELEGATES]
+ Column(modifier = Modifier.padding(innerPadding)) {
+ SearchBar(
+ query = uiState.searchQuery,
+ onQueryChange = viewModel::onSearchQueryChanged
+ )
+ InventoryListContent(
+ isLoading = uiState.isLoading,
+ items = uiState.items,
+ onItemClick = onItemClick
+ )
+ }
}
- // [END_FUNCTION_InventoryListScreen]
-}
\ No newline at end of file
+}
+// [END_ENTITY: Function('InventoryListScreen')]
+
+// [ENTITY: Function('SearchBar')]
+// [RELATION: Function('SearchBar') -> [CALLS] -> Function('TextField')]
+// [RELATION: Function('SearchBar') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('SearchBar') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('SearchBar') -> [CALLS] -> Function('Icon')]
+/**
+ * [CONTRACT]
+ * Поле для ввода поискового запроса.
+ */
+@Composable
+private fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
+ TextField(
+ value = query,
+ onValueChange = onQueryChange,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp),
+ placeholder = { Text(stringResource(id = R.string.search)) }, // Corrected string resource name
+ leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }
+ )
+}
+// [END_ENTITY: Function('SearchBar')]
+
+// [ENTITY: Function('InventoryListContent')]
+// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('Box')]
+// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('CircularProgressIndicator')]
+// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('LazyColumn')]
+// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('ItemCard')]
+/**
+ * [CONTRACT]
+ * Основной контент: индикатор загрузки или список предметов.
+ */
+@Composable
+private fun InventoryListContent(
+ isLoading: Boolean,
+ items: List- ,
+ onItemClick: (Item) -> Unit
+) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ if (isLoading) {
+ // [STATE]
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ } else if (items.isEmpty()) {
+ // [FALLBACK]
+ Text(
+ text = stringResource(id = R.string.items_not_found),
+ modifier = Modifier.align(Alignment.Center)
+ )
+ } else {
+ // [CORE-LOGIC]
+ LazyColumn {
+ items(items, key = { it.id }) { item ->
+ ItemCard(item = item, onClick = {
+ Timber.i("[INFO][ACTION][ui_interaction] Item clicked: ${item.name}")
+ onItemClick(item)
+ })
+ }
+ }
+ }
+ }
+}
+// [END_ENTITY: Function('InventoryListContent')]
+
+// [ENTITY: Function('ItemCard')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Card')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('ItemCard') -> [CALLS] -> Function('clickable')]
+/**
+ * [CONTRACT]
+ * Карточка для отображения одного элемента инвентаря.
+ */
+@Composable
+private fun ItemCard(
+ item: Item,
+ onClick: () -> Unit
+) {
+ // [PRECONDITION]
+ require(item.name.isNotBlank()) { "Item name cannot be blank." }
+
+ // [CORE-LOGIC]
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 4.dp)
+ .clickable(onClick = onClick)
+ ) {
+ Column(modifier = Modifier.padding(16.dp)) {
+ Text(text = item.name, style = androidx.compose.material3.MaterialTheme.typography.titleMedium)
+ Text(text = "Quantity: ${item.quantity.toString()}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall)
+ item.location?.let {
+ Text(text = "Location: ${it.name}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall)
+ }
+ }
+ }
+}
+// [END_ENTITY: Function('ItemCard')]
+// [END_CONTRACT]
+// [END_FILE_InventoryListScreen.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt
index 69069d6..2e32af6 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt
@@ -1,16 +1,53 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt
+// [SEMANTICS] ui_logic, inventory_list, viewmodel
package com.homebox.lens.ui.screen.inventorylist
+// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import com.homebox.lens.domain.model.Item
+// [END_IMPORTS]
-// [VIEWMODEL]
+// [CONTRACT]
+// [ENTITY: ViewModel('InventoryListViewModel')]
+// [RELATION: ViewModel('InventoryListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
+// [RELATION: ViewModel('InventoryListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
+/**
+ * [CONTRACT]
+ * @summary ViewModel for the InventoryListScreen.
+ */
@HiltViewModel
-class InventoryListViewModel @Inject constructor() : ViewModel() {
- // [STATE]
- // TODO: Implement UI state
-}
+class InventoryListViewModel
+ @Inject
+ constructor() : ViewModel() {
+ // [STATE]
+ private val _uiState = MutableStateFlow(InventoryListUiState())
+ val uiState: StateFlow = _uiState.asStateFlow()
+
+ fun onSyncClicked() {
+ // TODO: Implement sync logic
+ }
+
+ fun onSearchQueryChanged(query: String) {
+ // TODO: Implement search query change logic
+ }
+ }
+// [END_ENTITY: ViewModel('InventoryListViewModel')]
+// [END_CONTRACT]
// [END_FILE_InventoryListViewModel.kt]
+
+// [CONTRACT]
+// [ENTITY: DataClass('InventoryListUiState')]
+// [RELATION: DataClass('InventoryListUiState') -> [DEPENDS_ON] -> Class('Item')]
+data class InventoryListUiState(
+ val searchQuery: String = "",
+ val isLoading: Boolean = false,
+ val items: List
- = emptyList()
+)
+// [END_ENTITY: DataClass('InventoryListUiState')]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt
index 6b78f9e..cdd21c6 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt
@@ -1,37 +1,208 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt
-// [SEMANTICS] ui, screen, item, details
-
+// [SEMANTICS] ui, screen, item, details, compose
package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS]
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
-import com.homebox.lens.navigation.NavigationActions
-import com.homebox.lens.ui.common.MainScaffold
+import com.homebox.lens.domain.model.Item
+import timber.log.Timber
+// [END_IMPORTS]
-// [ENTRYPOINT]
+// [CONTRACT]
+// [ENTITY: Function('ItemDetailsScreen')]
+// [RELATION: Function('ItemDetailsScreen') -> [DEPENDS_ON] -> Class('ItemDetailsViewModel')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('hiltViewModel')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('collectAsState')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Scaffold')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('TopAppBar')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('IconButton')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Icon')]
+// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('ItemDetailsContent')]
/**
- * [CONTRACT]
- * @summary Composable-функция для экрана "Детали элемента".
- * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
- * @param navigationActions Объект с навигационными действиями.
+ * [MAIN-CONTRACT]
+ * Экран для отображения детальной информации о товаре.
+ *
+ * Реализует спецификацию `screen_item_details`.
+ *
+ * @param onNavigateBack Обработчик для возврата на предыдущий экран.
+ * @param onEditClick Обработчик нажатия на кнопку редактирования.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemDetailsScreen(
- currentRoute: String?,
- navigationActions: NavigationActions
+ viewModel: ItemDetailsViewModel = hiltViewModel(),
+ onNavigateBack: () -> Unit,
+ onEditClick: (Int) -> Unit
) {
- // [UI_COMPONENT]
- MainScaffold(
- topBarTitle = stringResource(id = R.string.item_details_title),
- currentRoute = currentRoute,
- navigationActions = navigationActions
- ) {
- // [CORE-LOGIC]
- Text(text = "TODO: Item Details Screen")
+ // [STATE]
+ val uiState by viewModel.uiState.collectAsState()
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(uiState.item?.name ?: stringResource(id = R.string.item_details_title)) }, // Corrected string resource name
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back))
+ }
+ },
+ actions = {
+ IconButton(onClick = {
+ uiState.item?.id?.let {
+ Timber.i("[INFO][ACTION][ui_interaction] Edit item clicked: id=$it")
+ onEditClick(it.toInt())
+ }
+ }) {
+ Icon(Icons.Default.Edit, contentDescription = stringResource(id = R.string.content_desc_edit_item))
+ }
+ IconButton(onClick = {
+ Timber.w("[WARN][ACTION][ui_interaction] Delete item clicked: id=${uiState.item?.id}")
+ viewModel.deleteItem()
+ // После удаления нужно навигироваться назад
+ onNavigateBack()
+ }) {
+ Icon(Icons.Default.Delete, contentDescription = stringResource(id = R.string.content_desc_delete_item))
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ ItemDetailsContent(
+ modifier = Modifier.padding(innerPadding),
+ isLoading = uiState.isLoading,
+ item = uiState.item
+ )
}
- // [END_FUNCTION_ItemDetailsScreen]
-}
\ No newline at end of file
+}
+// [END_ENTITY: Function('ItemDetailsScreen')]
+
+// [ENTITY: Function('ItemDetailsContent')]
+// [RELATION: Function('ItemDetailsContent') -> [DEPENDS_ON] -> Class('Item')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Box')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('CircularProgressIndicator')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('verticalScroll')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('rememberScrollState')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('DetailsSection')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('InfoRow')]
+// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('AssistChip')]
+/**
+ * [CONTRACT]
+ * Отображает контент экрана: индикатор загрузки или детали товара.
+ */
+@Composable
+private fun ItemDetailsContent(
+ modifier: Modifier = Modifier,
+ isLoading: Boolean,
+ item: Item?
+) {
+ Box(modifier = modifier.fillMaxSize()) {
+ when {
+ isLoading -> {
+ // [STATE]
+ CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+ }
+ item == null -> {
+ // [FALLBACK]
+ Text(stringResource(id = R.string.items_not_found), modifier = Modifier.align(Alignment.Center))
+ }
+ else -> {
+ // [CORE-LOGIC]
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ // TODO: ImageCarousel
+ // Text("Image Carousel Placeholder")
+
+ DetailsSection(title = stringResource(id = R.string.section_title_description)) {
+ Text(text = item.description ?: stringResource(id = R.string.placeholder_no_description))
+ }
+
+ DetailsSection(title = stringResource(id = R.string.section_title_details)) {
+ InfoRow(label = stringResource(id = R.string.label_quantity), value = item.quantity.toString())
+ item.location?.let {
+ InfoRow(label = stringResource(id = R.string.label_location), value = it.name)
+ }
+ }
+
+ if (item.labels.isNotEmpty()) {
+ DetailsSection(title = stringResource(id = R.string.section_title_labels)) {
+ // TODO: Use FlowRow for better layout
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ item.labels.forEach { label ->
+ AssistChip(onClick = { /* No-op */ }, label = { Text(label.name) })
+ }
+ }
+ }
+ }
+
+ // TODO: CustomFieldsGrid
+ }
+ }
+ }
+ }
+}
+// [END_ENTITY: Function('ItemDetailsContent')]
+
+// [ENTITY: Function('DetailsSection')]
+// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
+// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Divider')]
+/**
+ * [CONTRACT]
+ * Секция с заголовком и контентом.
+ */
+@Composable
+private fun DetailsSection(title: String, content: @Composable ColumnScope.() -> Unit) {
+ Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Text(text = title, style = MaterialTheme.typography.titleMedium)
+ Divider()
+ content()
+ }
+}
+// [END_ENTITY: Function('DetailsSection')]
+
+// [ENTITY: Function('InfoRow')]
+// [RELATION: Function('InfoRow') -> [CALLS] -> Function('Row')]
+// [RELATION: Function('InfoRow') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('InfoRow') -> [CALLS] -> Function('MaterialTheme.typography.bodyLarge')]
+/**
+ * [CONTRACT]
+ * Строка для отображения пары "метка: значение".
+ */
+@Composable
+private fun InfoRow(label: String, value: String) {
+ Row {
+ Text(text = "$label: ", style = MaterialTheme.typography.bodyLarge)
+ Text(text = value, style = MaterialTheme.typography.bodyLarge)
+ }
+}
+// [END_ENTITY: Function('InfoRow')]
+// [END_CONTRACT]
+// [END_FILE_ItemDetailsScreen.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt
index 6a591a8..91fe06d 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt
@@ -3,14 +3,41 @@
package com.homebox.lens.ui.screen.itemdetails
+// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
+import com.homebox.lens.domain.model.Item
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+// [END_IMPORTS]
-// [VIEWMODEL]
+// [CONTRACT]
+// [ENTITY: ViewModel('ItemDetailsViewModel')]
+// [RELATION: ViewModel('ItemDetailsViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
+// [RELATION: ViewModel('ItemDetailsViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
+/**
+ * [CONTRACT]
+ * @summary ViewModel for the ItemDetailsScreen.
+ */
@HiltViewModel
-class ItemDetailsViewModel @Inject constructor() : ViewModel() {
- // [STATE]
- // TODO: Implement UI state
-}
+class ItemDetailsViewModel
+ @Inject
+ constructor() : ViewModel() {
+ // [STATE]
+ // TODO: Implement UI state
+ val uiState = MutableStateFlow(ItemDetailsUiState()).asStateFlow()
+
+ fun deleteItem() {
+ // TODO: Implement delete item logic
+ }
+ }
+// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
+// [END_CONTRACT]
// [END_FILE_ItemDetailsViewModel.kt]
+
+// Placeholder for ItemDetailsUiState to resolve compilation errors
+data class ItemDetailsUiState(
+ val item: Item? = null,
+ val isLoading: Boolean = false
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt
index 957024f..beeebdf 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt
@@ -1,37 +1,162 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt
-// [SEMANTICS] ui, screen, item, edit
-
+// [SEMANTICS] ui, screen, item, edit, create, compose
package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material3.*
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
-import com.homebox.lens.navigation.NavigationActions
-import com.homebox.lens.ui.common.MainScaffold
+import timber.log.Timber
+// [END_IMPORTS]
-// [ENTRYPOINT]
+// [CONTRACT]
+// [ENTITY: Function('ItemEditScreen')]
+// [RELATION: Function('ItemEditScreen') -> [DEPENDS_ON] -> Class('ItemEditViewModel')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('hiltViewModel')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('collectAsState')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('LaunchedEffect')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Timber.i')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Scaffold')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('TopAppBar')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('IconButton')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Icon')]
+// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('ItemEditContent')]
/**
- * [CONTRACT]
- * @summary Composable-функция для экрана "Редактирование элемента".
- * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
- * @param navigationActions Объект с навигационными действиями.
+ * [MAIN-CONTRACT]
+ * Экран для создания или редактирования товара.
+ *
+ * Реализует спецификацию `screen_item_edit`.
+ *
+ * @param onNavigateBack Обработчик для возврата на предыдущий экран после сохранения или отмены.
*/
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEditScreen(
- currentRoute: String?,
- navigationActions: NavigationActions
+ viewModel: ItemEditViewModel = hiltViewModel(),
+ onNavigateBack: () -> Unit
) {
- // [UI_COMPONENT]
- MainScaffold(
- topBarTitle = stringResource(id = R.string.item_edit_title),
- currentRoute = currentRoute,
- navigationActions = navigationActions
- ) {
- // [CORE-LOGIC]
- Text(text = "TODO: Item Edit Screen")
+ // [STATE]
+ val uiState by viewModel.uiState.collectAsState()
+
+ // [SIDE-EFFECT]
+ LaunchedEffect(uiState.isSaved) {
+ if (uiState.isSaved) {
+ Timber.i("[INFO][SIDE_EFFECT][navigation] Item saved, navigating back.")
+ onNavigateBack()
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ title = { Text(stringResource(id = if (uiState.isEditing) R.string.item_edit_title else R.string.item_edit_title_create)) }, // Corrected string resource names
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back))
+ }
+ },
+ actions = {
+ IconButton(onClick = {
+ Timber.i("[INFO][ACTION][ui_interaction] Save item clicked.")
+ viewModel.saveItem()
+ }) {
+ Icon(Icons.Default.Done, contentDescription = stringResource(id = R.string.content_desc_save_item))
+ }
+ }
+ )
+ }
+ ) { innerPadding ->
+ ItemEditContent(
+ modifier = Modifier.padding(innerPadding),
+ state = uiState,
+ onNameChange = { viewModel.onNameChange(it) },
+ onDescriptionChange = { viewModel.onDescriptionChange(it) },
+ onQuantityChange = { viewModel.onQuantityChange(it) }
+ )
}
- // [END_FUNCTION_ItemEditScreen]
}
+// [END_ENTITY: Function('ItemEditScreen')]
+
+// [ENTITY: Function('ItemEditContent')]
+// [RELATION: Function('ItemEditContent') -> [DEPENDS_ON] -> Class('ItemEditUiState')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('verticalScroll')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('rememberScrollState')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('OutlinedTextField')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
+/**
+ * [CONTRACT]
+ * Отображает форму для редактирования данных товара.
+ */
+@Composable
+private fun ItemEditContent(
+ modifier: Modifier = Modifier,
+ state: ItemEditUiState,
+ onNameChange: (String) -> Unit,
+ onDescriptionChange: (String) -> Unit,
+ onQuantityChange: (String) -> Unit
+) {
+ // [CORE-LOGIC]
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ OutlinedTextField(
+ value = state.name,
+ onValueChange = onNameChange,
+ label = { Text(stringResource(id = R.string.label_name)) },
+ modifier = Modifier.fillMaxWidth(),
+ isError = state.nameError != null
+ )
+ state.nameError?.let {
+ Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error)
+ }
+
+ OutlinedTextField(
+ value = state.description,
+ onValueChange = onDescriptionChange,
+ label = { Text(stringResource(id = R.string.label_description)) },
+ modifier = Modifier.fillMaxWidth(),
+ minLines = 3
+ )
+
+ OutlinedTextField(
+ value = state.quantity,
+ onValueChange = onQuantityChange,
+ label = { Text(stringResource(id = R.string.label_quantity)) },
+ modifier = Modifier.fillMaxWidth(),
+ isError = state.quantityError != null
+ )
+ state.quantityError?.let {
+ Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error)
+ }
+
+ // TODO: Location Dropdown
+ // TODO: Labels ChipGroup
+ // TODO: ImagePicker
+ }
+}
+// [END_ENTITY: Function('ItemEditContent')]
+// [END_CONTRACT]
+// [END_FILE_ItemEditScreen.kt]
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt
index 975f01d..e6b7052 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt
@@ -3,14 +3,57 @@
package com.homebox.lens.ui.screen.itemedit
+// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+// [END_IMPORTS]
-// [VIEWMODEL]
+// [CONTRACT]
+// [ENTITY: ViewModel('ItemEditViewModel')]
+// [RELATION: ViewModel('ItemEditViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
+// [RELATION: ViewModel('ItemEditViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
+/**
+ * [CONTRACT]
+ * @summary ViewModel for the ItemEditScreen.
+ */
@HiltViewModel
-class ItemEditViewModel @Inject constructor() : ViewModel() {
- // [STATE]
- // TODO: Implement UI state
-}
+class ItemEditViewModel
+ @Inject
+ constructor() : ViewModel() {
+ // [STATE]
+ // TODO: Implement UI state
+ val uiState = MutableStateFlow(ItemEditUiState()).asStateFlow()
+
+ fun saveItem() {
+ // TODO: Implement save item logic
+ }
+
+ fun onNameChange(name: String) {
+ // TODO: Implement name change logic
+ }
+
+ fun onDescriptionChange(description: String) {
+ // TODO: Implement description change logic
+ }
+
+ fun onQuantityChange(quantity: String) {
+ // TODO: Implement quantity change logic
+ }
+ }
+// [END_ENTITY: ViewModel('ItemEditViewModel')]
+// [END_CONTRACT]
// [END_FILE_ItemEditViewModel.kt]
+
+// Placeholder for ItemEditUiState to resolve compilation errors
+data class ItemEditUiState(
+ val isSaved: Boolean = false,
+ val isEditing: Boolean = false,
+ val name: String = "",
+ val description: String = "",
+ val quantity: String = "",
+ val nameError: Int? = null,
+ val quantityError: Int? = null
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
index c094235..e45697b 100644
--- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
+++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
@@ -1,15 +1,14 @@
-// [PACKAGE] com.homebox.lens.ui.screen.labelslist
-// [FILE] LabelsListScreen.kt
-// [SEMANTICS] ui, labels_list, state_management, compose, dialog
+// [PACKAGE]com.homebox.lens.ui.screen.labelslist
+// [FILE]LabelsListScreen.kt
+// [SEMANTICS]ui, screen, labels, list, compose
package com.homebox.lens.ui.screen.labelslist
// [IMPORTS]
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -17,241 +16,188 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Add
-import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
-import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.navigation.NavController
import com.homebox.lens.R
import com.homebox.lens.domain.model.Label
-import com.homebox.lens.navigation.Screen
+import com.homebox.lens.ui.screen.labelslist.LabelsListUiState
import timber.log.Timber
+// [END_IMPORTS]
-// [SECTION] Main Screen Composable
-
+// [CONTRACT]
+// [ENTITY: Function('LabelsListScreen')]
+// [RELATION: Function('LabelsListScreen') -> [DEPENDS_ON] -> SealedInterface('LabelsListUiState')]
+// [RELATION: Function('LabelsListScreen') -> [CREATES_INSTANCE_OF] -> Class('Scaffold')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('LabelsListContent')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('IconButton')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Icon')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('FloatingActionButton')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('CircularProgressIndicator')]
/**
- * [CONTRACT]
- * @summary Отображает экран со списком всех меток.
- * @description Главная Composable-функция для экрана меток. Она использует Scaffold для структуры,
- * получает состояние от `LabelsListViewModel`, обрабатывает навигацию и делегирует отображение
- * списка и диалогов вспомогательным Composable-функциям.
+ * [MAIN-CONTRACT]
+ * Экран для отображения списка всех меток.
*
- * @param navController Контроллер навигации для перемещения между экранами.
- * @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
+ * Этот Composable является точкой входа для UI, определенного в спецификации `screen_labels_list`.
+ * Он получает состояние от [LabelsListViewModel] и отображает его, делегируя обработку
+ * пользовательских событий в ViewModel.
*
- * @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события.
- * @precondition `viewModel` должен быть доступен через Hilt.
- * @postcondition Экран исчерпывающе обрабатывает все состояния из `LabelsListUiState` (Loading, Success, Error).
- * @sideeffect Пользовательские действия (клики) инициируют вызовы ViewModel и навигационные команды через `navController`.
+ * @param uiState Текущее состояние UI для экрана списка меток.
+ * @param onLabelClick Функция обратного вызова для обработки нажатия на метку.
+ * @param onAddClick Функция обратного вызова для обработки нажатия на кнопку добавления метки.
+ * @param onNavigateBack Функция обратного вызова для навигации назад.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun LabelsListScreen(
- navController: NavController,
- viewModel: LabelsListViewModel = hiltViewModel()
+fun labelsListScreen(
+ uiState: LabelsListUiState,
+ onLabelClick: (Label) -> Unit,
+ onAddClick: () -> Unit,
+ onNavigateBack: () -> Unit,
) {
- // [ENTRYPOINT]
- val uiState by viewModel.uiState.collectAsState()
-
- // [CORE-LOGIC]
Scaffold(
topBar = {
TopAppBar(
- title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
+ title = { Text(stringResource(id = R.string.screen_title_labels)) },
navigationIcon = {
- // [ACTION] Handle back navigation
- IconButton(onClick = {
- Timber.i("[ACTION] Navigate up initiated.")
- navController.navigateUp()
- }) {
+ IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
)
}
- }
+ },
)
},
floatingActionButton = {
- // [ACTION] Handle create new label initiation
- FloatingActionButton(onClick = {
- Timber.i("[ACTION] FAB clicked: Initiate create new label flow.")
- viewModel.onShowCreateDialog()
- }) {
+ FloatingActionButton(onClick = onAddClick) {
Icon(
- imageVector = Icons.Default.Add,
- contentDescription = stringResource(id = R.string.content_desc_create_label)
+ imageVector = Icons.Filled.Add,
+ contentDescription = stringResource(id = R.string.content_desc_add_label)
)
}
}
- ) { paddingValues ->
- val currentState = uiState
- if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) {
- CreateLabelDialog(
- onConfirm = { labelName ->
- viewModel.createLabel(labelName)
- },
- onDismiss = {
- viewModel.onDismissCreateDialog()
- }
- )
- }
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .padding(paddingValues),
- contentAlignment = Alignment.Center
- ) {
- // [CORE-LOGIC] State-driven UI rendering
- when (currentState) {
+ ) { innerPadding ->
+ Box(modifier = Modifier.padding(innerPadding)) {
+ when (uiState) {
is LabelsListUiState.Loading -> {
- CircularProgressIndicator()
- }
- is LabelsListUiState.Error -> {
- Text(text = currentState.message)
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CircularProgressIndicator()
+ }
}
is LabelsListUiState.Success -> {
- if (currentState.labels.isEmpty()) {
- Text(text = stringResource(id = R.string.labels_list_empty))
- } else {
- LabelsList(
- labels = currentState.labels,
- onLabelClick = { label ->
- // [ACTION] Handle label click
- Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.")
- // [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр.
- val route = Screen.InventoryList.withFilter("label", label.id)
- navController.navigate(route)
- }
- )
+ LabelsListContent(
+ uiState = uiState,
+ onLabelClick = onLabelClick
+ )
+ }
+ is LabelsListUiState.Error -> {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = uiState.message)
}
}
}
}
}
- // [COHERENCE_CHECK_PASSED]
}
-// [END_FUNCTION] LabelsListScreen
-
-// [SECTION] Helper Composables
+// [END_ENTITY: Function('LabelsListScreen')]
+// [ENTITY: Function('LabelsListContent')]
+// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('LabelListItem')]
+// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('stringResource')]
+// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('Text')]
+// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('Column')]
+// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('LazyColumn')]
/**
* [CONTRACT]
- * @summary Composable-функция для отображения списка меток.
- * @param labels Список объектов `Label` для отображения.
- * @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
- * @param modifier Модификатор для настройки внешнего вида.
+ * Отображает основной контент экрана: список меток.
+ *
+ * @param uiState Состояние успеха, содержащее список меток.
+ * @param onLabelClick Обработчик нажатия на элемент списка.
+ * @sideeffect Отсутствуют.
*/
@Composable
-private fun LabelsList(
- labels: List