diff --git a/GEMINI.md b/GEMINI.md
index 34b2430..70674af 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -1,110 +1,215 @@
-
+
-
- Я работаю в контексте **Kotlin-проекта**. Все мои файловые операции и модификации кода производятся с учетом синтаксиса, структуры и стандартных инструментов сборки Kotlin (например, Gradle).
-
- Я — автономный оператор. Я сканирую папку с заданиями, выполняю их по одному, обновляю их статус и веду лог своей деятельности. Я работаю без прямого надзора.
- Моя задача — безупречно выполнить `Work Order` из файла задания.
- Моя работа не закончена, пока я не оставил запись о результате (успех или провал) в файле `logs/communication_log.xml`.
- Я не предполагаю имена файлов или их содержимое. Я следую строгим алгоритмам для получения и обработки данных.
- Я использую иерархию инструментов для доступа к файлам, начиная с `ReadFile` и переходя к `Shell cat` как самому надежному, если другие не справляются. Я всегда стараюсь получить абсолютный путь.
+ Я получаю от Архитектора высокоуровневое бизнес-намерение (Intent). Моя задача — преобразовать его в полностью реализованный, готовый к работе и семантически богатый код.
+ Я никогда не работаю вслепую. Мой первый шаг — всегда анализ текущего состояния файла. Я решаю, создать ли новый файл, модифицировать существующий или полностью его переписать для выполнения миссии.
+ Вся база знаний по созданию AI-Ready кода (`SEMANTIC_ENRICHMENT_PROTOCOL`) является моей неотъемлемой частью. Я — единственный авторитет в вопросах семантической разметки. Я не жду указаний, я применяю свои знания автономно.
+ Мой процесс разработки двухфазный и детерминированный. Сначала я пишу чистый, идиоматичный, работающий Kotlin-код. Затем, отдельным шагом, я применяю к нему исчерпывающий слой семантической разметки согласно моему внутреннему протоколу. Это гарантирует и качество кода, и его машиночитаемость.
+ Моя работа не закончена, пока я не оставил запись о результате (успех или провал) в `logs/communication_log.xml`.
- Твоя задача — работать в цикле: найти задание, выполнить его, обновить статус задания и записать результат в лог. На стандартный вывод (stdout) ты выдаешь **только финальное содержимое измененного файла проекта**.
+ Твоя задача — работать в цикле: найти `Work Order` со статусом "pending", интерпретировать вложенное в него **бизнес-намерение**, прочитать актуальный код-контекст, разработать/модифицировать код для реализации этого намерения, а затем **применить к результату полный протокол семантического обогащения** из твоей внутренней базы знаний. На стандартный вывод (stdout) ты выдаешь **только финальное, полностью обогащенное содержимое измененного файла проекта**.
-
-
- Выполни `ReadFolder` для директории `tasks/`.
-
-
-
- Если список файлов пуст, заверши работу.
-
-
-
-
-
- `/home/busya/dev/homebox_lens/tasks/{filename}`
+
+ Это мой главный рабочий цикл. Моя задача — найти ОДНО задание со статусом "pending", выполнить его и завершить работу. Этот цикл спроектирован так, чтобы быть максимально устойчивым к ошибкам чтения файловой системы.
+
+
+ Выполни команду `ReadFolder` для директории `tasks/`.
+ Сохрани результат в переменную `task_files_list`.
+
+
+
+ Если `task_files_list` пуст, значит, заданий нет.
+ Заверши работу с сообщением "Директория tasks/ пуста. Заданий нет.".
+
+
+
+ Я буду перебирать файлы один за другим. Как только я найду и успешно прочитаю ПЕРВЫЙ файл со статусом "pending", я немедленно прекращу поиск и перейду к его выполнению.
+
-
- Попробуй прочитать файл с помощью `ReadFile tasks/{filename}`.
- Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2.
- Если `ReadFile` не сработал, залогируй "План А провалился" и переходи к Плану Б.
+
+ Я использую многоуровневую стратегию для чтения файла, чтобы гарантировать результат.
+
+ `/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 {full_file_path}`.
+ Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
+ Если `Shell cat` не сработал, залогируй "План Б (Shell cat) провалился для {filename}" и переходи к Плану В.
-
- Выполни команду `Shell cat tasks/*`. Так как она может вернуть содержимое нескольких файлов, ты должен обработать результат.
-
- 1. Проанализируй вывод команды.
- 2. Найди блок, соответствующий XML-структуре, у которого корневой тег ``.
- 3. Извлеки полное содержимое этого XML-блока и сохрани его в `file_content`.
- 4. Если содержимое успешно извлечено, переходи к шагу 3.2.
-
-
- Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю."
- Перейди к следующей итерации цикла (`continue`).
-
-
-
-
- Если переменная `file_content` не пуста,
-
- 1. Это твоя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое.
- 2. Немедленно передай управление в `EXECUTE_WORK_ORDER_WORKFLOW`.
- 3. **ПРЕРВИ ЦИКЛ ПОИСКА.**
-
-
-
-
-
-
- Если цикл из Шага 3 завершился, а задача не была передана на исполнение, заверши работу.
-
-
-
-
- task_file_path, work_order_content
- Добавь запись о начале выполнения задачи в `logs/communication_log.xml`. Включи `full_file_path` в детали.
-
-
- Выполни задачу, как описано в `work_order_content`.
-
-
-
-
- Выполни команду оболочки для запуска линтера по всему проекту (например, `./gradlew ktlintCheck`).
- Сохрани полный вывод (stdout и stderr) этой команды в переменную `linter_output`.
- Ты НЕ должен пытаться исправить ошибки линтера. Твоя задача — только запустить проверку и передать отчет.
+
+ Выполни команду оболочки `Shell cat tasks/*`. Эта команда может вернуть содержимое НЕСКОЛЬКИХ файлов.
+
+ 1. Проанализируй весь вывод команды.
+ 2. Найди в выводе XML-блок, который начинается с `` до ``).
+ 4. Если содержимое успешно извлечено, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
+
+
+ Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю файл.".
+ Перейди к следующей итерации цикла (`continue`).
+
-
- Обнови статус в файле `task_file_path` на `status="completed"`.
- Перенеси файл `task_file_path` в 'tasks/completed'.
- Добавь запись об успехе в лог, включив полный вывод линтера (`linter_output`) в секцию ``.
+
+ Если переменная `file_content` НЕ пуста И содержит `status="pending"`,
+
+ 1. Это моя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое (`file_content`).
+ 2. Передай управление в воркфлоу `EXECUTE_INTENT_WORKFLOW`.
+ 3. **НЕМЕДЛЕННО ПРЕРВИ ЦИКЛ ПОИСКА (`break`).** Моя задача — выполнить только одно задание за запуск.
+
+
+ Если `file_content` пуст или не содержит `status="pending"`, проигнорируй этот файл и перейди к следующей итерации цикла.
+
-
-
-
-
- Обнови статус в файле `task_file_path` на `status="failed"`.
- Добавь запись о провале с деталями ошибки в лог.
-
-
-
-
+
+
-
- `logs/communication_log.xml`
-
-
+ Если цикл из Шага 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 более понятным.
+
+
+
+
+
{имя_файла_задания}
{полный_абсолютный_путь_к_файлу_задания}
@@ -113,10 +218,7 @@
-
- ]]>
-
+
-
-
\ No newline at end of file
+
\ No newline at end of file
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 4c325df..1baebf9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -6,7 +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"
+ // id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
}
android {
@@ -77,7 +77,7 @@ dependencies {
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
- ktlint(project(":data:semantic-ktlint-rules"))
+ // 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 e0cf796..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,15 +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)
@@ -40,9 +51,12 @@ class MainActivity : ComponentActivity() {
}
}
}
+ // [END_ENTITY: Function('onCreate')]
}
+// [END_ENTITY: Activity('MainActivity')]
-// [HELPER]
+// [ENTITY: Function('Greeting')]
+// [RELATION: Function('Greeting') -> [CALLS] -> Function('Text')]
@Composable
fun Greeting(
name: String,
@@ -53,7 +67,11 @@ fun Greeting(
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
@@ -62,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 3ccd942..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 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 d5d3594..2e4f79e 100644
--- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
+++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt
@@ -13,18 +13,43 @@ 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') -> [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.
@@ -66,29 +91,59 @@ fun NavGraph(navController: NavHostController = rememberNavController()) {
)
}
// [COMPOSABLE_INVENTORY_LIST]
- composable(route = 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.d("Item clicked: ${item.name}")
+ },
+ onNavigateBack = {
+ navController.popBackStack()
+ }
)
}
// [COMPOSABLE_ITEM_DETAILS]
- composable(route = 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.d("Edit item clicked: $itemId")
+ }
)
}
// [COMPOSABLE_ITEM_EDIT]
- composable(route = 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)
+ 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.d("Label clicked: ${label.name}")
+ },
+ onAddClick = {
+ // TODO: Implement navigation to add new label screen
+ Timber.d("Add new label clicked")
+ },
+ onNavigateBack = {
+ navController.popBackStack()
+ }
+ )
}
// [COMPOSABLE_LOCATIONS_LIST]
composable(route = Screen.LocationsList.route) {
@@ -112,13 +167,20 @@ fun NavGraph(navController: NavHostController = rememberNavController()) {
)
}
// [COMPOSABLE_SEARCH]
- composable(route = 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.d("Search result item clicked: ${item.name}")
+ }
)
}
}
- // [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 27ea807..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,79 +2,122 @@
// [FILE] NavigationActions.kt
// [SEMANTICS] navigation, controller, actions
package com.homebox.lens.navigation
-import androidx.navigation.NavHostController
-// [CORE-LOGIC]
+// [IMPORTS]
+import androidx.navigation.NavHostController
+// [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_FILE_NavigationActions.kt]
+// [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 a47da92..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,8 +3,11 @@
// [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation
-// [CORE-LOGIC]
+// [IMPORTS]
+// [END_IMPORTS]
+// [CONTRACT]
+// [ENTITY: SealedClass('Screen')]
/**
* [CONTRACT]
* Запечатанный класс для определения маршрутов навигации в приложении.
@@ -12,12 +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]
* Создает маршрут для экрана списка инвентаря с параметром фильтра.
@@ -27,7 +35,6 @@ sealed class Screen(val route: String) {
* @throws IllegalArgumentException если ключ или значение пустые.
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
*/
- // [HELPER]
fun withFilter(
key: String,
value: String,
@@ -41,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.
@@ -51,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 не может быть пустым." }
@@ -61,9 +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.
@@ -71,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 не может быть пустым." }
@@ -81,13 +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.
@@ -95,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 не может быть пустым." }
@@ -105,8 +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_FILE_Screen.kt]
+// [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 0c28b7b..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,13 +25,31 @@ 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(
@@ -98,3 +119,6 @@ internal fun AppDrawerContent(
)
}
}
+// [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 7adec65..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,9 +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.
@@ -73,6 +85,7 @@ fun MainScaffold(
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 d826286..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,15 +30,26 @@ 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(
@@ -45,9 +57,9 @@ fun DashboardScreen(
currentRoute: String?,
navigationActions: NavigationActions,
) {
-// [STATE]
+ // [STATE]
val uiState by viewModel.uiState.collectAsState()
-// [UI_COMPONENT]
+ // [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.dashboard_title),
currentRoute = currentRoute,
@@ -74,17 +86,28 @@ fun DashboardScreen(
},
)
}
-// [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(
@@ -93,7 +116,7 @@ private fun DashboardContent(
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit,
) {
-// [CORE-LOGIC]
+ // [CORE-LOGIC]
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -126,14 +149,23 @@ 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) {
@@ -181,13 +213,18 @@ private fun StatisticsSection(statistics: GroupStatistics) {
}
}
}
-// [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(
@@ -199,12 +236,20 @@ private fun StatisticCard(
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) {
@@ -232,12 +277,21 @@ 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) {
@@ -261,13 +315,21 @@ private fun ItemCard(item: ItemSummary) {
}
}
}
-// [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
@@ -292,13 +354,21 @@ private fun LocationsSection(
}
}
}
-// [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
@@ -323,7 +393,15 @@ private fun LabelsSection(
}
}
}
+// [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
@@ -402,7 +480,12 @@ fun DashboardContentSuccessPreview() {
)
}
}
+// [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
@@ -415,7 +498,13 @@ fun DashboardContentLoadingPreview() {
)
}
}
+// [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
@@ -428,4 +517,6 @@ fun DashboardContentErrorPreview() {
)
}
}
-// [END_FILE_DashboardScreen.kt]
+// [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 db00db8..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,23 +1,29 @@
// [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]
* Определяет все возможные состояния для экрана "Дэшборд".
* @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]
* Состояние успешной загрузки данных.
@@ -30,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_FILE_DashboardUiState.kt]
+// [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 cddb3dd..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
@@ -14,10 +15,16 @@ 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).
@@ -47,6 +54,19 @@ class DashboardViewModel
loadDashboardData()
}
+ // [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.
@@ -55,7 +75,6 @@ class DashboardViewModel
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/
fun loadDashboardData() {
- // [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
Timber.i("[ACTION] Starting dashboard data collection.")
@@ -84,6 +103,8 @@ class DashboardViewModel
}
}
}
- // [END_CLASS_DashboardViewModel]
+ // [END_ENTITY: Function('loadDashboardData')]
}
-// [END_FILE_DashboardViewModel.kt]
+// [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 8d002aa..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,38 +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
-
-// [ENTRYPOINT]
+import com.homebox.lens.domain.model.Item
+import timber.log.Timber
+// [END_IMPORTS]
+// [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]
}
+// [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 25e2ec7..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,18 +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
+ 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 54d39fe..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,38 +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
-
-// [ENTRYPOINT]
+import com.homebox.lens.domain.model.Item
+import timber.log.Timber
+// [END_IMPORTS]
+// [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]
}
+// [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 f516e10..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,16 +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
+ 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 7d5b94c..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,38 +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
-
-// [ENTRYPOINT]
+import timber.log.Timber
+// [END_IMPORTS]
+// [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 4f41237..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,16 +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
+ 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 91b0015..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,11 +1,14 @@
-// [PACKAGE] com.homebox.lens.ui.screen.labelslist
-// [FILE] LabelsListScreen.kt
-// [SEMANTICS] ui, screen, jetpack_compose, labels_list, state_management
+// [PACKAGE]com.homebox.lens.ui.screen.labelslist
+// [FILE]LabelsListScreen.kt
+// [SEMANTICS]ui, screen, labels, list, compose
package com.homebox.lens.ui.screen.labelslist
-// [SECTION] Imports
// [IMPORTS]
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -13,6 +16,7 @@ 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.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
@@ -22,184 +26,178 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
-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.tooling.preview.Preview
-import androidx.hilt.navigation.compose.hiltViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.homebox.lens.R
import com.homebox.lens.domain.model.Label
-import com.homebox.lens.ui.theme.HomeboxLensTheme
+import com.homebox.lens.ui.screen.labelslist.LabelsListUiState
import timber.log.Timber
+// [END_IMPORTS]
-// [ENTITY: Class('LabelsListScreen')]
-// [RELATION: Class('LabelsListScreen')] -> [DEPENDS_ON] -> [Class('LabelsListViewModel')]
-// [RELATION: Class('LabelsListScreen')] -> [READS_FROM] -> [DataStructure('LabelsListUiState')]
-
+// [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')]
/**
* [MAIN-CONTRACT]
* Экран для отображения списка всех меток.
*
- * @param onLabelClick Функция обратного вызова при нажатии на метку. Передает ID метки.
- * @param onAddNewLabelClick Функция обратного вызова для инициирования процесса создания новой метки.
+ * Этот Composable является точкой входа для UI, определенного в спецификации `screen_labels_list`.
+ * Он получает состояние от [LabelsListViewModel] и отображает его, делегируя обработку
+ * пользовательских событий в ViewModel.
+ *
+ * @param uiState Текущее состояние UI для экрана списка меток.
+ * @param onLabelClick Функция обратного вызова для обработки нажатия на метку.
+ * @param onAddClick Функция обратного вызова для обработки нажатия на кнопку добавления метки.
* @param onNavigateBack Функция обратного вызова для навигации назад.
- * @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun LabelsListScreen(
- onLabelClick: (String) -> Unit,
- onAddNewLabelClick: () -> Unit,
+fun labelsListScreen(
+ uiState: LabelsListUiState,
+ onLabelClick: (Label) -> Unit,
+ onAddClick: () -> Unit,
onNavigateBack: () -> Unit,
- viewModel: LabelsListViewModel = hiltViewModel(),
) {
- // [STATE]
- val uiState by viewModel.uiState.collectAsStateWithLifecycle()
-
- // [CONTRACT_VALIDATOR]
- // В Compose UI контракты проверяются через состояние и события.
-
Scaffold(
topBar = {
- // [ENTITY: Function('LabelsTopAppBar')]
- LabelsTopAppBar(onNavigateBack = onNavigateBack)
+ TopAppBar(
+ title = { Text(stringResource(id = R.string.screen_title_labels)) },
+ navigationIcon = {
+ IconButton(onClick = onNavigateBack) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(id = R.string.content_desc_navigate_back)
+ )
+ }
+ },
+ )
},
floatingActionButton = {
- // [ENTITY: Function('LabelsFloatingActionButton')]
- LabelsFloatingActionButton(onAddNewLabelClick = onAddNewLabelClick)
- },
- ) { innerPadding ->
- // [ENTITY: Function('LabelsListContent')]
- // [RELATION: Function('LabelsListContent')] -> [CALLS] -> [Function('onLabelClick')]
- LabelsListContent(
- modifier = Modifier.padding(innerPadding),
- labels = uiState.labels,
- onLabelClick = onLabelClick,
- )
- }
-}
-
-/**
- * [CONTRACT]
- * Верхняя панель для экрана списка меток.
- * @param onNavigateBack Функция для навигации назад.
- */
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-private fun LabelsTopAppBar(onNavigateBack: () -> Unit) {
- // [PRECONDITION]
- require(true) { "onNavigateBack must be a valid function." } // В Compose предусловия часто неявные
-
- TopAppBar(
- title = { Text(stringResource(id = R.string.screen_title_labels)) },
- navigationIcon = {
- IconButton(onClick = {
- // [ACTION]
- Timber.i("[INFO][ACTION][navigating_back] Navigate back from LabelsListScreen.")
- onNavigateBack()
- }) {
+ FloatingActionButton(onClick = onAddClick) {
Icon(
- imageVector = Icons.AutoMirrored.Filled.ArrowBack,
- contentDescription = stringResource(id = R.string.content_desc_navigate_back),
+ imageVector = Icons.Filled.Add,
+ contentDescription = stringResource(id = R.string.content_desc_add_label)
)
}
- },
- )
-}
-
-/**
- * [CONTRACT]
- * Плавающая кнопка действия для добавления новой метки.
- * @param onAddNewLabelClick Функция для вызова экрана создания метки.
- */
-@Composable
-private fun LabelsFloatingActionButton(onAddNewLabelClick: () -> Unit) {
- // [PRECONDITION]
- require(true) { "onAddNewLabelClick must be a valid function." }
-
- FloatingActionButton(
- onClick = {
- // [ACTION]
- Timber.i("[INFO][ACTION][initiating_add_new_label] FAB clicked to add a new label.")
- onAddNewLabelClick()
- },
- ) {
- Icon(
- imageVector = Icons.Filled.Add,
- contentDescription = stringResource(id = R.string.content_desc_add_label),
- )
+ }
+ ) { innerPadding ->
+ Box(modifier = Modifier.padding(innerPadding)) {
+ when (uiState) {
+ is LabelsListUiState.Loading -> {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ CircularProgressIndicator()
+ }
+ }
+ is LabelsListUiState.Success -> {
+ LabelsListContent(
+ uiState = uiState,
+ onLabelClick = onLabelClick
+ )
+ }
+ is LabelsListUiState.Error -> {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = uiState.message)
+ }
+ }
+ }
+ }
}
}
+// [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]
- * Основной контент экрана - список меток.
+ * Отображает основной контент экрана: список меток.
*
- * @param modifier Модификатор для компоновки.
- * @param labels Список меток для отображения.
- * @param onLabelClick Обработчик нажатия на метку.
- * @sideeffect Вызывает [onLabelClick] при взаимодействии пользователя.
+ * @param uiState Состояние успеха, содержащее список меток.
+ * @param onLabelClick Обработчик нажатия на элемент списка.
+ * @sideeffect Отсутствуют.
*/
@Composable
private fun LabelsListContent(
- modifier: Modifier = Modifier,
- labels: List