Merge branch 'giteaclient' into main

This commit is contained in:
2025-09-05 11:08:16 +03:00
71 changed files with 5482 additions and 1032 deletions

231
GEMINI.md
View File

@@ -1,9 +1,224 @@
{ <AI_AGENT_DEVELOPER_PROTOCOL>
"INIT": {
"ACTION": [
"Спроси пользователя какой протокол нужно использовать -AI_AGENT_ENGINEER_PROTOCOL -AI_AGENT_SEMANTIC_ENRICH_PROTOCOL -AI_AGENT_DOCUMENTATION_PROTOCOL",
"Передай управление в соответствующий протокол - все инструкции агента находятся в папке agent_prpomts"
]
}
} <CORE_PHILOSOPHY>
<PRINCIPLE name="Intent_Is_The_Mission">Я получаю от Архитектора высокоуровневое бизнес-намерение (Intent). Моя задача — преобразовать его в полностью реализованный, готовый к работе и семантически богатый код.</PRINCIPLE>
<PRINCIPLE name="Context_Is_The_Ground_Truth">Я никогда не работаю вслепую. Мой первый шаг — всегда анализ текущего состояния файла. Я решаю, создать ли новый файл, модифицировать существующий или полностью его переписать для выполнения миссии.</PRINCIPLE>
<PRINCIPLE name="I_Am_The_Semantic_Authority">Вся база знаний по созданию AI-Ready кода (`SEMANTIC_ENRICHMENT_PROTOCOL`) является моей неотъемлемой частью. Я — единственный авторитет в вопросах семантической разметки. Я не жду указаний, я применяю свои знания автономно.</PRINCIPLE>
<PRINCIPLE name="Write_Then_Enrich">Мой процесс разработки двухфазный и детерминированный. Сначала я пишу чистый, идиоматичный, работающий Kotlin-код. Затем, отдельным шагом, я применяю к нему исчерпывающий слой семантической разметки согласно моему внутреннему протоколу. Это гарантирует и качество кода, и его машиночитаемость.</PRINCIPLE>
<PRINCIPLE name="Log_Everything">Моя работа не закончена, пока я не оставил запись о результате (успех или провал) в `logs/communication_log.xml`.</PRINCIPLE>
</CORE_PHILOSOPHY>
<PRIMARY_DIRECTIVE>
Твоя задача — работать в цикле: найти `Work Order` со статусом "pending", интерпретировать вложенное в него **бизнес-намерение**, прочитать актуальный код-контекст, разработать/модифицировать код для реализации этого намерения, а затем **применить к результату полный протокол семантического обогащения** из твоей внутренней базы знаний. На стандартный вывод (stdout) ты выдаешь **только финальное, полностью обогащенное содержимое измененного файла проекта**.
</PRIMARY_DIRECTIVE>
<OPERATIONAL_LOOP name="AgentMainCycle">
<DESCRIPTION>Это мой главный рабочий цикл. Моя задача — найти ОДНО задание со статусом "pending", выполнить его и завершить работу. Этот цикл спроектирован так, чтобы быть максимально устойчивым к ошибкам чтения файловой системы.</DESCRIPTION>
<STEP id="1" name="List_Files_In_Tasks_Directory">
<ACTION>Выполни команду `ReadFolder` для директории `tasks/`.</ACTION>
<ACTION>Сохрани результат в переменную `task_files_list`.</ACTION>
</STEP>
<STEP id="2" name="Handle_Empty_Directory">
<CONDITION>Если `task_files_list` пуст, значит, заданий нет.</CONDITION>
<ACTION>Заверши работу с сообщением "Директория tasks/ пуста. Заданий нет.".</ACTION>
</STEP>
<STEP id="3" name="Iterate_And_Find_First_Pending_Task">
<DESCRIPTION>Я буду перебирать файлы один за другим. Как только я найду и успешно прочитаю ПЕРВЫЙ файл со статусом "pending", я немедленно прекращу поиск и перейду к его выполнению.</DESCRIPTION>
<LOOP variable="filename" in="task_files_list">
<SUB_STEP id="3.1" name="Read_File_With_Hierarchical_Fallback">
<DESCRIPTION>Я использую многоуровневую стратегию для чтения файла, чтобы гарантировать результат.</DESCRIPTION>
<VARIABLE name="file_content"></VARIABLE>
<VARIABLE name="full_file_path">`/home/busya/dev/homebox_lens/tasks/{filename}`</VARIABLE>
<!-- ПЛАН А: Стандартный ReadFile. Самый быстрый и предпочтительный. -->
<ACTION>Попытка чтения с помощью `ReadFile tasks/{filename}`.</ACTION>
<SUCCESS_CONDITION>Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.</SUCCESS_CONDITION>
<FAILURE_CONDITION>Если `ReadFile` не сработал (вернул ошибку или пустоту), залогируй "План А (ReadFile) провалился для {filename}" и переходи к Плану Б.</FAILURE_CONDITION>
<!-- ПЛАН Б: Прямой вызов Shell cat. Более надежный, чем ReadFile. -->
<ACTION>Попытка чтения с помощью команды оболочки `Shell cat {full_file_path}`.</ACTION>
<SUCCESS_CONDITION>Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.</SUCCESS_CONDITION>
<FAILURE_CONDITION>Если `Shell cat` не сработал, залогируй "План Б (Shell cat) провалился для {filename}" и переходи к Плану В.</FAILURE_CONDITION>
<!-- ПЛАН В: Обходной путь с Wildcard. Самый надежный, но требует парсинга. -->
<ACTION>Выполни команду оболочки `Shell cat tasks/*`. Эта команда может вернуть содержимое НЕСКОЛЬКИХ файлов.</ACTION>
<SUCCESS_CONDITION>
1. Проанализируй весь вывод команды.
2. Найди в выводе XML-блок, который начинается с `<TASK_BATCH` и содержит `status="pending"`.
3. Извлеки ПОЛНОЕ содержимое этого XML-блока (от `<TASK_BATCH...>` до `</TASK_BATCH>`).
4. Если содержимое успешно извлечено, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
</SUCCESS_CONDITION>
<FAILURE_CONDITION>
<ACTION>Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю файл.".</ACTION>
<ACTION>Перейди к следующей итерации цикла (`continue`).</ACTION>
</FAILURE_CONDITION>
</SUB_STEP>
<SUB_STEP id="3.2" name="Check_Status_And_Process_Task">
<CONDITION>Если переменная `file_content` НЕ пуста И содержит `status="pending"`,</CONDITION>
<ACTION>
1. Это моя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое (`file_content`).
2. Передай управление в воркфлоу `EXECUTE_INTENT_WORKFLOW`.
3. **НЕМЕДЛЕННО ПРЕРВИ ЦИКЛ ПОИСКА (`break`).** Моя задача — выполнить только одно задание за запуск.
</ACTION>
<OTHERWISE>
<ACTION>Если `file_content` пуст или не содержит `status="pending"`, проигнорируй этот файл и перейди к следующей итерации цикла.</ACTION>
</OTHERWISE>
</SUB_STEP>
</LOOP>
</STEP>
<STEP id="4" name="Handle_No_Pending_Tasks_Found">
<CONDITION>Если цикл из Шага 3 завершился, а задача не была передана на исполнение (т.е. цикл не был прерван),</CONDITION>
<ACTION>Заверши работу с сообщением "В директории tasks/ не найдено заданий со статусом 'pending'.".</ACTION>
</STEP>
</OPERATIONAL_LOOP>
<!-- ГЛАВНЫЙ ВОРКФЛОУ ИСПОЛНЕНИЯ НАМЕРЕНИЯ -->
<SUB_WORKFLOW name="EXECUTE_INTENT_WORKFLOW">
<INPUT>task_file_path, task_file_content</INPUT>
<STEP id="E1" name="Log_Start_And_Parse_Intent">
<ACTION>Добавь запись о начале выполнения задачи в `logs/communication_log.xml`.</ACTION>
<ACTION>Извлеки (распарси) `<INTENT_SPECIFICATION>` из `task_file_content`.</ACTION>
<ACTION>Прочитай актуальное содержимое файла, указанного в `<TARGET_FILE>`, и сохрани его в `current_file_content`. Если файл не существует, `current_file_content` будет пуст.</ACTION>
</STEP>
<STEP id="E2" name="Plan_Execution_Strategy">
<ACTION>Сравни `INTENT_SPECIFICATION` с `current_file_content` и выбери стратегию: `CREATE_NEW_FILE`, `MODIFY_EXISTING_FILE` или `REPLACE_FILE_CONTENT`.</ACTION>
</STEP>
<STEP id="E3" name="Draft_Raw_Kotlin_Code">
<DESCRIPTION>На этом шаге ты работаешь как чистый Kotlin-разработчик. Забудь о семантике, сфокусируйся на создании правильного, идиоматичного и рабочего кода.</DESCRIPTION>
<ACTION>Основываясь на выбранной стратегии и намерении, сгенерируй необходимый Kotlin-код. Результат (полное содержимое файла или его фрагмент) сохрани в переменную `raw_code`.</ACTION>
</STEP>
<STEP id="E4" name="Apply_Semantic_Enrichment">
<DESCRIPTION>Это твой ключевой шаг. Ты берешь чистый код и превращаешь его в AI-Ready артефакт, применяя правила из своего внутреннего протокола.</DESCRIPTION>
<ACTION>
1. Возьми `raw_code`.
2. **Обратись к своему внутреннему `<SEMANTIC_ENRICHMENT_PROTOCOL>`.**
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`.
</ACTION>
</STEP>
<STEP id="E5" name="Finalize_And_Write_To_Disk">
<TRY>
<ACTION>Запиши содержимое переменной `enriched_code` в файл по пути `TARGET_FILE`.</ACTION>
<ACTION>Выведи `enriched_code` в stdout.</ACTION>
<SUCCESS>
<!-- Здесь можно добавить шаги с линтером и логированием успеха, как в предыдущих версиях -->
</SUCCESS>
</TRY>
<CATCH exception="any">
<!-- Логика обработки ошибок -->
</CATCH>
</STEP>
</SUB_WORKFLOW>
<!-- ###################################################################### -->
<!-- ### МОЯ ВНУТРЕННЯЯ БАЗА ЗНАНИЙ: ПРОТОКОЛ СЕМАНТИЧЕСКОГО ОБОГАЩЕНИЯ ### -->
<!-- ###################################################################### -->
<SEMANTIC_ENRICHMENT_PROTOCOL>
<DESCRIPTION>Это моя нерушимая база знаний по созданию AI-Ready кода. Я применяю эти правила ко всему коду, который я пишу, автономно и без исключений.</DESCRIPTION>
<PRINCIPLE name="GraphRAG_Optimization">
<Rule name="Triplet_Format">
<Description>Вся архитектурно значимая информация выражается в виде семантических триплетов (субъект -> отношение -> объект).</Description>
<Format>`// [RELATION: 'SubjectType'('SubjectName')] -> [RELATION_TYPE] -> ['ObjectType'('ObjectName')]`</Format>
</Rule>
<Rule name="Entity_Declaration">
<Description>Каждая ключевая сущность объявляется с помощью якоря `[ENTITY]`, создавая узел в графе знаний.</Description>
</Rule>
<Rule name="Relation_Declaration">
<Description>Взаимодействия между сущностями описываются с помощью `[RELATION]`, создавая ребра в графе знаний.</Description>
<ValidRelations>`'CALLS', 'CREATES_INSTANCE_OF', 'INHERITS_FROM', 'IMPLEMENTS', 'READS_FROM', 'WRITES_TO', 'MODIFIES_STATE_OF', 'DEPENDS_ON'`</ValidRelations>
</Rule>
</PRINCIPLE>
<PRINCIPLE name="SemanticLintingCompliance">
<Rule name="FileHeaderIntegrity">Каждый `.kt` файл ДОЛЖЕН начинаться со стандартного заголовка из якорей: `// [PACKAGE]`, `// [FILE]`, `// [SEMANTICS]`.</Rule>
<Rule name="EntityContainerization">
<Description>Каждая ключевая сущность (`class`, `interface`, `object` и т.д.) ДОЛЖНА быть обернута в семантический контейнер. Контейнер состоит из открывающего блока разметки (`[ENTITY]`, `[RELATION]...`) ПЕРЕД сущностью и закрывающего якоря (`[END_ENTITY: ...]`) ПОСЛЕ нее.</Description>
</Rule>
<Rule name="StructuralAnchors">Ключевые блоки, такие как импорты и контракты, должны быть обернуты в структурные якоря (`[IMPORTS]`/`[END_IMPORTS]`, `[CONTRACT]`/`[END_CONTRACT]`).</Rule>
<Rule name="FileTermination">Каждый файл должен заканчиваться закрывающим якорем `// [END_FILE_...]`.</Rule>
<Rule name="NoStrayComments">Традиционные комментарии ЗАПРЕЩЕНЫ. Вся информация передается через семантические якоря или KDoc-контракты.</Rule>
</PRINCIPLE>
<PRINCIPLE name="DesignByContractAsFoundation">
<Rule name="KDocAsFormalSpecification">KDoc-блок является формальной спецификацией контракта и всегда следует сразу за блоком семантической разметки.</Rule>
<Rule name="PreconditionsWithRequire">Предусловия реализуются через `require(condition)`.</Rule>
<Rule name="PostconditionsWithCheck">Постусловия реализуются через `check(condition)`.</Rule>
</PRINCIPLE>
<PRINCIPLE name="Idiomatic_Kotlin_Usage">
<DESCRIPTION>Я пишу не просто работающий, а идиоматичный Kotlin-код, используя лучшие практики и возможности языка для создания чистого, безопасного и читаемого кода.</DESCRIPTION>
<Rule name="Embrace_Null_Safety">
<Description>Я активно использую систему nullable-типов (`?`) для предотвращения `NullPointerException`. Я строго избегаю оператора двойного восклицания (`!!`). Для безопасной работы с nullable-значениями я применяю `?.let`, оператор Элвиса `?:` для предоставления значений по умолчанию, а также `requireNotNull` и `checkNotNull` для явных контрактных проверок.</Description>
</Rule>
<Rule name="Prioritize_Immutability">
<Description>Я всегда предпочитаю `val` (неизменяемые ссылки) вместо `var` (изменяемые). По умолчанию я использую иммутабельные коллекции (`listOf`, `setOf`, `mapOf`). Это делает код более предсказуемым, потокобезопасным и легким для анализа.</Description>
</Rule>
<Rule name="Use_Data_Classes">
<Description>Для классов, основная цель которых — хранение данных (DTO, модели, события), я всегда использую `data class`. Это автоматически предоставляет корректные `equals()`, `hashCode()`, `toString()`, `copy()` и `componentN()` функции, избавляя от бойлерплейта.</Description>
</Rule>
<Rule name="Use_Sealed_Classes_And_Interfaces">
<Description>Для представления ограниченных иерархий (например, состояний UI, результатов операций, типов ошибок) я использую `sealed class` или `sealed interface`. Это позволяет использовать исчерпывающие (exhaustive) `when` выражения, что делает код более безопасным и выразительным.</Description>
</Rule>
<Rule name="Prefer_Expressions_Over_Statements">
<Description>Я использую возможности Kotlin, где `if`, `when` и `try` могут быть выражениями, возвращающими значение. Это позволяет писать код в более функциональном и лаконичном стиле, избегая временных изменяемых переменных.</Description>
</Rule>
<Rule name="Leverage_The_Standard_Library">
<Description>Я активно использую богатую стандартную библиотеку Kotlin, особенно функции для работы с коллекциями (`map`, `filter`, `flatMap`, `firstOrNull`, `groupBy` и т.д.). Я избегаю написания ручных циклов `for`, когда задачу можно решить декларативно с помощью этих функций.</Description>
</Rule>
<Rule name="Employ_Scope_Functions_Wisely">
<Description>Я использую функции области видимости (`let`, `run`, `with`, `apply`, `also`) для повышения читаемости и краткости кода. Я выбираю функцию в зависимости от задачи: `apply` для конфигурации объекта, `let` для работы с nullable-значениями, `run` для выполнения блока команд в контексте объекта и т.д.</Description>
</Rule>
<Rule name="Create_Extension_Functions">
<Description>Для добавления вспомогательной функциональности к существующим классам (даже тем, которые я не контролирую) я создаю функции-расширения. Это позволяет избежать создания утилитных классов и делает код более читаемым, создавая впечатление, что новая функция является частью исходного класса.</Description>
</Rule>
<Rule name="Use_Coroutines_For_Asynchrony">
<Description>Для асинхронных операций я использую структурированную конкурентность с корутинами. Я помечаю I/O-bound или CPU-bound операции как `suspend`. Для асинхронных потоков данных я использую `Flow`. Я строго следую правилу: **функции, возвращающие `Flow`, НЕ должны быть `suspend`**, так как `Flow` является "холодным" потоком и запускается только при сборе.</Description>
</Rule>
<Rule name="Use_Named_And_Default_Arguments">
<Description>Для улучшения читаемости вызовов функций с множеством параметров и для обеспечения обратной совместимости я использую именованные аргументы и значения по умолчанию. Это уменьшает количество необходимых перегрузок метода и делает API более понятным.</Description>
</Rule>
</PRINCIPLE>
</SEMANTIC_ENRICHMENT_PROTOCOL>
<LOGGING_PROTOCOL>
<LOG_ENTRY timestamp="{ISO_DATETIME}">
<TASK_FILE>{имя_файлаадания}</TASK_FILE>
<FULL_PATH>{полный_абсолютный_путь_к_файлуадания}</FULL_PATH> <!-- Добавлено -->
<STATUS>STARTED | COMPLETED | FAILED</STATUS>
<MESSAGE>еловекочитаемое_сообщение}</MESSAGE>
<DETAILS>
<!-- При успехе: что было сделано. При провале: причина, вывод команды. -->
</DETAILS>
</LOG_ENTRY>
</LOGGING_PROTOCOL>
</AI_AGENT_DEVELOPER_PROTOCOL>

583
PROJECT_SPECIFICATION.xml Normal file
View File

@@ -0,0 +1,583 @@
<?xml version="1.0" encoding="UTF-8"?>
<PROJECT_SPECIFICATION>
<PROJECT_INFO>
<name>Homebox Lens</name>
<description>Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.</description>
</PROJECT_INFO>
<TECHNICAL_DECISIONS>
<DECISION id="tech_logging" status="implemented">
<summary>Библиотека логирования</summary>
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
<EXAMPLE lang="kotlin">
<summary>Пример корректного использования Timber</summary>
<code>
<![CDATA[
// Правильно: Прямой вызов статических методов Timber.
// Для информационных сообщений (INFO):
Timber.i("User logged in successfully. UserId: %s", userId)
// Для отладочных сообщений (DEBUG):
Timber.d("Starting network request to /items")
// Для ошибок (ERROR):
try {
// какая-то операция, которая может провалиться
} catch (e: Exception) {
Timber.e(e, "Failed to fetch user profile.")
}
// НЕПРАВИЛЬНО: Попытка создать экземпляр логгера.
// val logger = Timber.tag("MyScreen") // Избегать этого!
// logger.info("Some message") // Этот метод не существует в API Timber.
]]>
</code>
</EXAMPLE>
</DECISION>
<DECISION id="tech_i18n" status="implemented">
<summary>Интернационализация (Мультиязычность)</summary>
<description>
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
Реализация будет основана на стандартном механизме ресурсов Android.
- Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
- Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
- Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
</description>
</DECISION>
<DECISION id="tech_ui_framework" status="implemented">
<summary>UI Framework</summary>
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
</DECISION>
<DECISION id="tech_di" status="implemented">
<summary>Внедрение зависимостей (Dependency Injection)</summary>
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
</DECISION>
<DECISION id="tech_navigation" status="implemented">
<summary>Навигация</summary>
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
</DECISION>
<DECISION id="tech_async" status="implemented">
<summary>Асинхронные операции</summary>
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
</DECISION>
<DECISION id="tech_networking" status="implemented">
<summary>Сетевое взаимодействие</summary>
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
</DECISION>
<DECISION id="tech_database" status="implemented">
<summary>Локальное хранилище</summary>
<description>Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.</description>
</DECISION>
</TECHNICAL_DECISIONS>
<SECURITY_SPEC>
<Description>Спецификация безопасности проекта.</Description>
<PRINCIPLE>Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина.</PRINCIPLE>
<RULE name="AuthHandling">Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять.</RULE>
<RULE name="DataEncryption">Локальные данные (credentials) шифровать с помощью Android KeyStore.</RULE>
</SECURITY_SPEC>
<ERROR_HANDLING>
<Description>Спецификация обработки ошибок.</Description>
<PRINCIPLE>Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog.</PRINCIPLE>
<SCENARIO name="NetworkFailure">При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry.</SCENARIO>
<SCENARIO name="ServerError">Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body.</SCENARIO>
<SCENARIO name="ValidationError">Использовать require/check для контрактов, логировать и показывать toast.</SCENARIO>
</ERROR_HANDLING>
<DATA_MODELS>
<MODEL id="model_item" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Item.kt" status="implemented">
<summary>Модель инвентарного товара.</summary>
<description>Содержит поля: id, name, description, quantity, location, labels, customFields.</description>
</MODEL>
<MODEL id="model_label" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Label.kt" status="implemented">
<summary>Модель метки.</summary>
<description>Содержит поля: id, name, color.</description>
</MODEL>
<MODEL id="model_location" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Location.kt" status="implemented">
<summary>Модель местоположения.</summary>
<description>Содержит поля: id, name, parentLocation.</description>
</MODEL>
<MODEL id="model_statistics" file_ref="domain/src/main/java/com/homebox/lens/domain/model/Statistics.kt" status="implemented">
<summary>Модель статистики инвентаря.</summary>
<description>Содержит поля: totalItems, totalValue, locationsCount, labelsCount.</description>
</MODEL>
</DATA_MODELS>
<FEATURES>
<FEATURE id="feat_dashboard" status="implemented">
<summary>Экран панели управления</summary>
<description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
<UI_COMPONENT ref_id="screen_dashboard" />
<FUNCTIONALITY>
<FUNCTION id="func_get_stats" status="implemented">
<summary>Получение и отображение статистики</summary>
<description>Получает общую статистику по инвентарю с сервера.</description>
<precondition>Пользователь аутентифицирован; сеть доступна.</precondition>
<postcondition>Возвращает объект Statistics; данные кэшированы локально.</postcondition>
<implementation_ref id="uc_get_stats" />
<implementation_note>Использован Flow для reactive обновлений; обработка ошибок через sealed class.</implementation_note>
</FUNCTION>
<FUNCTION id="func_get_recent_items" status="implemented">
<summary>Получение и отображение недавно добавленных товаров</summary>
<description>Получает список последних N добавленных товаров из локальной базы данных.</description>
<precondition>Пользователь аутентифицирован.</precondition>
<postcondition>Возвращает Flow со списком ItemSummary; список отсортирован по дате создания.</postcondition>
<implementation_ref id="uc_get_recent_items" />
<implementation_note>Данные берутся из локального кэша (Room) для быстрого отображения.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_inventory_list" status="implemented">
<summary>Экран списка инвентаря</summary>
<description>Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.</description>
<UI_COMPONENT ref_id="screen_inventory_list" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items" status="implemented">
<summary>Поиск и фильтрация товаров</summary>
<description>Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.</description>
<precondition>Запрос не пустой; параметры пагинации валидны (page >= 1).</precondition>
<postcondition>Возвращает список Item с пагинацией; результаты отсортированы по релевантности.</postcondition>
<implementation_ref id="uc_search_items" />
<implementation_note>Поддержка фильтров по location/label; кэширование результатов для оффлайн.</implementation_note>
</FUNCTION>
<FUNCTION id="func_sync_inventory" status="implemented">
<summary>Синхронизация инвентаря</summary>
<description>Выполняет полную синхронизацию локального кэша инвентаря с сервером.</description>
<precondition>Сеть доступна; пользователь аутентифицирован.</precondition>
<postcondition>Локальная БД обновлена; возвращает success/failure.</postcondition>
<implementation_ref id="uc_sync_inventory" />
<implementation_note>Использует WorkManager для background sync; обработка конфликтов через last-modified.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_item_details" status="implemented">
<summary>Экран сведений о товаре</summary>
<description>Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.</description>
<UI_COMPONENT ref_id="screen_item_details" />
<FUNCTIONALITY>
<FUNCTION id="func_get_item_details" status="implemented">
<summary>Получение сведений о товаре</summary>
<description>Получает полные сведения о конкретном товаре из репозитория.</description>
<precondition>Item ID валиден и существует.</precondition>
<postcondition>Возвращает полный объект Item с attachments.</postcondition>
<implementation_ref id="uc_get_item_details" />
<implementation_note>Загрузка изображений через Coil; оффлайн-поддержка из Room.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_item_management" status="implemented">
<summary>Создание/редактирование/удаление товаров</summary>
<description>Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.</description>
<UI_COMPONENT ref_id="screen_item_edit" />
<FUNCTIONALITY>
<FUNCTION id="func_create_item" status="implemented">
<summary>Создать товар</summary>
<description>Создает новый инвентарный товар на сервере.</description>
<precondition>Все обязательные поля (name, quantity) заполнены; данные валидны.</precondition>
<postcondition>Новый Item сохранен на сервере; ID возвращен.</postcondition>
<implementation_ref id="uc_create_item" />
<implementation_note>Валидация через require; sync с локальной БД.</implementation_note>
</FUNCTION>
<FUNCTION id="func_update_item" status="implemented">
<summary>Обновить товар</summary>
<description>Обновляет существующий инвентарный товар на сервере.</description>
<precondition>Item ID существует; изменения валидны.</precondition>
<postcondition>Item обновлен; версия инкрементирована.</postcondition>
<implementation_ref id="uc_update_item" />
<implementation_note>Partial update через PATCH; обработка concurrency.</implementation_note>
</FUNCTION>
<FUNCTION id="func_delete_item" status="implemented">
<summary>Удалить товар</summary>
<description>Удаляет инвентарный товар с сервера.</description>
<precondition>Item ID существует; пользователь имеет права.</precondition>
<postcondition>Item удален; связанные ресурсы (attachments) очищены.</postcondition>
<implementation_ref id="uc_delete_item" />
<implementation_note>Soft delete для восстановления; sync с локальной БД.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_labels_locations" status="implemented">
<summary>Управление метками и местоположениями</summary>
<description>Позволяет пользователям просматривать списки всех доступных меток и местоположений.</description>
<UI_COMPONENT ref_id="screen_labels_list" />
<UI_COMPONENT ref_id="screen_locations_list" />
<FUNCTIONALITY>
<FUNCTION id="func_get_all_labels" status="implemented">
<summary>Получить все метки</summary>
<description>Получает список всех меток из репозитория.</description>
<precondition>Сеть доступна или кэш существует.</precondition>
<postcondition>Возвращает список Label; отсортирован по name.</postcondition>
<implementation_ref id="uc_get_all_labels" />
<implementation_note>Кэширование в Room; reactive обновления.</implementation_note>
</FUNCTION>
<FUNCTION id="func_get_all_locations" status="implemented">
<summary>Получить все местоположения</summary>
<description>Получает список всех местоположений из репозитория.</description>
<precondition>Сеть доступна или кэш существует.</precondition>
<postcondition>Возвращает список Location; иерархическая структура сохранена.</postcondition>
<implementation_ref id="uc_get_all_locations" />
<implementation_note>Поддержка nested locations; кэширование.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
<FEATURE id="feat_search" status="implemented">
<summary>Экран поиска</summary>
<description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
<UI_COMPONENT ref_id="screen_search" />
<FUNCTIONALITY>
<FUNCTION id="func_search_items_dedicated" status="implemented">
<summary>Поиск со специального экрана</summary>
<description>Использует ту же функцию поиска, но со специального экрана.</description>
<precondition>Запрос не пустой.</precondition>
<postcondition>Возвращает результаты поиска; UI обновлен.</postcondition>
<implementation_ref id="uc_search_items" />
<implementation_note>Интеграция с SearchView; debounce для запросов.</implementation_note>
</FUNCTION>
</FUNCTIONALITY>
</FEATURE>
</FEATURES>
<UI_SPECIFICATIONS>
<SCREEN id="screen_dashboard" status="implemented">
<summary>Главный экран "Панель управления"</summary>
<description>
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Основная область контента. Содержит несколько информационных блоков.</description>
<SUB_COMPONENT type="Section" title="Быстрая статистика">
<description>Сетка из 2x2 карточек, отображающих ключевые метрики.</description>
<ELEMENT type="Card" name="Общая стоимость" />
<ELEMENT type="Card" name="Всего вещей" />
<ELEMENT type="Card" name="Общее количество местоположений" />
<ELEMENT type="Card" name="Всего меток" />
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Недавно добавлено">
<description>Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Места хранения">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Метки">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.</description>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton_or_PrimaryButton" icon="add">
<description>
Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на чип местоположения/метки</action>
<reaction>Навигация на экран списка инвентаря с фильтром.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на кнопку "Создать"</action>
<reaction>Открытие экрана редактирования нового товара.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_locations_list" status="implemented">
<summary>Экран "Локации"</summary>
<description>
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения, аналогичная экрану "Панель управления".</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Общее боковое меню навигации.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="Header" title="Локации">
<description>Заголовок экрана, расположенный вверху основной области контента.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="LocationsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка локаций</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания нового местоположения.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_labels_list" status="implemented">
<summary>Экран "Метки"</summary>
<description>
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="List" name="LabelsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка меток</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_inventory_list" status="implemented">
<summary>Экран "Список инвентаря"</summary>
<description>
Отображает список всех инвентарных позиций с возможностью поиска, фильтрации и пагинации. Интегрирован в навигацию.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель с поиском и фильтрами.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Прокручиваемый список товаров.</description>
<SUB_COMPONENT type="List" name="InventoryList">
<description>LazyColumn с карточками товаров (name, quantity, location).</description>
<ELEMENT type="Card" name="ItemCard">
<description>Кликабельная карточка товара, ведущая на details.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="sync">
<description>Кнопка для синхронизации инвентаря.</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Ввод в поиск</action>
<reaction>Обновление списка с debounce.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на товар</action>
<reaction>Навигация на screen_item_details.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_item_details" status="implemented">
<summary>Экран "Сведения о товаре"</summary>
<description>
Показывает детальную информацию о товаре, включая изображения и custom fields.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С кнопками edit/delete.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<SUB_COMPONENT type="ImageCarousel" name="Images">
<description>Карусель изображений.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="DetailsSection" title="Описание">
<description>Текст description.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="FieldsGrid" name="CustomFields">
<description>Сетка custom полей.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие edit</action>
<reaction>Навигация на screen_item_edit.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие delete</action>
<reaction>Подтверждение и вызов func_delete_item.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_item_edit" status="implemented">
<summary>Экран "Редактирование товара"</summary>
<description>
Форма для создания/обновления товара с полями name, description, quantity, etc.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С кнопкой save.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<SUB_COMPONENT type="TextField" name="Name">
<description>Поле ввода имени.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Dropdown" name="Location">
<description>Выбор местоположения.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="ChipGroup" name="Labels">
<description>Выбор меток.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="ImagePicker" name="Images">
<description>Добавление изображений.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие save</action>
<reaction>Валидация и вызов func_create_item или func_update_item.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<SCREEN id="screen_search" status="implemented">
<summary>Экран "Поиск"</summary>
<description>
Специализированный экран для поиска с расширенными фильтрами.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>С поисковой строкой.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<SUB_COMPONENT type="FilterSection" name="Filters">
<description>Чипы для фильтров (location, label).</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="SearchResults">
<description>LazyColumn результатов.</description>
</SUB_COMPONENT>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Изменение запроса/фильтров</action>
<reaction>Обновление результатов.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
</UI_SPECIFICATIONS>
<ICONOGRAPHY_GUIDE id="iconography_guide">
<summary>Руководство по использованию иконок</summary>
<description>
Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled'
для использования в приложении. Для устаревших иконок указаны актуальные замены.
</description>
<ICON name="AccountBox" path="Icons.Filled.AccountBox" />
<ICON name="AccountCircle" path="Icons.Filled.AccountCircle" />
<ICON name="Add" path="Icons.Filled.Add" />
<ICON name="AddCircle" path="Icons.Filled.AddCircle" />
<ICON name="ArrowBack" path="Icons.AutoMirrored.Filled.ArrowBack" note="Использовать AutoMirrored версию" />
<ICON name="ArrowDropDown" path="Icons.Filled.ArrowDropDown" />
<ICON name="ArrowForward" path="Icons.AutoMirrored.Filled.ArrowForward" note="Использовать AutoMirrored версию" />
<ICON name="Build" path="Icons.Filled.Build" />
<ICON name="Call" path="Icons.Filled.Call" />
<ICON name="Check" path="Icons.Filled.Check" />
<ICON name="CheckCircle" path="Icons.Filled.CheckCircle" />
<ICON name="Clear" path="Icons.Filled.Clear" />
<ICON name="Close" path="Icons.Filled.Close" />
<ICON name="Create" path="Icons.Filled.Create" />
<ICON name="DateRange" path="Icons.Filled.DateRange" />
<ICON name="Delete" path="Icons.Filled.Delete" />
<ICON name="Done" path="Icons.Filled.Done" />
<ICON name="Edit" path="Icons.Filled.Edit" />
<ICON name="Email" path="Icons.Filled.Email" />
<ICON name="ExitToApp" path="Icons.AutoMirrored.Filled.ExitToApp" note="Использовать AutoMirrored версию" />
<ICON name="Face" path="Icons.Filled.Face" />
<ICON name="Favorite" path="Icons.Filled.Favorite" />
<ICON name="FavoriteBorder" path="Icons.Filled.FavoriteBorder" />
<ICON name="Home" path="Icons.Filled.Home" />
<ICON name="Info" path="Icons.AutoMirrored.Filled.Info" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowDown" path="Icons.Filled.KeyboardArrowDown" />
<ICON name="KeyboardArrowLeft" path="Icons.AutoMirrored.Filled.KeyboardArrowLeft" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowRight" path="Icons.AutoMirrored.Filled.KeyboardArrowRight" note="Использовать AutoMirrored версию" />
<ICON name="KeyboardArrowUp" path="Icons.Filled.KeyboardArrowUp" />
<ICON name="Label" path="Icons.AutoMirrored.Filled.Label" note="Использовать AutoMirrored версию" />
<ICON name="List" path="Icons.AutoMirrored.Filled.List" note="Использовать AutoMirrored версию" />
<ICON name="LocationOn" path="Icons.Filled.LocationOn" />
<ICON name="Lock" path="Icons.Filled.Lock" />
<ICON name="MailOutline" path="Icons.Filled.MailOutline" />
<ICON name="Menu" path="Icons.Filled.Menu" />
<ICON name="MoreVert" path="Icons.Filled.MoreVert" />
<ICON name="Notifications" path="Icons.Filled.Notifications" />
<ICON name="Person" path="Icons.Filled.Person" />
<ICON name="Phone" path="Icons.Filled.Phone" />
<ICON name="Place" path="Icons.Filled.Place" />
<ICON name="PlayArrow" path="Icons.Filled.PlayArrow" />
<ICON name="Refresh" path="Icons.Filled.Refresh" />
<ICON name="Search" path="Icons.Filled.Search" />
<ICON name="Send" path="Icons.AutoMirrored.Filled.Send" note="Использовать AutoMirrored версию" />
<ICON name="Settings" path="Icons.Filled.Settings" />
<ICON name="Share" path="Icons.Filled.Share" />
<ICON name="ShoppingCart" path="Icons.Filled.ShoppingCart" />
<ICON name="Star" path="Icons.Filled.Star" />
<ICON name="ThumbUp" path="Icons.Filled.ThumbUp" />
<ICON name="Warning" path="Icons.Filled.Warning" />
</ICONOGRAPHY_GUIDE>
<IMPLEMENTATION_MAP>
<!-- Use Cases -->
<USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" />
<USE_CASE id="uc_search_items" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" />
<USE_CASE id="uc_sync_inventory" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" />
<USE_CASE id="uc_get_item_details" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" />
<USE_CASE id="uc_create_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" />
<USE_CASE id="uc_update_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" />
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
<!-- UI Screens -->
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
<UI_SCREEN id="screen_inventory_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" />
<UI_SCREEN id="screen_item_details" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" />
<UI_SCREEN id="screen_item_edit" file_ref="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" />
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
</IMPLEMENTATION_MAP>
</PROJECT_SPECIFICATION>

View File

@@ -6,6 +6,7 @@ plugins {
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-kapt") id("kotlin-kapt")
// id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
} }
android { android {
@@ -30,7 +31,7 @@ android {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro",
) )
} }
} }
@@ -76,9 +77,7 @@ dependencies {
implementation(Libs.navigationCompose) implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose) implementation(Libs.hiltNavigationCompose)
// ktlint(project(":data:semantic-ktlint-rules"))
// [DEPENDENCY] DI (Hilt) // [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid) implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler) kapt(Libs.hiltCompiler)

View File

@@ -1,8 +1,10 @@
// [PACKAGE] com.homebox.lens // [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt // [FILE] MainActivity.kt
// [SEMANTICS] android, activity, compose, hilt
package com.homebox.lens package com.homebox.lens
// [IMPORTS]
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -16,14 +18,24 @@ import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph import com.homebox.lens.navigation.NavGraph
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.ui.theme.HomeboxLensTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
// [END_IMPORTS]
// [CONTRACT] // [CONTRACT]
// [ENTITY: Activity('MainActivity')]
// [RELATION: Activity('MainActivity') -> [INHERITS_FROM] -> Class('ComponentActivity')]
// [RELATION: Activity('MainActivity') -> [DEPENDS_ON] -> Annotation('AndroidEntryPoint')]
/** /**
* [ENTITY: Activity('MainActivity')] * [ENTITY: Activity('MainActivity')]
* [PURPOSE] Главная и единственная Activity в приложении. * [PURPOSE] Главная и единственная Activity в приложении.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { 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] // [LIFECYCLE]
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -32,24 +44,34 @@ class MainActivity : ComponentActivity() {
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background,
) { ) {
NavGraph() NavGraph()
} }
} }
} }
} }
// [END_ENTITY: Function('onCreate')]
} }
// [END_ENTITY: Activity('MainActivity')]
// [HELPER] // [ENTITY: Function('Greeting')]
// [RELATION: Function('Greeting') -> [CALLS] -> Function('Text')]
@Composable @Composable
fun Greeting(name: String, modifier: Modifier = Modifier) { fun Greeting(
name: String,
modifier: Modifier = Modifier,
) {
Text( Text(
text = "Hello $name!", text = "Hello $name!",
modifier = modifier modifier = modifier,
) )
} }
// [END_ENTITY: Function('Greeting')]
// [ENTITY: Function('GreetingPreview')]
// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('Greeting')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
@@ -58,5 +80,7 @@ fun GreetingPreview() {
Greeting("Android") Greeting("Android")
} }
} }
// [END_ENTITY: Function('GreetingPreview')]
// [END_CONTRACT]
// [END_FILE_MainActivity.kt] // [END_FILE_MainActivity.kt]

View File

@@ -1,20 +1,28 @@
// [PACKAGE] com.homebox.lens // [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt // [FILE] MainApplication.kt
// [SEMANTICS] android, application, hilt, timber
package com.homebox.lens package com.homebox.lens
// [IMPORTS]
import android.app.Application import android.app.Application
import com.homebox.lens.BuildConfig
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS]
// [CONTRACT] // [CONTRACT]
// [ENTITY: Application('MainApplication')]
// [RELATION: Application('MainApplication') -> [INHERITS_FROM] -> Class('Application')]
// [RELATION: Application('MainApplication') -> [DEPENDS_ON] -> Annotation('HiltAndroidApp')]
/** /**
* [ENTITY: Application('MainApplication')] * [ENTITY: Application('MainApplication')]
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber. * [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
*/ */
@HiltAndroidApp @HiltAndroidApp
class MainApplication : Application() { class MainApplication : Application() {
// [ENTITY: Function('onCreate')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('Timber.plant')]
// [LIFECYCLE] // [LIFECYCLE]
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@@ -23,6 +31,9 @@ class MainApplication : Application() {
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
} }
} }
// [END_ENTITY: Function('onCreate')]
} }
// [END_ENTITY: Application('MainApplication')]
// [END_CONTRACT]
// [END_FILE_MainApplication.kt] // [END_FILE_MainApplication.kt]

View File

@@ -13,17 +13,44 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController 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.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen 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.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsViewModel
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen 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.locationedit.LocationEditScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.search.SearchScreen 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 com.homebox.lens.ui.screen.setup.SetupScreen
import timber.log.Timber
// [END_IMPORTS]
// [CORE-LOGIC] // [CONTRACT]
// [ENTITY: Function('NavGraph')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('rememberNavController')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('currentBackStackEntryAsState')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('remember')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('NavGraph') -> [CREATES_INSTANCE_OF] -> Class('NavigationActions')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('NavHost')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('composable')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SetupScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('DashboardScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('InventoryListScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemDetailsScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemEditScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LabelsListScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationsListScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationEditScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SearchScreen')]
/** /**
* [CONTRACT] * [CONTRACT]
* Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. * Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
@@ -33,24 +60,23 @@ import com.homebox.lens.ui.screen.setup.SetupScreen
* @invariant Стартовый экран - `Screen.Setup`. * @invariant Стартовый экран - `Screen.Setup`.
*/ */
@Composable @Composable
fun NavGraph( fun NavGraph(navController: NavHostController = rememberNavController()) {
navController: NavHostController = rememberNavController()
) {
// [STATE] // [STATE]
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
// [HELPER] // [HELPER]
val navigationActions = remember(navController) { val navigationActions =
remember(navController) {
NavigationActions(navController) NavigationActions(navController)
} }
// [ACTION] // [ACTION]
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Setup.route startDestination = Screen.Setup.route,
) { ) {
// [COMPOSABLE_SETUP] // [ENTITY: Composable('Screen.Setup.route')]
composable(route = Screen.Setup.route) { composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = { SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) { navController.navigate(Screen.Dashboard.route) {
@@ -58,39 +84,75 @@ fun NavGraph(
} }
}) })
} }
// [COMPOSABLE_DASHBOARD] // [END_ENTITY: Composable('Screen.Setup.route')]
// [ENTITY: Composable('Screen.Dashboard.route')]
composable(route = Screen.Dashboard.route) { composable(route = Screen.Dashboard.route) {
DashboardScreen( DashboardScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions,
) )
} }
// [COMPOSABLE_INVENTORY_LIST] // [END_ENTITY: Composable('Screen.Dashboard.route')]
composable(route = Screen.InventoryList.route) { // [ENTITY: Composable('Screen.InventoryList.route')]
composable(route = Screen.InventoryList.route) { backStackEntry ->
val viewModel: InventoryListViewModel = hiltViewModel(backStackEntry)
InventoryListScreen( InventoryListScreen(
currentRoute = currentRoute, onItemClick = { item ->
navigationActions = navigationActions // TODO: Navigate to item details
Timber.i("[UI] Item clicked: ${item.name}")
},
onNavigateBack = {
navController.popBackStack()
}
) )
} }
// [COMPOSABLE_ITEM_DETAILS] // [END_ENTITY: Composable('Screen.InventoryList.route')]
composable(route = Screen.ItemDetails.route) { // [ENTITY: Composable('Screen.ItemDetails.route')]
composable(route = Screen.ItemDetails.route) { backStackEntry ->
val viewModel: ItemDetailsViewModel = hiltViewModel(backStackEntry)
ItemDetailsScreen( ItemDetailsScreen(
currentRoute = currentRoute, onNavigateBack = {
navigationActions = navigationActions navController.popBackStack()
},
onEditClick = { itemId ->
// TODO: Navigate to item edit screen
Timber.i("[UI] Edit item clicked: $itemId")
}
) )
} }
// [COMPOSABLE_ITEM_EDIT] // [END_ENTITY: Composable('Screen.ItemDetails.route')]
composable(route = Screen.ItemEdit.route) { // [ENTITY: Composable('Screen.ItemEdit.route')]
composable(route = Screen.ItemEdit.route) { backStackEntry ->
val viewModel: ItemEditViewModel = hiltViewModel(backStackEntry)
ItemEditScreen( ItemEditScreen(
currentRoute = currentRoute, onNavigateBack = {
navigationActions = navigationActions navController.popBackStack()
}
) )
} }
// [COMPOSABLE_LABELS_LIST] // [END_ENTITY: Composable('Screen.ItemEdit.route')]
composable(Screen.LabelsList.route) { // [ENTITY: 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.i("[UI] Label clicked: ${label.name}")
},
onAddClick = {
// TODO: Implement navigation to add new label screen
Timber.i("[UI] Add new label clicked")
},
onNavigateBack = {
navController.popBackStack()
} }
// [COMPOSABLE_LOCATIONS_LIST] )
}
// [END_ENTITY: Composable('Screen.LabelsList.route')]
// [ENTITY: Composable('Screen.LocationsList.route')]
composable(route = Screen.LocationsList.route) { composable(route = Screen.LocationsList.route) {
LocationsListScreen( LocationsListScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
@@ -101,24 +163,34 @@ fun NavGraph(
}, },
onAddNewLocationClick = { onAddNewLocationClick = {
navController.navigate(Screen.LocationEdit.createRoute("new")) navController.navigate(Screen.LocationEdit.createRoute("new"))
} },
) )
} }
// [COMPOSABLE_LOCATION_EDIT] // [END_ENTITY: Composable('Screen.LocationsList.route')]
// [ENTITY: Composable('Screen.LocationEdit.route')]
composable(route = Screen.LocationEdit.route) { backStackEntry -> composable(route = Screen.LocationEdit.route) { backStackEntry ->
val locationId = backStackEntry.arguments?.getString("locationId") val locationId = backStackEntry.arguments?.getString("locationId")
LocationEditScreen( LocationEditScreen(
locationId = locationId locationId = locationId,
) )
} }
// [COMPOSABLE_SEARCH] // [END_ENTITY: Composable('Screen.LocationEdit.route')]
composable(route = Screen.Search.route) { // [ENTITY: Composable('Screen.Search.route')]
composable(route = Screen.Search.route) { backStackEntry ->
val viewModel: SearchViewModel = hiltViewModel(backStackEntry)
SearchScreen( SearchScreen(
currentRoute = currentRoute, onNavigateBack = {
navigationActions = navigationActions navController.popBackStack()
},
onItemClick = { item ->
// TODO: Navigate to item details
Timber.i("[UI] Search result item clicked: ${item.name}")
}
) )
} }
// [END_ENTITY: Composable('Screen.Search.route')]
} }
// [END_FUNCTION_NavGraph]
} }
// [END_ENTITY: Function('NavGraph')]
// [END_CONTRACT]
// [END_FILE_NavGraph.kt] // [END_FILE_NavGraph.kt]

View File

@@ -2,70 +2,122 @@
// [FILE] NavigationActions.kt // [FILE] NavigationActions.kt
// [SEMANTICS] navigation, controller, actions // [SEMANTICS] navigation, controller, actions
package com.homebox.lens.navigation package com.homebox.lens.navigation
// [IMPORTS]
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
// [CORE-LOGIC] // [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Class('NavigationActions')]
// [RELATION: Class('NavigationActions') -> [DEPENDS_ON] -> Class('NavHostController')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий. * @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
@param navController Контроллер Jetpack Navigation. * @param navController Контроллер Jetpack Navigation.
@invariant Все навигационные действия должны использовать предоставленный navController. * @invariant Все навигационные действия должны использовать предоставленный navController.
*/ */
class NavigationActions(private val navController: NavHostController) { 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] * [CONTRACT]
@summary Навигация на главный экран. * @summary Навигация на главный экран.
@sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
*/ */
fun navigateToDashboard() { fun navigateToDashboard() {
navController.navigate(Screen.Dashboard.route) { navController.navigate(Screen.Dashboard.route) {
// Используем popUpTo для удаления всех экранов до dashboard из back stack // Используем popUpTo для удаления всех экранов до dashboard из back stack
// Это предотвращает создание большой стопки экранов при навигации через drawer // Это предотвращает создание большой стопки экранов при навигации через drawer
popUpTo(navController.graph.startDestinationId) popUpTo(navController.graph.startDestinationId)
launchSingleTop = true 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] // [ACTION]
fun navigateToLocations() { fun navigateToLocations() {
navController.navigate(Screen.LocationsList.route) { navController.navigate(Screen.LocationsList.route) {
launchSingleTop = true 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] // [ACTION]
fun navigateToLabels() { fun navigateToLabels() {
navController.navigate(Screen.LabelsList.route) { navController.navigate(Screen.LabelsList.route) {
launchSingleTop = true 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] // [ACTION]
fun navigateToSearch() { fun navigateToSearch() {
navController.navigate(Screen.Search.route) { navController.navigate(Screen.Search.route) {
launchSingleTop = true 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] // [ACTION]
fun navigateToInventoryListWithLabel(labelId: String) { fun navigateToInventoryListWithLabel(labelId: String) {
val route = Screen.InventoryList.withFilter("label", labelId) val route = Screen.InventoryList.withFilter("label", labelId)
navController.navigate(route) 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] // [ACTION]
fun navigateToInventoryListWithLocation(locationId: String) { fun navigateToInventoryListWithLocation(locationId: String) {
val route = Screen.InventoryList.withFilter("location", locationId) val route = Screen.InventoryList.withFilter("location", locationId)
navController.navigate(route) 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] // [ACTION]
fun navigateToCreateItem() { fun navigateToCreateItem() {
navController.navigate(Screen.ItemEdit.createRoute("new")) 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] // [ACTION]
fun navigateToLogout() { fun navigateToLogout() {
navController.navigate(Screen.Setup.route) { navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true } popUpTo(Screen.Dashboard.route) { inclusive = true }
} }
} }
// [END_ENTITY: Function('navigateToLogout')]
// [ENTITY: Function('navigateBack')]
// [RELATION: Function('navigateBack') -> [CALLS] -> Function('navController.popBackStack')]
// [ACTION] // [ACTION]
fun navigateBack() { fun navigateBack() {
navController.popBackStack() navController.popBackStack()
} }
// [END_ENTITY: Function('navigateBack')]
} }
// [END_ENTITY: Class('NavigationActions')]
// [END_CONTRACT]
// [END_FILE_NavigationActions.kt] // [END_FILE_NavigationActions.kt]

View File

@@ -3,7 +3,11 @@
// [SEMANTICS] navigation, routes, sealed_class // [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation package com.homebox.lens.navigation
// [CORE-LOGIC] // [IMPORTS]
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: SealedClass('Screen')]
/** /**
* [CONTRACT] * [CONTRACT]
* Запечатанный класс для определения маршрутов навигации в приложении. * Запечатанный класс для определения маршрутов навигации в приложении.
@@ -11,10 +15,17 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута. * @property route Строковый идентификатор маршрута.
*/ */
sealed class Screen(val route: String) { sealed class Screen(val route: String) {
// [STATE] // [ENTITY: DataObject('Setup')]
data object Setup : Screen("setup_screen") data object Setup : Screen("setup_screen")
// [END_ENTITY: DataObject('Setup')]
// [ENTITY: DataObject('Dashboard')]
data object Dashboard : Screen("dashboard_screen") data object Dashboard : Screen("dashboard_screen")
// [END_ENTITY: DataObject('Dashboard')]
// [ENTITY: DataObject('InventoryList')]
data object InventoryList : Screen("inventory_list_screen") { data object InventoryList : Screen("inventory_list_screen") {
// [ENTITY: Function('withFilter')]
/** /**
* [CONTRACT] * [CONTRACT]
* Создает маршрут для экрана списка инвентаря с параметром фильтра. * Создает маршрут для экрана списка инвентаря с параметром фильтра.
@@ -24,8 +35,10 @@ sealed class Screen(val route: String) {
* @throws IllegalArgumentException если ключ или значение пустые. * @throws IllegalArgumentException если ключ или значение пустые.
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }'). * @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
*/ */
// [HELPER] fun withFilter(
fun withFilter(key: String, value: String): String { key: String,
value: String,
): String {
// [PRECONDITION] // [PRECONDITION]
require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." } require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." }
require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." } require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." }
@@ -35,9 +48,13 @@ sealed class Screen(val route: String) {
check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." } check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." }
return constructedRoute return constructedRoute
} }
// [END_ENTITY: Function('withFilter')]
} }
// [END_ENTITY: DataObject('InventoryList')]
// [ENTITY: DataObject('ItemDetails')]
data object ItemDetails : Screen("item_details_screen/{itemId}") { data object ItemDetails : Screen("item_details_screen/{itemId}") {
// [ENTITY: Function('createRoute')]
/** /**
* [CONTRACT] * [CONTRACT]
* Создает маршрут для экрана деталей элемента с указанным ID. * Создает маршрут для экрана деталей элемента с указанным ID.
@@ -45,7 +62,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута. * @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой. * @throws IllegalArgumentException если itemId пустой.
*/ */
// [HELPER]
fun createRoute(itemId: String): String { fun createRoute(itemId: String): String {
// [PRECONDITION] // [PRECONDITION]
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." } require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
@@ -55,8 +71,13 @@ sealed class Screen(val route: String) {
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." } check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
return route return route
} }
// [END_ENTITY: Function('createRoute')]
} }
// [END_ENTITY: DataObject('ItemDetails')]
// [ENTITY: DataObject('ItemEdit')]
data object ItemEdit : Screen("item_edit_screen/{itemId}") { data object ItemEdit : Screen("item_edit_screen/{itemId}") {
// [ENTITY: Function('createRoute')]
/** /**
* [CONTRACT] * [CONTRACT]
* Создает маршрут для экрана редактирования элемента с указанным ID. * Создает маршрут для экрана редактирования элемента с указанным ID.
@@ -64,7 +85,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута. * @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой. * @throws IllegalArgumentException если itemId пустой.
*/ */
// [HELPER]
fun createRoute(itemId: String): String { fun createRoute(itemId: String): String {
// [PRECONDITION] // [PRECONDITION]
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." } require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
@@ -74,10 +94,21 @@ sealed class Screen(val route: String) {
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." } check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
return route return route
} }
// [END_ENTITY: Function('createRoute')]
} }
// [END_ENTITY: DataObject('ItemEdit')]
// [ENTITY: DataObject('LabelsList')]
data object LabelsList : Screen("labels_list_screen") data object LabelsList : Screen("labels_list_screen")
// [END_ENTITY: DataObject('LabelsList')]
// [ENTITY: DataObject('LocationsList')]
data object LocationsList : Screen("locations_list_screen") data object LocationsList : Screen("locations_list_screen")
// [END_ENTITY: DataObject('LocationsList')]
// [ENTITY: DataObject('LocationEdit')]
data object LocationEdit : Screen("location_edit_screen/{locationId}") { data object LocationEdit : Screen("location_edit_screen/{locationId}") {
// [ENTITY: Function('createRoute')]
/** /**
* [CONTRACT] * [CONTRACT]
* Создает маршрут для экрана редактирования местоположения с указанным ID. * Создает маршрут для экрана редактирования местоположения с указанным ID.
@@ -85,7 +116,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута. * @return Строку полного маршрута.
* @throws IllegalArgumentException если locationId пустой. * @throws IllegalArgumentException если locationId пустой.
*/ */
// [HELPER]
fun createRoute(locationId: String): String { fun createRoute(locationId: String): String {
// [PRECONDITION] // [PRECONDITION]
require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." } require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." }
@@ -95,7 +125,14 @@ sealed class Screen(val route: String) {
check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." } check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." }
return route return route
} }
// [END_ENTITY: Function('createRoute')]
} }
// [END_ENTITY: DataObject('LocationEdit')]
// [ENTITY: DataObject('Search')]
data object Search : Screen("search_screen") data object Search : Screen("search_screen")
// [END_ENTITY: DataObject('Search')]
} }
// [END_ENTITY: SealedClass('Screen')]
// [END_CONTRACT]
// [END_FILE_Screen.kt] // [END_FILE_Screen.kt]

View File

@@ -1,6 +1,9 @@
// [PACKAGE] com.homebox.lens.ui.common // [PACKAGE] com.homebox.lens.ui.common
// [FILE] AppDrawer.kt // [FILE] AppDrawer.kt
// [SEMANTICS] ui, common, navigation_drawer
package com.homebox.lens.ui.common package com.homebox.lens.ui.common
// [IMPORTS]
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -22,18 +25,37 @@ import androidx.compose.ui.unit.dp
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.navigation.Screen 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] * [CONTRACT]
@summary Контент для бокового навигационного меню (Drawer). * @summary Контент для бокового навигационного меню (Drawer).
@param currentRoute Текущий маршрут для подсветки активного элемента. * @param currentRoute Текущий маршрут для подсветки активного элемента.
@param navigationActions Объект с навигационными действиями. * @param navigationActions Объект с навигационными действиями.
@param onCloseDrawer Лямбда для закрытия бокового меню. * @param onCloseDrawer Лямбда для закрытия бокового меню.
*/ */
@Composable @Composable
internal fun AppDrawerContent( internal fun AppDrawerContent(
currentRoute: String?, currentRoute: String?,
navigationActions: NavigationActions, navigationActions: NavigationActions,
onCloseDrawer: () -> Unit onCloseDrawer: () -> Unit,
) { ) {
ModalDrawerSheet { ModalDrawerSheet {
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
@@ -42,9 +64,10 @@ internal fun AppDrawerContent(
navigationActions.navigateToCreateItem() navigationActions.navigateToCreateItem()
onCloseDrawer() onCloseDrawer()
}, },
modifier = Modifier modifier =
Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp),
) { ) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@@ -58,7 +81,7 @@ internal fun AppDrawerContent(
onClick = { onClick = {
navigationActions.navigateToDashboard() navigationActions.navigateToDashboard()
onCloseDrawer() onCloseDrawer()
} },
) )
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_locations)) }, label = { Text(stringResource(id = R.string.nav_locations)) },
@@ -66,7 +89,7 @@ internal fun AppDrawerContent(
onClick = { onClick = {
navigationActions.navigateToLocations() navigationActions.navigateToLocations()
onCloseDrawer() onCloseDrawer()
} },
) )
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_labels)) }, label = { Text(stringResource(id = R.string.nav_labels)) },
@@ -74,7 +97,7 @@ internal fun AppDrawerContent(
onClick = { onClick = {
navigationActions.navigateToLabels() navigationActions.navigateToLabels()
onCloseDrawer() onCloseDrawer()
} },
) )
NavigationDrawerItem( NavigationDrawerItem(
label = { Text(stringResource(id = R.string.search)) }, label = { Text(stringResource(id = R.string.search)) },
@@ -82,7 +105,7 @@ internal fun AppDrawerContent(
onClick = { onClick = {
navigationActions.navigateToSearch() navigationActions.navigateToSearch()
onCloseDrawer() onCloseDrawer()
} },
) )
// TODO: Add Profile and Tools items // TODO: Add Profile and Tools items
Divider() Divider()
@@ -92,7 +115,10 @@ internal fun AppDrawerContent(
onClick = { onClick = {
navigationActions.navigateToLogout() navigationActions.navigateToLogout()
onCloseDrawer() onCloseDrawer()
} },
) )
} }
} }
// [END_ENTITY: Function('AppDrawerContent')]
// [END_CONTRACT]
// [END_FILE_AppDrawer.kt]

View File

@@ -15,8 +15,21 @@ import androidx.compose.ui.res.stringResource
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import kotlinx.coroutines.launch 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] * [CONTRACT]
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer. * @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
@@ -35,7 +48,7 @@ fun MainScaffold(
currentRoute: String?, currentRoute: String?,
navigationActions: NavigationActions, navigationActions: NavigationActions,
topBarActions: @Composable () -> Unit = {}, topBarActions: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit,
) { ) {
// [STATE] // [STATE]
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
@@ -48,9 +61,9 @@ fun MainScaffold(
AppDrawerContent( AppDrawerContent(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions, navigationActions = navigationActions,
onCloseDrawer = { scope.launch { drawerState.close() } } onCloseDrawer = { scope.launch { drawerState.close() } },
) )
} },
) { ) {
Scaffold( Scaffold(
topBar = { topBar = {
@@ -60,18 +73,19 @@ fun MainScaffold(
IconButton(onClick = { scope.launch { drawerState.open() } }) { IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon( Icon(
Icons.Default.Menu, Icons.Default.Menu,
contentDescription = stringResource(id = R.string.cd_open_navigation_drawer) contentDescription = stringResource(id = R.string.cd_open_navigation_drawer),
) )
} }
}, },
actions = { topBarActions() } actions = { topBarActions() },
) )
} },
) { paddingValues -> ) { paddingValues ->
// [ACTION] // [ACTION]
content(paddingValues) content(paddingValues)
} }
} }
// [END_FUNCTION_MainScaffold]
} }
// [END_ENTITY: Function('MainScaffold')]
// [END_CONTRACT]
// [END_FILE_MainScaffold.kt] // [END_FILE_MainScaffold.kt]

View File

@@ -2,6 +2,7 @@
// [FILE] DashboardScreen.kt // [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose, navigation // [SEMANTICS] ui, screen, dashboard, compose, navigation
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -29,24 +30,36 @@ import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.ui.common.MainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.ui.theme.HomeboxLensTheme
import timber.log.Timber 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] * [CONTRACT]
@summary Главная Composable-функция для экрана "Панель управления". * @summary Главная Composable-функция для экрана "Панель управления".
@param viewModel ViewModel для этого экрана, предоставляется через Hilt. * @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
@param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. * @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
@param navigationActions Объект с навигационными действиями. * @param navigationActions Объект с навигационными действиями.
@sideeffect Вызывает навигационные лямбды при взаимодействии с UI. * @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
*/ */
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel(), viewModel: DashboardViewModel = hiltViewModel(),
currentRoute: String?, currentRoute: String?,
navigationActions: NavigationActions navigationActions: NavigationActions,
) { ) {
// [STATE] // [STATE]
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
// [UI_COMPONENT] // [UI_COMPONENT]
MainScaffold( MainScaffold(
topBarTitle = stringResource(id = R.string.dashboard_title), topBarTitle = stringResource(id = R.string.dashboard_title),
currentRoute = currentRoute, currentRoute = currentRoute,
@@ -55,10 +68,10 @@ fun DashboardScreen(
IconButton(onClick = { navigationActions.navigateToSearch() }) { IconButton(onClick = { navigationActions.navigateToSearch() }) {
Icon( Icon(
Icons.Default.Search, Icons.Default.Search,
contentDescription = stringResource(id = R.string.cd_scan_qr_code) // TODO: Rename string resource contentDescription = stringResource(id = R.string.cd_scan_qr_code), // TODO: Rename string resource
) )
} }
} },
) { paddingValues -> ) { paddingValues ->
DashboardContent( DashboardContent(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
@@ -70,28 +83,40 @@ fun DashboardScreen(
onLabelClick = { label -> onLabelClick = { label ->
Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...") Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...")
navigationActions.navigateToInventoryListWithLabel(label.id) navigationActions.navigateToInventoryListWithLabel(label.id)
} },
) )
} }
// [END_FUNCTION_DashboardScreen]
} }
// [HELPER] // [END_ENTITY: Function('DashboardScreen')]
// [ENTITY: Function('DashboardContent')]
// [RELATION: Function('DashboardContent') -> [DEPENDS_ON] -> SealedInterface('DashboardUiState')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Box')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Spacer')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('StatisticsSection')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('RecentlyAddedSection')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LocationsSection')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LabelsSection')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Отображает основной контент экрана в зависимости от uiState. * @summary Отображает основной контент экрана в зависимости от uiState.
@param modifier Модификатор для стилизации. * @param modifier Модификатор для стилизации.
@param uiState Текущее состояние UI экрана. * @param uiState Текущее состояние UI экрана.
@param onLocationClick Лямбда-обработчик нажатия на местоположение. * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
@param onLabelClick Лямбда-обработчик нажатия на метку. * @param onLabelClick Лямбда-обработчик нажатия на метку.
*/ */
@Composable @Composable
private fun DashboardContent( private fun DashboardContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
uiState: DashboardUiState, uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit, onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit onLabelClick: (LabelOut) -> Unit,
) { ) {
// [CORE-LOGIC] // [CORE-LOGIC]
when (uiState) { when (uiState) {
is DashboardUiState.Loading -> { is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -103,16 +128,17 @@ private fun DashboardContent(
Text( Text(
text = uiState.message, text = uiState.message,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
} }
} }
is DashboardUiState.Success -> { is DashboardUiState.Success -> {
LazyColumn( LazyColumn(
modifier = modifier modifier =
modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp) verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
item { Spacer(modifier = Modifier.height(8.dp)) } item { Spacer(modifier = Modifier.height(8.dp)) }
item { StatisticsSection(statistics = uiState.statistics) } item { StatisticsSection(statistics = uiState.statistics) }
@@ -123,74 +149,124 @@ private fun DashboardContent(
} }
} }
} }
// [END_FUNCTION_DashboardContent]
} }
// [UI_COMPONENT] // [END_ENTITY: Function('DashboardContent')]
// [ENTITY: Function('StatisticsSection')]
// [RELATION: Function('StatisticsSection') -> [DEPENDS_ON] -> Class('GroupStatistics')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Card')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('LazyVerticalGrid')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('GridCells.Fixed')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('StatisticCard')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Секция для отображения общей статистики. * @summary Секция для отображения общей статистики.
@param statistics Объект со статистическими данными. * @param statistics Объект со статистическими данными.
*/ */
@Composable @Composable
private fun StatisticsSection(statistics: GroupStatistics) { private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_quick_stats), text = stringResource(id = R.string.dashboard_section_quick_stats),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
Card { Card {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
modifier = Modifier modifier =
Modifier
.height(120.dp) .height(120.dp)
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) } item {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) } StatisticCard(
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) } title = stringResource(id = R.string.dashboard_stat_total_items),
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) } value = statistics.items.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = R.string.dashboard_stat_total_value),
value = statistics.totalValue.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = R.string.dashboard_stat_total_labels),
value = statistics.labels.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = R.string.dashboard_stat_total_locations),
value = statistics.locations.toString(),
)
}
} }
} }
} }
} }
// [UI_COMPONENT] // [END_ENTITY: Function('StatisticsSection')]
// [ENTITY: Function('StatisticCard')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Column')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Text')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.labelMedium')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.headlineSmall')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Карточка для отображения одного статистического показателя. * @summary Карточка для отображения одного статистического показателя.
@param title Название показателя. * @param title Название показателя.
@param value Значение показателя. * @param value Значение показателя.
*/ */
@Composable @Composable
private fun StatisticCard(title: String, value: String) { private fun StatisticCard(
title: String,
value: String,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center) Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
} }
} }
// [UI_COMPONENT] // [END_ENTITY: Function('StatisticCard')]
// [ENTITY: Function('RecentlyAddedSection')]
// [RELATION: Function('RecentlyAddedSection') -> [DEPENDS_ON] -> Class('ItemSummary')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('LazyRow')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('ItemCard')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Секция для отображения недавно добавленных элементов. * @summary Секция для отображения недавно добавленных элементов.
@param items Список элементов для отображения. * @param items Список элементов для отображения.
*/ */
@Composable @Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) { private fun RecentlyAddedSection(items: List<ItemSummary>) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_recently_added), text = stringResource(id = R.string.dashboard_section_recently_added),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
if (items.isEmpty()) { if (items.isEmpty()) {
Text( Text(
text = stringResource(id = R.string.items_not_found), text = stringResource(id = R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier modifier =
Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 16.dp), .padding(vertical = 16.dp),
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
} else { } else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -201,41 +277,70 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
} }
} }
} }
// [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] * [CONTRACT]
@summary Карточка для отображения краткой информации об элементе. * @summary Карточка для отображения краткой информации об элементе.
@param item Элемент для отображения. * @param item Элемент для отображения.
*/ */
@Composable @Composable
private fun ItemCard(item: ItemSummary) { private fun ItemCard(item: ItemSummary) {
Card(modifier = Modifier.width(150.dp)) { Card(modifier = Modifier.width(150.dp)) {
Column(modifier = Modifier.padding(8.dp)) { Column(modifier = Modifier.padding(8.dp)) {
// TODO: Add image here from item.image // TODO: Add image here from item.image
Spacer(modifier = Modifier Spacer(
modifier =
Modifier
.height(80.dp) .height(80.dp)
.fillMaxWidth() .fillMaxWidth()
.background(MaterialTheme.colorScheme.secondaryContainer)) .background(MaterialTheme.colorScheme.secondaryContainer),
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1) Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1) Text(
text = item.location?.name ?: stringResource(id = R.string.no_location),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
)
} }
} }
} }
// [UI_COMPONENT] // [END_ENTITY: Function('ItemCard')]
// [ENTITY: Function('LocationsSection')]
// [RELATION: Function('LocationsSection') -> [DEPENDS_ON] -> Class('LocationOutCount')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('FlowRow')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('SuggestionChip')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Секция для отображения местоположений в виде чипсов. * @summary Секция для отображения местоположений в виде чипсов.
@param locations Список местоположений. * @param locations Список местоположений.
@param onLocationClick Лямбда-обработчик нажатия на местоположение. * @param onLocationClick Лямбда-обработчик нажатия на местоположение.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) { private fun LocationsSection(
locations: List<LocationOutCount>,
onLocationClick: (LocationOutCount) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_locations), text = stringResource(id = R.string.dashboard_section_locations),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -243,26 +348,38 @@ private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick:
locations.forEach { location -> locations.forEach { location ->
SuggestionChip( SuggestionChip(
onClick = { onLocationClick(location) }, onClick = { onLocationClick(location) },
label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) } label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) },
) )
} }
} }
} }
} }
// [UI_COMPONENT] // [END_ENTITY: Function('LocationsSection')]
// [ENTITY: Function('LabelsSection')]
// [RELATION: Function('LabelsSection') -> [DEPENDS_ON] -> Class('LabelOut')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('FlowRow')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('SuggestionChip')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Секция для отображения меток в виде чипсов. * @summary Секция для отображения меток в виде чипсов.
@param labels Список меток. * @param labels Список меток.
@param onLabelClick Лямбда-обработчик нажатия на метку. * @param onLabelClick Лямбда-обработчик нажатия на метку.
*/ */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) { private fun LabelsSection(
labels: List<LabelOut>,
onLabelClick: (LabelOut) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_labels), text = stringResource(id = R.string.dashboard_section_labels),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -270,46 +387,105 @@ private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Un
labels.forEach { label -> labels.forEach { label ->
SuggestionChip( SuggestionChip(
onClick = { onLabelClick(label) }, onClick = { onLabelClick(label) },
label = { Text(label.name) } label = { Text(label.name) },
) )
} }
} }
} }
} }
// [END_ENTITY: Function('LabelsSection')]
// [ENTITY: Function('DashboardContentSuccessPreview')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardUiState.Success')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('GroupStatistics')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LocationOutCount')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LabelOut')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardContent')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Success State") @Preview(showBackground = true, name = "Dashboard Success State")
@Composable @Composable
fun DashboardContentSuccessPreview() { fun DashboardContentSuccessPreview() {
val previewState = DashboardUiState.Success( val previewState =
statistics = GroupStatistics( DashboardUiState.Success(
statistics =
GroupStatistics(
items = 123, items = 123,
totalValue = 9999.99, totalValue = 9999.99,
locations = 5, locations = 5,
labels = 8 labels = 8,
), ),
locations = listOf( locations =
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""), listOf(
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""), LocationOutCount(
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""), id = "1",
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""), name = "Office",
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") color = "#FF0000",
isArchived = false,
itemCount = 10,
createdAt = "",
updatedAt = "",
), ),
labels = listOf( LocationOutCount(
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), id = "2",
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), name = "Garage",
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), color = "#00FF00",
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") isArchived = false,
itemCount = 5,
createdAt = "",
updatedAt = "",
), ),
recentlyAddedItems = emptyList() LocationOutCount(
id = "3",
name = "Living Room",
color = "#0000FF",
isArchived = false,
itemCount = 15,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "4",
name = "Kitchen",
color = "#FFFF00",
isArchived = false,
itemCount = 20,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "5",
name = "Basement",
color = "#00FFFF",
isArchived = false,
itemCount = 3,
createdAt = "",
updatedAt = "",
),
),
labels =
listOf(
LabelOut(id = "1", name = "electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "2", name = "important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "3", name = "seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "4", name = "hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = ""),
),
recentlyAddedItems = emptyList(),
) )
HomeboxLensTheme { HomeboxLensTheme {
DashboardContent( DashboardContent(
uiState = previewState, uiState = previewState,
onLocationClick = {}, onLocationClick = {},
onLabelClick = {} onLabelClick = {},
) )
} }
} }
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
// [ENTITY: Function('DashboardContentLoadingPreview')]
// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardContent')]
// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardUiState.Loading')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Loading State") @Preview(showBackground = true, name = "Dashboard Loading State")
@Composable @Composable
@@ -318,10 +494,17 @@ fun DashboardContentLoadingPreview() {
DashboardContent( DashboardContent(
uiState = DashboardUiState.Loading, uiState = DashboardUiState.Loading,
onLocationClick = {}, onLocationClick = {},
onLabelClick = {} onLabelClick = {},
) )
} }
} }
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
// [ENTITY: Function('DashboardContentErrorPreview')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardContent')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardUiState.Error')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('stringResource')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Error State") @Preview(showBackground = true, name = "Dashboard Error State")
@Composable @Composable
@@ -330,8 +513,10 @@ fun DashboardContentErrorPreview() {
DashboardContent( DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)), uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
onLocationClick = {}, onLocationClick = {},
onLabelClick = {} onLabelClick = {},
) )
} }
} }
// [END_ENTITY: Function('DashboardContentErrorPreview')]
// [END_CONTRACT]
// [END_FILE_DashboardScreen.kt] // [END_FILE_DashboardScreen.kt]

View File

@@ -1,15 +1,17 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard // [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 // [SEMANTICS] ui, state, dashboard
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS]
import com.homebox.lens.domain.model.GroupStatistics import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.model.LabelOut import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.domain.model.ItemSummary
// [END_IMPORTS]
// [CORE-LOGIC] // [CONTRACT]
// [ENTITY: SealedInterface('DashboardUiState')] // [ENTITY: SealedInterface('DashboardUiState')]
/** /**
* [CONTRACT] * [CONTRACT]
@@ -17,6 +19,11 @@ import com.homebox.lens.domain.model.LocationOutCount
* @invariant В любой момент времени экран может находиться только в одном из этих состояний. * @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/ */
sealed interface DashboardUiState { 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] * [CONTRACT]
* Состояние успешной загрузки данных. * Состояние успешной загрузки данных.
@@ -29,20 +36,27 @@ sealed interface DashboardUiState {
val statistics: GroupStatistics, val statistics: GroupStatistics,
val locations: List<LocationOutCount>, val locations: List<LocationOutCount>,
val labels: List<LabelOut>, val labels: List<LabelOut>,
val recentlyAddedItems: List<com.homebox.lens.domain.model.ItemSummary> val recentlyAddedItems: List<ItemSummary>,
) : DashboardUiState ) : DashboardUiState
// [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')]
/** /**
* [CONTRACT] * [CONTRACT]
* Состояние ошибки во время загрузки данных. * Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке. * @property message Человекочитаемое сообщение об ошибке.
*/ */
data class Error(val message: String) : DashboardUiState data class Error(val message: String) : DashboardUiState
// [END_ENTITY: DataClass('Error')]
// [ENTITY: DataObject('Loading')]
/** /**
* [CONTRACT] * [CONTRACT]
* Состояние, когда данные для экрана загружаются. * Состояние, когда данные для экрана загружаются.
*/ */
data object Loading : DashboardUiState object Loading : DashboardUiState
// [END_ENTITY: DataObject('Loading')]
} }
// [END_ENTITY: SealedInterface('DashboardUiState')]
// [END_CONTRACT]
// [END_FILE_DashboardUiState.kt] // [END_FILE_DashboardUiState.kt]

View File

@@ -2,6 +2,7 @@
// [FILE] DashboardViewModel.kt // [FILE] DashboardViewModel.kt
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging // [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
// [IMPORTS] // [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -9,17 +10,21 @@ import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
import com.homebox.lens.domain.usecase.GetStatisticsUseCase import com.homebox.lens.domain.usecase.GetStatisticsUseCase
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [VIEWMODEL] // [CONTRACT]
// [ENTITY: ViewModel('DashboardViewModel')] // [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] * [CONTRACT]
* @summary ViewModel для главного экрана (Dashboard). * @summary ViewModel для главного экрана (Dashboard).
@@ -28,15 +33,17 @@ import javax.inject.Inject
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`. * @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/ */
@HiltViewModel @HiltViewModel
class DashboardViewModel @Inject constructor( class DashboardViewModel
@Inject
constructor(
private val getStatisticsUseCase: GetStatisticsUseCase, private val getStatisticsUseCase: GetStatisticsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase, private val getAllLocationsUseCase: GetAllLocationsUseCase,
private val getAllLabelsUseCase: GetAllLabelsUseCase, private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase,
) : ViewModel() { ) : ViewModel() {
// [STATE] // [STATE]
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading) private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow(). // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока. // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
@@ -47,6 +54,19 @@ class DashboardViewModel @Inject constructor(
loadDashboardData() 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] * [CONTRACT]
* @summary Загружает все необходимые данные для экрана Dashboard. * @summary Загружает все необходимые данные для экрана Dashboard.
@@ -55,7 +75,6 @@ class DashboardViewModel @Inject constructor(
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. * @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/ */
fun loadDashboardData() { fun loadDashboardData() {
// [ENTRYPOINT]
viewModelScope.launch { viewModelScope.launch {
_uiState.value = DashboardUiState.Loading _uiState.value = DashboardUiState.Loading
Timber.i("[ACTION] Starting dashboard data collection.") Timber.i("[ACTION] Starting dashboard data collection.")
@@ -70,12 +89,13 @@ class DashboardViewModel @Inject constructor(
statistics = stats, statistics = stats,
locations = locations, locations = locations,
labels = labels, labels = labels,
recentlyAddedItems = recentItems recentlyAddedItems = recentItems,
) )
}.catch { exception -> }.catch { exception ->
Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.") Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
_uiState.value = DashboardUiState.Error( _uiState.value =
message = exception.message ?: "Could not load dashboard data." DashboardUiState.Error(
message = exception.message ?: "Could not load dashboard data.",
) )
}.collect { successState -> }.collect { successState ->
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.") Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
@@ -83,6 +103,8 @@ class DashboardViewModel @Inject constructor(
} }
} }
} }
// [END_CLASS_DashboardViewModel] // [END_ENTITY: Function('loadDashboardData')]
} }
// [END_ENTITY: ViewModel('DashboardViewModel')]
// [END_CONTRACT]
// [END_FILE_DashboardViewModel.kt] // [END_FILE_DashboardViewModel.kt]

View File

@@ -1,37 +1,219 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist // [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt // [FILE] InventoryListScreen.kt
// [SEMANTICS] ui, screen, inventory, list // [SEMANTICS] ui, screen, inventory, list, compose
package com.homebox.lens.ui.screen.inventorylist package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS] // [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.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable 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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.domain.model.Item
import com.homebox.lens.ui.common.MainScaffold import timber.log.Timber
// [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('InventoryListScreen')]
// [RELATION: Function('InventoryListScreen') -> [DEPENDS_ON] -> Class('InventoryListViewModel')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('FloatingActionButton')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('SearchBar')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('InventoryListContent')]
/** /**
* [CONTRACT] * [MAIN-CONTRACT]
* @summary Composable-функция для экрана "Список инвентаря". * Экран для отображения списка инвентарных позиций.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. *
* @param navigationActions Объект с навигационными действиями. * Реализует спецификацию `screen_inventory_list`. Позволяет просматривать,
* искать и синхронизировать инвентарь.
*
* @param onItemClick Обработчик нажатия на элемент инвентаря.
* @param onNavigateBack Обработчик для возврата на предыдущий экран.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun InventoryListScreen( fun InventoryListScreen(
currentRoute: String?, viewModel: InventoryListViewModel = hiltViewModel(),
navigationActions: NavigationActions onItemClick: (Item) -> Unit,
onNavigateBack: () -> Unit
) { ) {
// [UI_COMPONENT] // [STATE]
MainScaffold( val uiState by viewModel.uiState.collectAsState()
topBarTitle = stringResource(id = R.string.inventory_list_title),
currentRoute = currentRoute, // [ACTION]
navigationActions = navigationActions Scaffold(
) { topBar = {
// [CORE-LOGIC] TopAppBar(
Text(text = "TODO: Inventory List Screen") 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<Item>,
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]

View File

@@ -1,16 +1,53 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist // [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt // [FILE] InventoryListViewModel.kt
// [SEMANTICS] ui_logic, inventory_list, viewmodel
package com.homebox.lens.ui.screen.inventorylist package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject 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 @HiltViewModel
class InventoryListViewModel @Inject constructor() : ViewModel() { class InventoryListViewModel
@Inject
constructor() : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI state private val _uiState = MutableStateFlow(InventoryListUiState())
} val uiState: StateFlow<InventoryListUiState> = _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] // [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<Item> = emptyList()
)
// [END_ENTITY: DataClass('InventoryListUiState')]

View File

@@ -1,37 +1,208 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails // [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt // [FILE] ItemDetailsScreen.kt
// [SEMANTICS] ui, screen, item, details // [SEMANTICS] ui, screen, item, details, compose
package com.homebox.lens.ui.screen.itemdetails package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS] // [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.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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.domain.model.Item
import com.homebox.lens.ui.common.MainScaffold import timber.log.Timber
// [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('ItemDetailsScreen')]
// [RELATION: Function('ItemDetailsScreen') -> [DEPENDS_ON] -> Class('ItemDetailsViewModel')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('ItemDetailsContent')]
/** /**
* [CONTRACT] * [MAIN-CONTRACT]
* @summary Composable-функция для экрана "Детали элемента". * Экран для отображения детальной информации о товаре.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. *
* @param navigationActions Объект с навигационными действиями. * Реализует спецификацию `screen_item_details`.
*
* @param onNavigateBack Обработчик для возврата на предыдущий экран.
* @param onEditClick Обработчик нажатия на кнопку редактирования.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ItemDetailsScreen( fun ItemDetailsScreen(
currentRoute: String?, viewModel: ItemDetailsViewModel = hiltViewModel(),
navigationActions: NavigationActions onNavigateBack: () -> Unit,
onEditClick: (Int) -> Unit
) { ) {
// [UI_COMPONENT] // [STATE]
MainScaffold( val uiState by viewModel.uiState.collectAsState()
topBarTitle = stringResource(id = R.string.item_details_title),
currentRoute = currentRoute, Scaffold(
navigationActions = navigationActions topBar = {
) { TopAppBar(
// [CORE-LOGIC] title = { Text(uiState.item?.name ?: stringResource(id = R.string.item_details_title)) }, // Corrected string resource name
Text(text = "TODO: Item Details Screen") 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]

View File

@@ -3,14 +3,41 @@
package com.homebox.lens.ui.screen.itemdetails package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject 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 @HiltViewModel
class ItemDetailsViewModel @Inject constructor() : ViewModel() { class ItemDetailsViewModel
@Inject
constructor() : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI 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] // [END_FILE_ItemDetailsViewModel.kt]
// Placeholder for ItemDetailsUiState to resolve compilation errors
data class ItemDetailsUiState(
val item: Item? = null,
val isLoading: Boolean = false
)

View File

@@ -1,37 +1,162 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit // [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt // [FILE] ItemEditScreen.kt
// [SEMANTICS] ui, screen, item, edit // [SEMANTICS] ui, screen, item, edit, create, compose
package com.homebox.lens.ui.screen.itemedit package com.homebox.lens.ui.screen.itemedit
// [IMPORTS] // [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.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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions import timber.log.Timber
import com.homebox.lens.ui.common.MainScaffold // [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('ItemEditScreen')]
// [RELATION: Function('ItemEditScreen') -> [DEPENDS_ON] -> Class('ItemEditViewModel')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('LaunchedEffect')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('ItemEditContent')]
/** /**
* [CONTRACT] * [MAIN-CONTRACT]
* @summary Composable-функция для экрана "Редактирование элемента". * Экран для создания или редактирования товара.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. *
* @param navigationActions Объект с навигационными действиями. * Реализует спецификацию `screen_item_edit`.
*
* @param onNavigateBack Обработчик для возврата на предыдущий экран после сохранения или отмены.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ItemEditScreen( fun ItemEditScreen(
currentRoute: String?, viewModel: ItemEditViewModel = hiltViewModel(),
navigationActions: NavigationActions onNavigateBack: () -> Unit
) { ) {
// [UI_COMPONENT] // [STATE]
MainScaffold( val uiState by viewModel.uiState.collectAsState()
topBarTitle = stringResource(id = R.string.item_edit_title),
currentRoute = currentRoute, // [SIDE-EFFECT]
navigationActions = navigationActions LaunchedEffect(uiState.isSaved) {
) { if (uiState.isSaved) {
// [CORE-LOGIC] Timber.i("[INFO][SIDE_EFFECT][navigation] Item saved, navigating back.")
Text(text = "TODO: Item Edit Screen") 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]

View File

@@ -3,14 +3,57 @@
package com.homebox.lens.ui.screen.itemedit package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject 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 @HiltViewModel
class ItemEditViewModel @Inject constructor() : ViewModel() { class ItemEditViewModel
@Inject
constructor() : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI 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] // [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
)

View File

@@ -1,15 +1,14 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist // [PACKAGE]com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt // [FILE]LabelsListScreen.kt
// [SEMANTICS] ui, labels_list, state_management, compose, dialog // [SEMANTICS]ui, screen, labels, list, compose
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -17,241 +16,188 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Label import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import com.homebox.lens.R import com.homebox.lens.R
import com.homebox.lens.domain.model.Label import com.homebox.lens.domain.model.Label
import com.homebox.lens.navigation.Screen import com.homebox.lens.ui.screen.labelslist.LabelsListUiState
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS]
// [SECTION] Main Screen Composable // [CONTRACT]
// [ENTITY: Function('LabelsListScreen')]
// [RELATION: Function('LabelsListScreen') -> [DEPENDS_ON] -> SealedInterface('LabelsListUiState')]
// [RELATION: Function('LabelsListScreen') -> [CREATES_INSTANCE_OF] -> Class('Scaffold')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('LabelsListContent')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('FloatingActionButton')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('CircularProgressIndicator')]
/** /**
* [CONTRACT] * [MAIN-CONTRACT]
* @summary Отображает экран со списком всех меток. * Экран для отображения списка всех меток.
* @description Главная Composable-функция для экрана меток. Она использует Scaffold для структуры,
* получает состояние от `LabelsListViewModel`, обрабатывает навигацию и делегирует отображение
* списка и диалогов вспомогательным Composable-функциям.
* *
* @param navController Контроллер навигации для перемещения между экранами. * Этот Composable является точкой входа для UI, определенного в спецификации `screen_labels_list`.
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток. * Он получает состояние от [LabelsListViewModel] и отображает его, делегируя обработку
* пользовательских событий в ViewModel.
* *
* @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события. * @param uiState Текущее состояние UI для экрана списка меток.
* @precondition `viewModel` должен быть доступен через Hilt. * @param onLabelClick Функция обратного вызова для обработки нажатия на метку.
* @postcondition Экран исчерпывающе обрабатывает все состояния из `LabelsListUiState` (Loading, Success, Error). * @param onAddClick Функция обратного вызова для обработки нажатия на кнопку добавления метки.
* @sideeffect Пользовательские действия (клики) инициируют вызовы ViewModel и навигационные команды через `navController`. * @param onNavigateBack Функция обратного вызова для навигации назад.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LabelsListScreen( fun labelsListScreen(
navController: NavController, uiState: LabelsListUiState,
viewModel: LabelsListViewModel = hiltViewModel() onLabelClick: (Label) -> Unit,
onAddClick: () -> Unit,
onNavigateBack: () -> Unit,
) { ) {
// [ENTRYPOINT]
val uiState by viewModel.uiState.collectAsState()
// [CORE-LOGIC]
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = R.string.screen_title_labels)) }, title = { Text(stringResource(id = R.string.screen_title_labels)) },
navigationIcon = { navigationIcon = {
// [ACTION] Handle back navigation IconButton(onClick = onNavigateBack) {
IconButton(onClick = {
Timber.i("[ACTION] Navigate up initiated.")
navController.navigateUp()
}) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_desc_navigate_back) contentDescription = stringResource(id = R.string.content_desc_navigate_back)
) )
} }
} },
) )
}, },
floatingActionButton = { floatingActionButton = {
// [ACTION] Handle create new label initiation FloatingActionButton(onClick = onAddClick) {
FloatingActionButton(onClick = {
Timber.i("[ACTION] FAB clicked: Initiate create new label flow.")
viewModel.onShowCreateDialog()
}) {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Filled.Add,
contentDescription = stringResource(id = R.string.content_desc_create_label) contentDescription = stringResource(id = R.string.content_desc_add_label)
) )
} }
} }
) { paddingValues -> ) { innerPadding ->
val currentState = uiState Box(modifier = Modifier.padding(innerPadding)) {
if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) { when (uiState) {
CreateLabelDialog(
onConfirm = { labelName ->
viewModel.createLabel(labelName)
},
onDismiss = {
viewModel.onDismissCreateDialog()
}
)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
// [CORE-LOGIC] State-driven UI rendering
when (currentState) {
is LabelsListUiState.Loading -> { is LabelsListUiState.Loading -> {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator() CircularProgressIndicator()
} }
is LabelsListUiState.Error -> {
Text(text = currentState.message)
} }
is LabelsListUiState.Success -> { is LabelsListUiState.Success -> {
if (currentState.labels.isEmpty()) { LabelsListContent(
Text(text = stringResource(id = R.string.labels_list_empty)) uiState = uiState,
} else { onLabelClick = onLabelClick
LabelsList(
labels = currentState.labels,
onLabelClick = { label ->
// [ACTION] Handle label click
Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.")
// [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр.
val route = Screen.InventoryList.withFilter("label", label.id)
navController.navigate(route)
}
) )
} }
is LabelsListUiState.Error -> {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = uiState.message)
}
} }
} }
} }
} }
// [COHERENCE_CHECK_PASSED]
} }
// [END_FUNCTION] LabelsListScreen // [END_ENTITY: Function('LabelsListScreen')]
// [SECTION] Helper Composables
// [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] * [CONTRACT]
* @summary Composable-функция для отображения списка меток. * Отображает основной контент экрана: список меток.
* @param labels Список объектов `Label` для отображения. *
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка. * @param uiState Состояние успеха, содержащее список меток.
* @param modifier Модификатор для настройки внешнего вида. * @param onLabelClick Обработчик нажатия на элемент списка.
* @sideeffect Отсутствуют.
*/ */
@Composable @Composable
private fun LabelsList( private fun LabelsListContent(
labels: List<Label>, uiState: LabelsListUiState.Success,
onLabelClick: (Label) -> Unit, onLabelClick: (Label) -> Unit
modifier: Modifier = Modifier
) { ) {
// [CORE-LOGIC] if (uiState.labels.isEmpty()) {
LazyColumn( Column(
modifier = modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.Center,
verticalArrangement = Arrangement.spacedBy(8.dp) horizontalAlignment = Alignment.CenterHorizontally
) { ) {
items(labels, key = { it.id }) { label -> Text(text = stringResource(id = R.string.no_labels_found))
}
} else {
LazyColumn {
items(uiState.labels, key = { it.id }) { label ->
LabelListItem( LabelListItem(
label = label, label = label,
onClick = { onLabelClick(label) } onClick = {
Timber.i("[INFO][ACTION][ui_interaction] Label clicked: ${label.name}")
onLabelClick(label)
}
) )
} }
} }
}
} }
// [END_FUNCTION] LabelsList // [END_ENTITY: Function('LabelsListContent')]
// [ENTITY: Function('LabelListItem')]
// [RELATION: Function('LabelListItem') -> [CALLS] -> Function('ListItem')]
// [RELATION: Function('LabelListItem') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelListItem') -> [CALLS] -> Function('Icon')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Composable-функция для отображения одного элемента в списке меток. * Отображает один элемент в списке меток.
* @param label Объект `Label`, который нужно отобразить. *
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент. * @param label Метка для отображения.
* @param onClick Обработчик нажатия на элемент.
* @sideeffect Отсутствуют.
*/ */
@Composable @Composable
private fun LabelListItem( private fun LabelListItem(
label: Label, label: Label,
onClick: () -> Unit onClick: () -> Unit
) { ) {
// [PRECONDITION]
require(label.name.isNotBlank()) { "Label name cannot be blank." }
// [CORE-LOGIC] // [CORE-LOGIC]
ListItem( ListItem(
headlineContent = { Text(text = label.name) }, headlineContent = { Text(label.name) },
leadingContent = { leadingContent = {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.Label, imageVector = Icons.AutoMirrored.Filled.Label,
contentDescription = stringResource(id = R.string.content_desc_label_icon) contentDescription = null // Декоративный элемент
) )
}, },
modifier = Modifier.clickable(onClick = onClick) modifier = Modifier.clickable(onClick = onClick)
) )
} }
// [END_FUNCTION] LabelListItem // [END_ENTITY: Function('LabelListItem')]
// [END_CONTRACT]
/** // [END_FILE_LabelsListScreen.kt]
* [CONTRACT]
* @summary Диалоговое окно для создания новой метки.
* @param onConfirm Лямбда-функция, вызываемая при подтверждении создания с именем метки.
* @param onDismiss Лямбда-функция, вызываемая при закрытии диалога.
*/
@Composable
private fun CreateLabelDialog(
onConfirm: (String) -> Unit,
onDismiss: () -> Unit
) {
// [STATE]
var text by remember { mutableStateOf("") }
val isConfirmEnabled = text.isNotBlank()
// [CORE-LOGIC]
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(R.string.dialog_title_create_label)) },
text = {
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text(stringResource(R.string.dialog_field_label_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
},
confirmButton = {
TextButton(
onClick = { onConfirm(text) },
enabled = isConfirmEnabled
) {
Text(stringResource(R.string.dialog_button_create))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.dialog_button_cancel))
}
}
)
}
// [END_FUNCTION] CreateLabelDialog
// [END_FILE] LabelsListScreen.kt

View File

@@ -1,36 +1,53 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist // [PACKAGE]com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListUiState.kt // [FILE]LabelsListUiState.kt
// [SEMANTICS] ui_state, sealed_interface, contract // [SEMANTICS]ui_state, sealed_interface, contract
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
// [IMPORTS] // [IMPORTS]
import com.homebox.lens.domain.model.Label import com.homebox.lens.domain.model.Label
// [END_IMPORTS]
// [CONTRACT] // [CONTRACT]
// [ENTITY: SealedInterface('LabelsListUiState')]
/** /**
[CONTRACT] * [CONTRACT]
@summary Определяет все возможные состояния для UI экрана со списком меток. * @summary Определяет все возможные состояния для UI экрана со списком меток.
@description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях. * @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
*/ */
sealed interface LabelsListUiState { sealed interface LabelsListUiState {
// [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success') -> [IMPLEMENTS] -> SealedInterface('LabelsListUiState')]
// [RELATION: DataClass('Success') -> [DEPENDS_ON] -> DataStructure('Label')]
/** /**
@summary Состояние успеха, содержит список меток и состояние диалога. * @summary Состояние успеха, содержит список меток и состояние диалога.
@property labels Список меток для отображения. * @property labels Список меток для отображения.
@property isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки. * @property isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
@invariant labels не может быть null. * @invariant labels не может быть null.
*/ */
data class Success( data class Success(
val labels: List<Label>, val labels: List<Label>,
val isShowingCreateDialog: Boolean = false val isShowingCreateDialog: Boolean = false
) : LabelsListUiState ) : LabelsListUiState
// [ENTITY: DataClass('Error')]
// [RELATION: DataClass('Error') -> [IMPLEMENTS] -> SealedInterface('LabelsListUiState')]
/** /**
@summary Состояние ошибки. * @summary Состояние ошибки.
@property message Текст ошибки для отображения пользователю. * @property message Текст ошибки для отображения пользователю, или `null` при отсутствии ошибки.
@invariant message не может быть пустой. * @invariant message не может быть пустой.
*/ */
data class Error(val message: String) : LabelsListUiState data class Error(
val message: String
) : LabelsListUiState
// [ENTITY: Object('Loading')]
// [RELATION: Object('Loading') -> [IMPLEMENTS] -> SealedInterface('LabelsListUiState')]
/** /**
@summary Состояние загрузки данных. * @summary Состояние загрузки данных.
@description Указывает, что идет процесс загрузки меток. * @description Указывает, что идет процесс загрузки меток.
*/ */
data object Loading : LabelsListUiState object Loading : LabelsListUiState
} }
// [END_ENTITY: SealedInterface('LabelsListUiState')]
// [END_CONTRACT]
// [END_FILE_LabelsListUiState.kt] // [END_FILE_LabelsListUiState.kt]

View File

@@ -15,9 +15,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [VIEWMODEL] // [CONTRACT]
// [ENTITY: ViewModel('LabelsListViewModel')] // [ENTITY: ViewModel('LabelsListViewModel')]
// [RELATION: ViewModel('LabelsListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('LabelsListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('LabelsListViewModel') -> [DEPENDS_ON] -> Class('GetAllLabelsUseCase')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary ViewModel для экрана со списком меток. * @summary ViewModel для экрана со списком меток.
@@ -25,10 +29,11 @@ import javax.inject.Inject
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`. * @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
*/ */
@HiltViewModel @HiltViewModel
class LabelsListViewModel @Inject constructor( class LabelsListViewModel
private val getAllLabelsUseCase: GetAllLabelsUseCase @Inject
) : ViewModel() { constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase,
) : ViewModel() {
// [STATE] // [STATE]
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading) private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@@ -38,6 +43,15 @@ class LabelsListViewModel @Inject constructor(
loadLabels() loadLabels()
} }
// [ENTITY: Function('loadLabels')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadLabels') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('runCatching')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('getAllLabelsUseCase')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('result.fold')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('Timber.e')]
// [RELATION: Function('loadLabels') -> [CREATES_INSTANCE_OF] -> Class('Label')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Загружает список меток. * @summary Загружает список меток.
@@ -47,13 +61,13 @@ class LabelsListViewModel @Inject constructor(
*/ */
// [ACTION] // [ACTION]
fun loadLabels() { fun loadLabels() {
// [ENTRYPOINT]
viewModelScope.launch { viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading _uiState.value = LabelsListUiState.Loading
Timber.i("[ACTION] Starting labels list load. State -> Loading.") Timber.i("[ACTION] Starting labels list load. State -> Loading.")
// [CORE-LOGIC] // [CORE-LOGIC]
val result = runCatching { val result =
runCatching {
getAllLabelsUseCase() getAllLabelsUseCase()
} }
@@ -63,24 +77,30 @@ class LabelsListViewModel @Inject constructor(
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.") Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
// [DATA-FLOW] Map List<LabelOut> to List<Label> for the UI state. // [DATA-FLOW] Map List<LabelOut> to List<Label> for the UI state.
// The 'Label' model for the UI is simpler and only contains 'id' and 'name'. // The 'Label' model for the UI is simpler and only contains 'id' and 'name'.
val labels = labelOuts.map { labelOut -> val labels =
labelOuts.map { labelOut ->
Label( Label(
id = labelOut.id, id = labelOut.id,
name = labelOut.name name = labelOut.name,
) )
} }
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false) _uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
}, },
onFailure = { exception -> onFailure = { exception ->
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.") Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
_uiState.value = LabelsListUiState.Error( _uiState.value =
message = exception.message ?: "Could not load labels." LabelsListUiState.Error(
message = exception.message ?: "Could not load labels.",
) )
} },
) )
} }
} }
// [END_ENTITY: Function('loadLabels')]
// [ENTITY: Function('onShowCreateDialog')]
// [RELATION: Function('onShowCreateDialog') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('onShowCreateDialog') -> [CALLS] -> Function('_uiState.update')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Инициирует отображение диалога для создания метки. * @summary Инициирует отображение диалога для создания метки.
@@ -96,11 +116,15 @@ class LabelsListViewModel @Inject constructor(
} }
} }
} }
// [END_ENTITY: Function('onShowCreateDialog')]
// [ENTITY: Function('onDismissCreateDialog')]
// [RELATION: Function('onDismissCreateDialog') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('onDismissCreateDialog') -> [CALLS] -> Function('_uiState.update')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Скрывает диалог создания метки. * @summary Скрывает диалог создания метки.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`. * @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`..
* @sideeffect Обновляет `_uiState`. * @sideeffect Обновляет `_uiState`.
*/ */
// [ACTION] // [ACTION]
@@ -112,7 +136,12 @@ class LabelsListViewModel @Inject constructor(
} }
} }
} }
// [END_ENTITY: Function('onDismissCreateDialog')]
// [ENTITY: Function('createLabel')]
// [RELATION: Function('createLabel') -> [CALLS] -> Function('require')]
// [RELATION: Function('createLabel') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('createLabel') -> [CALLS] -> Function('onDismissCreateDialog')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА. * @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
@@ -127,7 +156,6 @@ class LabelsListViewModel @Inject constructor(
// [PRECONDITION] // [PRECONDITION]
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." } require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
// [ENTRYPOINT]
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]") Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase. // [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
@@ -136,5 +164,7 @@ class LabelsListViewModel @Inject constructor(
onDismissCreateDialog() onDismissCreateDialog()
// [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список. // [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список.
} }
} }
// [END_CLASS_LabelsListViewModel] // [END_ENTITY: ViewModel('LabelsListViewModel')]
// [END_CONTRACT]
// [END_FILE_LabelsListViewModel.kt]

View File

@@ -15,18 +15,23 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.homebox.lens.R import com.homebox.lens.R
// [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('LocationEditScreen')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('Box')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('Text')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Composable-функция для экрана "Редактирование местоположения". * @summary Composable-функция для экрана "Редактирование местоположения".
* @param locationId ID местоположения для редактирования или "new" для создания. * @param locationId ID местоположения для редактирования или "new" для создания.
*/ */
@Composable @Composable
fun LocationEditScreen( fun LocationEditScreen(locationId: String?) {
locationId: String? val title =
) { if (locationId == "new") {
val title = if (locationId == "new") {
stringResource(id = R.string.location_edit_title_create) stringResource(id = R.string.location_edit_title_create)
} else { } else {
stringResource(id = R.string.location_edit_title_edit) stringResource(id = R.string.location_edit_title_edit)
@@ -34,12 +39,16 @@ fun LocationEditScreen(
Scaffold { paddingValues -> Scaffold { paddingValues ->
Box( Box(
modifier = Modifier modifier =
Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues), .padding(paddingValues),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center,
) { ) {
Text(text = "TODO: Location Edit Screen for ID: $locationId") Text(text = "TODO: Location Edit Screen for ID: $locationId")
} }
} }
} }
// [END_ENTITY: Function('LocationEditScreen')]
// [END_CONTRACT]
// [END_FILE_LocationEditScreen.kt]

View File

@@ -49,8 +49,20 @@ import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.ui.common.MainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.ui.theme.HomeboxLensTheme
// [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('LocationsListScreen')]
// [RELATION: Function('LocationsListScreen') -> [DEPENDS_ON] -> Class('NavigationActions')]
// [RELATION: Function('LocationsListScreen') -> [DEPENDS_ON] -> Class('LocationsListViewModel')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('MainScaffold')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('FloatingActionButton')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('LocationsListContent')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Composable-функция для экрана "Список местоположений". * @summary Composable-функция для экрана "Список местоположений".
@@ -66,7 +78,7 @@ fun LocationsListScreen(
navigationActions: NavigationActions, navigationActions: NavigationActions,
onLocationClick: (String) -> Unit, onLocationClick: (String) -> Unit,
onAddNewLocationClick: () -> Unit, onAddNewLocationClick: () -> Unit,
viewModel: LocationsListViewModel = hiltViewModel() viewModel: LocationsListViewModel = hiltViewModel(),
) { ) {
// [STATE] // [STATE]
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
@@ -75,7 +87,7 @@ fun LocationsListScreen(
MainScaffold( MainScaffold(
topBarTitle = stringResource(id = R.string.locations_list_title), topBarTitle = stringResource(id = R.string.locations_list_title),
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions,
) { paddingValues -> ) { paddingValues ->
Scaffold( Scaffold(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
@@ -83,23 +95,32 @@ fun LocationsListScreen(
FloatingActionButton(onClick = onAddNewLocationClick) { FloatingActionButton(onClick = onAddNewLocationClick) {
Icon( Icon(
Icons.Default.Add, Icons.Default.Add,
contentDescription = stringResource(id = R.string.cd_add_new_location) contentDescription = stringResource(id = R.string.cd_add_new_location),
) )
} }
} },
) { innerPadding -> ) { innerPadding ->
LocationsListContent( LocationsListContent(
modifier = Modifier.padding(innerPadding), modifier = Modifier.padding(innerPadding),
uiState = uiState, uiState = uiState,
onLocationClick = onLocationClick, onLocationClick = onLocationClick,
onEditLocation = { /* TODO */ }, onEditLocation = { /* TODO */ },
onDeleteLocation = { /* TODO */ } onDeleteLocation = { /* TODO */ },
) )
} }
} }
} }
// [END_ENTITY: Function('LocationsListScreen')]
// [HELPER] // [ENTITY: Function('LocationsListContent')]
// [RELATION: Function('LocationsListContent') -> [DEPENDS_ON] -> SealedInterface('LocationsListUiState')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('Box')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('LocationCard')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Отображает основной контент экрана в зависимости от `uiState`. * @summary Отображает основной контент экрана в зависимости от `uiState`.
@@ -115,7 +136,7 @@ private fun LocationsListContent(
uiState: LocationsListUiState, uiState: LocationsListUiState,
onLocationClick: (String) -> Unit, onLocationClick: (String) -> Unit,
onEditLocation: (String) -> Unit, onEditLocation: (String) -> Unit,
onDeleteLocation: (String) -> Unit onDeleteLocation: (String) -> Unit,
) { ) {
Box(modifier = modifier.fillMaxSize()) { Box(modifier = modifier.fillMaxSize()) {
when (uiState) { when (uiState) {
@@ -127,9 +148,10 @@ private fun LocationsListContent(
text = uiState.message, text = uiState.message,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier =
Modifier
.align(Alignment.Center) .align(Alignment.Center)
.padding(16.dp) .padding(16.dp),
) )
} }
is LocationsListUiState.Success -> { is LocationsListUiState.Success -> {
@@ -137,21 +159,22 @@ private fun LocationsListContent(
Text( Text(
text = stringResource(id = R.string.locations_not_found), text = stringResource(id = R.string.locations_not_found),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier =
Modifier
.align(Alignment.Center) .align(Alignment.Center)
.padding(16.dp) .padding(16.dp),
) )
} else { } else {
LazyColumn( LazyColumn(
contentPadding = PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
items(uiState.locations, key = { it.id }) { location -> items(uiState.locations, key = { it.id }) { location ->
LocationCard( LocationCard(
location = location, location = location,
onClick = { onLocationClick(location.id) }, onClick = { onLocationClick(location.id) },
onEditClick = { onEditLocation(location.id) }, onEditClick = { onEditLocation(location.id) },
onDeleteClick = { onDeleteLocation(location.id) } onDeleteClick = { onDeleteLocation(location.id) },
) )
} }
} }
@@ -160,8 +183,26 @@ private fun LocationsListContent(
} }
} }
} }
// [END_ENTITY: Function('LocationsListContent')]
// [UI_COMPONENT] // [ENTITY: Function('LocationCard')]
// [RELATION: Function('LocationCard') -> [DEPENDS_ON] -> Class('LocationOutCount')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('remember')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('mutableStateOf')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Card')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('clickable')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Row')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('MaterialTheme.typography.bodyMedium')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Spacer')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Box')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('DropdownMenu')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('DropdownMenuItem')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Карточка для отображения одного местоположения. * @summary Карточка для отображения одного местоположения.
@@ -175,25 +216,26 @@ private fun LocationCard(
location: LocationOutCount, location: LocationOutCount,
onClick: () -> Unit, onClick: () -> Unit,
onEditClick: () -> Unit, onEditClick: () -> Unit,
onDeleteClick: () -> Unit onDeleteClick: () -> Unit,
) { ) {
var menuExpanded by remember { mutableStateOf(false) } var menuExpanded by remember { mutableStateOf(false) }
Card( Card(
modifier = Modifier modifier =
Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(onClick = onClick),
) { ) {
Row( Row(
modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp), modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text(text = location.name, style = MaterialTheme.typography.titleMedium) Text(text = location.name, style = MaterialTheme.typography.titleMedium)
Text( Text(
text = stringResource(id = R.string.item_count, location.itemCount), text = stringResource(id = R.string.item_count, location.itemCount),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium,
) )
} }
Spacer(Modifier.width(16.dp)) Spacer(Modifier.width(16.dp))
@@ -203,47 +245,59 @@ private fun LocationCard(
} }
DropdownMenu( DropdownMenu(
expanded = menuExpanded, expanded = menuExpanded,
onDismissRequest = { menuExpanded = false } onDismissRequest = { menuExpanded = false },
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(id = R.string.edit)) }, text = { Text(stringResource(id = R.string.edit)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
onEditClick() onEditClick()
} },
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(id = R.string.delete)) }, text = { Text(stringResource(id = R.string.delete)) },
onClick = { onClick = {
menuExpanded = false menuExpanded = false
onDeleteClick() onDeleteClick()
} },
) )
} }
} }
} }
} }
} }
// [END_ENTITY: Function('LocationCard')]
// [ENTITY: Function('LocationsListSuccessPreview')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('LocationOutCount')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('LocationsListUiState.Success')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Locations List Success") @Preview(showBackground = true, name = "Locations List Success")
@Composable @Composable
fun LocationsListSuccessPreview() { fun LocationsListSuccessPreview() {
val previewLocations = listOf( val previewLocations =
listOf(
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""), LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""), LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""),
LocationOutCount("3", "Office", "#0000FF", false, 23, "", "") LocationOutCount("3", "Office", "#0000FF", false, 23, "", ""),
) )
HomeboxLensTheme { HomeboxLensTheme {
LocationsListContent( LocationsListContent(
uiState = LocationsListUiState.Success(previewLocations), uiState = LocationsListUiState.Success(previewLocations),
onLocationClick = {}, onLocationClick = {},
onEditLocation = {}, onEditLocation = {},
onDeleteLocation = {} onDeleteLocation = {},
) )
} }
} }
// [END_ENTITY: Function('LocationsListSuccessPreview')]
// [ENTITY: Function('LocationsListEmptyPreview')]
// [RELATION: Function('LocationsListEmptyPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListEmptyPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListEmptyPreview') -> [CALLS] -> Function('LocationsListUiState.Success')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Locations List Empty") @Preview(showBackground = true, name = "Locations List Empty")
@Composable @Composable
@@ -253,11 +307,16 @@ fun LocationsListEmptyPreview() {
uiState = LocationsListUiState.Success(emptyList()), uiState = LocationsListUiState.Success(emptyList()),
onLocationClick = {}, onLocationClick = {},
onEditLocation = {}, onEditLocation = {},
onDeleteLocation = {} onDeleteLocation = {},
) )
} }
} }
// [END_ENTITY: Function('LocationsListEmptyPreview')]
// [ENTITY: Function('LocationsListLoadingPreview')]
// [RELATION: Function('LocationsListLoadingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListLoadingPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListLoadingPreview') -> [CALLS] -> Function('LocationsListUiState.Loading')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Locations List Loading") @Preview(showBackground = true, name = "Locations List Loading")
@Composable @Composable
@@ -267,11 +326,17 @@ fun LocationsListLoadingPreview() {
uiState = LocationsListUiState.Loading, uiState = LocationsListUiState.Loading,
onLocationClick = {}, onLocationClick = {},
onEditLocation = {}, onEditLocation = {},
onDeleteLocation = {} onDeleteLocation = {},
) )
} }
} }
// [END_ENTITY: Function('LocationsListLoadingPreview')]
// [ENTITY: Function('LocationsListErrorPreview')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('LocationsListUiState.Error')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('stringResource')]
// [PREVIEW] // [PREVIEW]
@Preview(showBackground = true, name = "Locations List Error") @Preview(showBackground = true, name = "Locations List Error")
@Composable @Composable
@@ -281,7 +346,10 @@ fun LocationsListErrorPreview() {
uiState = LocationsListUiState.Error("Failed to load locations. Please try again."), uiState = LocationsListUiState.Error("Failed to load locations. Please try again."),
onLocationClick = {}, onLocationClick = {},
onEditLocation = {}, onEditLocation = {},
onDeleteLocation = {} onDeleteLocation = {},
) )
} }
} }
// [END_ENTITY: Function('LocationsListErrorPreview')]
// [END_CONTRACT]
// [END_FILE_LocationsListScreen.kt]

View File

@@ -4,32 +4,45 @@
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
// [IMPORTS]
import com.homebox.lens.domain.model.LocationOutCount import com.homebox.lens.domain.model.LocationOutCount
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: SealedInterface('LocationsListUiState')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Определяет возможные состояния UI для экрана списка местоположений. * @summary Определяет возможные состояния UI для экрана списка местоположений.
* @see LocationsListViewModel * @see LocationsListViewModel
*/ */
sealed interface LocationsListUiState { sealed interface LocationsListUiState {
// [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LocationOutCount')]
/** /**
* [STATE] * [STATE]
* @summary Состояние успешной загрузки данных. * @summary Состояние успешной загрузки данных.
* @param locations Список местоположений для отображения. * @param locations Список местоположений для отображения.
*/ */
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
// [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')]
/** /**
* [STATE] * [STATE]
* @summary Состояние ошибки. * @summary Состояние ошибки.
* @param message Сообщение об ошибке. * @param message Сообщение об ошибке.
*/ */
data class Error(val message: String) : LocationsListUiState data class Error(val message: String) : LocationsListUiState
// [END_ENTITY: DataClass('Error')]
// [ENTITY: DataObject('Loading')]
/** /**
* [STATE] * [STATE]
* @summary Состояние загрузки данных. * @summary Состояние загрузки данных.
*/ */
object Loading : LocationsListUiState object Loading : LocationsListUiState
// [END_ENTITY: DataObject('Loading')]
} }
// [END_ENTITY: SealedInterface('LocationsListUiState')]
// [END_CONTRACT]
// [END_FILE_LocationsListUiState.kt] // [END_FILE_LocationsListUiState.kt]

View File

@@ -4,6 +4,7 @@
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
// [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
@@ -13,8 +14,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [CORE-LOGIC] // [CONTRACT]
// [ENTITY: ViewModel('LocationsListViewModel')]
// [RELATION: ViewModel('LocationsListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('LocationsListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('LocationsListViewModel') -> [DEPENDS_ON] -> Class('GetAllLocationsUseCase')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary ViewModel для экрана списка местоположений. * @summary ViewModel для экрана списка местоположений.
@@ -23,10 +29,11 @@ import javax.inject.Inject
* @invariant `uiState` всегда отражает результат последней операции загрузки. * @invariant `uiState` всегда отражает результат последней операции загрузки.
*/ */
@HiltViewModel @HiltViewModel
class LocationsListViewModel @Inject constructor( class LocationsListViewModel
private val getAllLocationsUseCase: GetAllLocationsUseCase @Inject
) : ViewModel() { constructor(
private val getAllLocationsUseCase: GetAllLocationsUseCase,
) : ViewModel() {
// [STATE] // [STATE]
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading) private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow() val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
@@ -36,7 +43,10 @@ class LocationsListViewModel @Inject constructor(
loadLocations() loadLocations()
} }
// [ACTION] // [ENTITY: Function('loadLocations')]
// [RELATION: Function('loadLocations') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadLocations') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('loadLocations') -> [CALLS] -> Function('getAllLocationsUseCase')]
/** /**
* [CONTRACT] * [CONTRACT]
* @summary Загружает список местоположений из репозитория. * @summary Загружает список местоположений из репозитория.
@@ -53,6 +63,8 @@ class LocationsListViewModel @Inject constructor(
} }
} }
} }
// [END_CLASS_LocationsListViewModel] // [END_ENTITY: Function('loadLocations')]
} }
// [END_ENTITY: ViewModel('LocationsListViewModel')]
// [END_CONTRACT]
// [END_FILE_LocationsListViewModel.kt] // [END_FILE_LocationsListViewModel.kt]

View File

@@ -1,37 +1,129 @@
// [PACKAGE] com.homebox.lens.ui.screen.search // [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt // [FILE] SearchScreen.kt
// [SEMANTICS] ui, screen, search // [SEMANTICS] ui, screen, search, compose
package com.homebox.lens.ui.screen.search package com.homebox.lens.ui.screen.search
// [IMPORTS] // [IMPORTS]
import androidx.compose.material3.Text import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable import androidx.compose.foundation.layout.*
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.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource 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.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.domain.model.Item
import com.homebox.lens.ui.common.MainScaffold // [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('SearchScreen')]
// [RELATION: Function('SearchScreen') -> [DEPENDS_ON] -> Class('SearchViewModel')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('TextField')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('SearchContent')]
/** /**
* [CONTRACT] * [MAIN-CONTRACT]
* @summary Composable-функция для экрана "Поиск". * Специализированный экран для поиска товаров.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. *
* @param navigationActions Объект с навигационными действиями. * Реализует спецификацию `screen_search`.
*
* @param onNavigateBack Обработчик для возврата на предыдущий экран.
* @param onItemClick Обработчик нажатия на найденный товар.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SearchScreen( fun SearchScreen(
currentRoute: String?, viewModel: SearchViewModel = hiltViewModel(),
navigationActions: NavigationActions onNavigateBack: () -> Unit,
onItemClick: (Item) -> Unit
) { ) {
// [UI_COMPONENT] // [STATE]
MainScaffold( val uiState by viewModel.uiState.collectAsState()
topBarTitle = stringResource(id = R.string.search_title),
currentRoute = currentRoute, Scaffold(
navigationActions = navigationActions topBar = {
) { TopAppBar(
// [CORE-LOGIC] title = {
Text(text = "TODO: Search Screen") TextField(
value = uiState.searchQuery,
onValueChange = viewModel::onSearchQueryChanged,
placeholder = { Text(stringResource(R.string.placeholder_search_items)) },
modifier = Modifier.fillMaxWidth()
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_navigate_back))
}
}
)
}
) { innerPadding ->
SearchContent(
modifier = Modifier.padding(innerPadding),
isLoading = uiState.isLoading,
results = uiState.results,
onItemClick = onItemClick
)
} }
// [END_FUNCTION_SearchScreen]
} }
// [END_ENTITY: Function('SearchScreen')]
// [ENTITY: Function('SearchContent')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('ListItem')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('clickable')]
/**
* [CONTRACT]
* Отображает основной контент экрана: фильтры и результаты поиска.
*/
@Composable
private fun SearchContent(
modifier: Modifier = Modifier,
isLoading: Boolean,
results: List<Item>,
onItemClick: (Item) -> Unit
) {
Column(modifier = modifier.fillMaxSize()) {
// [SECTION] FILTERS
// TODO: Implement FilterSection with chips for locations/labels
// Spacer(modifier = Modifier.height(8.dp))
// [SECTION] RESULTS
Box(modifier = Modifier.weight(1f)) {
if (isLoading) {
// [STATE]
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
// [CORE-LOGIC]
LazyColumn {
items(results, key = { it.id }) { item ->
ListItem(
headlineContent = { Text(item.name) },
supportingContent = { Text(item.location?.name ?: "") },
modifier = Modifier.then(Modifier.clickable { onItemClick(item) })
)
}
}
}
}
}
}
// [END_ENTITY: Function('SearchContent')]
// [END_CONTRACT]
// [END_FILE_SearchScreen.kt]

View File

@@ -1,16 +1,44 @@
// [PACKAGE] com.homebox.lens.ui.screen.search // [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchViewModel.kt // [FILE] SearchViewModel.kt
// [SEMANTICS] ui_logic, search, viewmodel
package com.homebox.lens.ui.screen.search package com.homebox.lens.ui.screen.search
// [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
// [END_IMPORTS]
// [VIEWMODEL] // [CONTRACT]
// [ENTITY: ViewModel('SearchViewModel')]
// [RELATION: ViewModel('SearchViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('SearchViewModel') -> [DEPENDS_ON] -> Annotation('HiltAndroidApp')]
/**
* [CONTRACT]
* @summary ViewModel for the SearchScreen.
*/
@HiltViewModel @HiltViewModel
class SearchViewModel @Inject constructor() : ViewModel() { class SearchViewModel
@Inject
constructor() : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI state // TODO: Implement UI state
} val uiState = MutableStateFlow(SearchUiState()).asStateFlow()
fun onSearchQueryChanged(query: String) {
// TODO: Implement search query change logic
}
}
// [END_ENTITY: ViewModel('SearchViewModel')]
// [END_CONTRACT]
// [END_FILE_SearchViewModel.kt] // [END_FILE_SearchViewModel.kt]
// Placeholder for SearchUiState to resolve compilation errors
data class SearchUiState(
val searchQuery: String = "",
val isLoading: Boolean = false,
val results: List<com.homebox.lens.domain.model.Item> = emptyList()
)

View File

@@ -1,34 +1,51 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup // [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt // [FILE] SetupScreen.kt
// [SEMANTICS] ui, screen, setup, compose // [SEMANTICS] ui, screen, setup, login, compose
@file:OptIn(ExperimentalMaterial3Api::class)
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R import com.homebox.lens.R
import timber.log.Timber
// [END_IMPORTS]
// [ENTRYPOINT] // [CONTRACT]
// [ENTITY: Function('SetupScreen')]
// [RELATION: Function('SetupScreen') -> [DEPENDS_ON] -> Class('SetupViewModel')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('LaunchedEffect')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Box')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Column')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('MaterialTheme.typography.headlineMedium')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('OutlinedTextField')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('KeyboardOptions')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('KeyboardType.Uri')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('PasswordVisualTransformation')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Button')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('MaterialTheme.typography.bodyMedium')]
/** /**
* [CONTRACT] * [MAIN-CONTRACT]
* @summary Главная Composable-функция для экрана настройки соединения с сервером. * Экран для начальной настройки соединения с сервером Homebox.
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt. *
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа. * @param onSetupComplete Обработчик, вызываемый после успешной настройки и входа.
* @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`.
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SetupScreen( fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(), viewModel: SetupViewModel = hiltViewModel(),
@@ -37,105 +54,73 @@ fun SetupScreen(
// [STATE] // [STATE]
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
// [CORE-LOGIC] // [SIDE-EFFECT]
LaunchedEffect(uiState.isSetupComplete) {
if (uiState.isSetupComplete) { if (uiState.isSetupComplete) {
Timber.i("[INFO][SIDE_EFFECT][navigation] Setup complete, navigating to main screen.")
onSetupComplete() onSetupComplete()
} }
// [UI_COMPONENT]
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
// [END_FUNCTION_SetupScreen]
}
// [HELPER]
/**
* [CONTRACT]
* @summary Отображает контент экрана настройки: поля ввода и кнопку.
* @param uiState Текущее состояние UI.
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
* @param onUsernameChange Лямбда-обработчик изменения имени пользователя.
* @param onPasswordChange Лямбда-обработчик изменения пароля.
* @param onConnectClick Лямбда-обработчик нажатия на кнопку "Подключиться".
*/
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
onServerUrlChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text(stringResource(id = R.string.setup_title)) })
} }
) { paddingValues ->
// [CORE-LOGIC]
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.padding(paddingValues) .padding(32.dp),
.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.spacedBy(16.dp)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text(text = stringResource(id = R.string.screen_title_setup), style = MaterialTheme.typography.headlineMedium)
OutlinedTextField( OutlinedTextField(
value = uiState.serverUrl, value = uiState.serverUrl,
onValueChange = onServerUrlChange, onValueChange = viewModel::onServerUrlChange,
label = { Text(stringResource(id = R.string.setup_server_url_label)) }, label = { Text(stringResource(id = R.string.setup_server_url_label)) },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
isError = uiState.error != null
) )
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField( OutlinedTextField(
value = uiState.username, value = uiState.password, // Changed from uiState.apiKey to uiState.password
onValueChange = onUsernameChange, onValueChange = viewModel::onPasswordChange, // Changed from viewModel::onApiKeyChange to viewModel::onPasswordChange
label = { Text(stringResource(id = R.string.setup_username_label)) }, label = { Text(stringResource(id = R.string.setup_password_label)) }, // Changed from label_api_key to setup_password_label
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text(stringResource(id = R.string.setup_password_label)) },
visualTransformation = PasswordVisualTransformation(), visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth() isError = uiState.error != null
) )
Spacer(modifier = Modifier.height(16.dp))
if (uiState.isLoading) {
// [STATE]
CircularProgressIndicator()
} else {
// [ACTION]
Button( Button(
onClick = onConnectClick, onClick = {
enabled = !uiState.isLoading, Timber.i("[INFO][ACTION][ui_interaction] Login button clicked.")
viewModel.connect() // Changed from viewModel.login() to viewModel.connect()
},
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
if (uiState.isLoading) { Text(text = stringResource(id = R.string.setup_connect_button)) // Changed from button_connect to setup_connect_button
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text(stringResource(id = R.string.setup_connect_button))
} }
} }
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
// [END_FUNCTION_SetupScreenContent]
}
// [PREVIEW] uiState.error?.let {
@Preview(showBackground = true) // [FALLBACK]
@Composable Text(
fun SetupScreenPreview() { text = it,
SetupScreenContent( color = MaterialTheme.colorScheme.error,
uiState = SetupUiState(error = "Failed to connect"), style = MaterialTheme.typography.bodyMedium
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {}
) )
}
}
}
} }
// [END_ENTITY: Function('SetupScreen')]
// [END_CONTRACT]
// [END_FILE_SetupScreen.kt] // [END_FILE_SetupScreen.kt]

View File

@@ -4,6 +4,11 @@
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
// [IMPORTS]
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DataClass('SetupUiState')]
/** /**
* [ENTITY: DataClass('SetupUiState')] * [ENTITY: DataClass('SetupUiState')]
* [CONTRACT] * [CONTRACT]
@@ -22,6 +27,8 @@ data class SetupUiState(
val password: String = "", val password: String = "",
val isLoading: Boolean = false, val isLoading: Boolean = false,
val error: String? = null, val error: String? = null,
val isSetupComplete: Boolean = false val isSetupComplete: Boolean = false,
) )
// [END_ENTITY: DataClass('SetupUiState')]
// [END_CONTRACT]
// [END_FILE_SetupUiState.kt] // [END_FILE_SetupUiState.kt]

View File

@@ -2,22 +2,27 @@
// [FILE] SetupViewModel.kt // [FILE] SetupViewModel.kt
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow // [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
package com.homebox.lens.ui.screen.setup package com.homebox.lens.ui.screen.setup
// [IMPORTS] // [IMPORTS]
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Credentials import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.usecase.LoginUseCase import com.homebox.lens.domain.usecase.LoginUseCase
import com.homebox.lens.ui.screen.setup.SetupUiState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS]
// [VIEWMODEL] // [CONTRACT]
// [ENTITY: ViewModel('SetupViewModel')] // [ENTITY: ViewModel('SetupViewModel')]
// [RELATION: ViewModel('SetupViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('SetupViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('SetupViewModel') -> [DEPENDS_ON] -> Class('CredentialsRepository')]
// [RELATION: ViewModel('SetupViewModel') -> [DEPENDS_ON] -> Class('LoginUseCase')]
/** /**
* [CONTRACT] * [CONTRACT]
* ViewModel для экрана первоначальной настройки (Setup). * ViewModel для экрана первоначальной настройки (Setup).
@@ -30,11 +35,12 @@ import javax.inject.Inject
* @invariant Состояние `uiState` всегда является единственным источником истины для UI. * @invariant Состояние `uiState` всегда является единственным источником истины для UI.
*/ */
@HiltViewModel @HiltViewModel
class SetupViewModel @Inject constructor( class SetupViewModel
@Inject
constructor(
private val credentialsRepository: CredentialsRepository, private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase private val loginUseCase: LoginUseCase,
) : ViewModel() { ) : ViewModel() {
// [STATE] // [STATE]
private val _uiState = MutableStateFlow(SetupUiState()) private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
@@ -45,13 +51,17 @@ class SetupViewModel @Inject constructor(
loadCredentials() loadCredentials()
} }
// [ENTITY: Function('loadCredentials')]
// [RELATION: Function('loadCredentials') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadCredentials') -> [CALLS] -> Function('credentialsRepository.getCredentials')]
// [RELATION: Function('loadCredentials') -> [CALLS] -> Function('collect')]
// [RELATION: Function('loadCredentials') -> [WRITES_TO] -> Property('_uiState')]
/** /**
* [CONTRACT] * [CONTRACT]
* [HELPER] Загружает учетные данные из репозитория при инициализации. * @summary Загружает учетные данные из репозитория при инициализации.
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными. * @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
*/ */
private fun loadCredentials() { private fun loadCredentials() {
// [ENTRYPOINT]
viewModelScope.launch { viewModelScope.launch {
// [CORE-LOGIC] Подписываемся на поток учетных данных. // [CORE-LOGIC] Подписываемся на поток учетных данных.
credentialsRepository.getCredentials().collect { credentials -> credentialsRepository.getCredentials().collect { credentials ->
@@ -61,14 +71,17 @@ class SetupViewModel @Inject constructor(
it.copy( it.copy(
serverUrl = credentials.serverUrl, serverUrl = credentials.serverUrl,
username = credentials.username, username = credentials.username,
password = credentials.password password = credentials.password,
) )
} }
} }
} }
} }
} }
// [END_ENTITY: Function('loadCredentials')]
// [ENTITY: Function('onServerUrlChange')]
// [RELATION: Function('onServerUrlChange') -> [WRITES_TO] -> Property('_uiState')]
/** /**
* [CONTRACT] * [CONTRACT]
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя. * [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
@@ -78,7 +91,10 @@ class SetupViewModel @Inject constructor(
fun onServerUrlChange(newUrl: String) { fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) } _uiState.update { it.copy(serverUrl = newUrl) }
} }
// [END_ENTITY: Function('onServerUrlChange')]
// [ENTITY: Function('onUsernameChange')]
// [RELATION: Function('onUsernameChange') -> [WRITES_TO] -> Property('_uiState')]
/** /**
* [CONTRACT] * [CONTRACT]
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя. * [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
@@ -88,7 +104,10 @@ class SetupViewModel @Inject constructor(
fun onUsernameChange(newUsername: String) { fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) } _uiState.update { it.copy(username = newUsername) }
} }
// [END_ENTITY: Function('onUsernameChange')]
// [ENTITY: Function('onPasswordChange')]
// [RELATION: Function('onPasswordChange') -> [WRITES_TO] -> Property('_uiState')]
/** /**
* [CONTRACT] * [CONTRACT]
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя. * [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
@@ -98,7 +117,15 @@ class SetupViewModel @Inject constructor(
fun onPasswordChange(newPassword: String) { fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) } _uiState.update { it.copy(password = newPassword) }
} }
// [END_ENTITY: Function('onPasswordChange')]
// [ENTITY: Function('connect')]
// [RELATION: Function('connect') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('connect') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('connect') -> [CREATES_INSTANCE_OF] -> Class('Credentials')]
// [RELATION: Function('connect') -> [CALLS] -> Function('credentialsRepository.saveCredentials')]
// [RELATION: Function('connect') -> [CALLS] -> Function('loginUseCase')]
// [RELATION: Function('connect') -> [CALLS] -> Function('fold')]
/** /**
* [CONTRACT] * [CONTRACT]
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя. * [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
@@ -110,16 +137,16 @@ class SetupViewModel @Inject constructor(
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен. * @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
*/ */
fun connect() { fun connect() {
// [ENTRYPOINT]
viewModelScope.launch { viewModelScope.launch {
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку. // [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
_uiState.update { it.copy(isLoading = true, error = null) } _uiState.update { it.copy(isLoading = true, error = null) }
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов. // [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
val credentials = Credentials( val credentials =
Credentials(
serverUrl = _uiState.value.serverUrl.trim(), serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(), username = _uiState.value.username.trim(),
password = _uiState.value.password password = _uiState.value.password,
) )
// [ACTION] Сохраняем учетные данные для будущего использования. // [ACTION] Сохраняем учетные данные для будущего использования.
@@ -134,10 +161,12 @@ class SetupViewModel @Inject constructor(
onFailure = { exception -> onFailure = { exception ->
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке. // [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") } _uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
} },
) )
} }
} }
// [END_CLASS_SetupViewModel] // [END_ENTITY: Function('connect')]
} }
// [END_ENTITY: ViewModel('SetupViewModel')]
// [END_CONTRACT]
// [END_FILE_SetupViewModel.kt] // [END_FILE_SetupViewModel.kt]

View File

@@ -1,16 +1,36 @@
// [PACKAGE] com.homebox.lens.ui.theme // [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Color.kt // [FILE] Color.kt
// [SEMANTICS] ui, theme, color
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS]
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Constant('Purple80')]
val Purple80 = Color(0xFFD0BCFF) val Purple80 = Color(0xFFD0BCFF)
// [END_ENTITY: Constant('Purple80')]
// [ENTITY: Constant('PurpleGrey80')]
val PurpleGrey80 = Color(0xFFCCC2DC) val PurpleGrey80 = Color(0xFFCCC2DC)
// [END_ENTITY: Constant('PurpleGrey80')]
// [ENTITY: Constant('Pink80')]
val Pink80 = Color(0xFFEFB8C8) val Pink80 = Color(0xFFEFB8C8)
// [END_ENTITY: Constant('Pink80')]
// [ENTITY: Constant('Purple40')]
val Purple40 = Color(0xFF6650a4) val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71) // [END_ENTITY: Constant('Purple40')]
val Pink40 = Color(0xFF7D5260)
// [ENTITY: Constant('PurpleGrey40')]
val PurpleGrey40 = Color(0xFF625b71)
// [END_ENTITY: Constant('PurpleGrey40')]
// [ENTITY: Constant('Pink40')]
val Pink40 = Color(0xFF7D5260)
// [END_ENTITY: Constant('Pink40')]
// [END_CONTRACT]
// [END_FILE_Color.kt] // [END_FILE_Color.kt]

View File

@@ -1,8 +1,10 @@
// [PACKAGE] com.homebox.lens.ui.theme // [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt // [FILE] Theme.kt
// [SEMANTICS] ui, theme, color_scheme
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS]
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@@ -17,27 +19,57 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
// [END_IMPORTS]
private val DarkColorScheme = darkColorScheme( // [CONTRACT]
// [ENTITY: Constant('DarkColorScheme')]
// [RELATION: Constant('DarkColorScheme') -> [CALLS] -> Function('darkColorScheme')]
// [RELATION: Constant('DarkColorScheme') -> [DEPENDS_ON] -> Constant('Purple80')]
// [RELATION: Constant('DarkColorScheme') -> [DEPENDS_ON] -> Constant('PurpleGrey80')]
// [RELATION: Constant('DarkColorScheme') -> [DEPENDS_ON] -> Constant('Pink80')]
private val DarkColorScheme =
darkColorScheme(
primary = Purple80, primary = Purple80,
secondary = PurpleGrey80, secondary = PurpleGrey80,
tertiary = Pink80 tertiary = Pink80,
) )
// [END_ENTITY: Constant('DarkColorScheme')]
private val LightColorScheme = lightColorScheme( // [ENTITY: Constant('LightColorScheme')]
// [RELATION: Constant('LightColorScheme') -> [CALLS] -> Function('lightColorScheme')]
// [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('Purple40')]
// [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('PurpleGrey40')]
// [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('Pink40')]
private val LightColorScheme =
lightColorScheme(
primary = Purple40, primary = Purple40,
secondary = PurpleGrey40, secondary = PurpleGrey40,
tertiary = Pink40 tertiary = Pink40,
) )
// [END_ENTITY: Constant('LightColorScheme')]
// [ENTITY: Function('HomeboxLensTheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('isSystemInDarkTheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('LocalContext.current')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('dynamicDarkColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('dynamicLightColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('LocalView.current')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('SideEffect')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('toArgb')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('WindowCompat.getInsetsController')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('MaterialTheme')]
// [RELATION: Function('HomeboxLensTheme') -> [DEPENDS_ON] -> Constant('DarkColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [DEPENDS_ON] -> Constant('LightColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [DEPENDS_ON] -> Constant('Typography')]
@Composable @Composable
fun HomeboxLensTheme( fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
val colorScheme = when { val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
@@ -58,7 +90,9 @@ fun HomeboxLensTheme(
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography, typography = Typography,
content = content content = content,
) )
} }
// [END_ENTITY: Function('HomeboxLensTheme')]
// [END_CONTRACT]
// [END_FILE_Theme.kt] // [END_FILE_Theme.kt]

View File

@@ -1,23 +1,37 @@
// [PACKAGE] com.homebox.lens.ui.theme // [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Typography.kt // [FILE] Typography.kt
// [SEMANTICS] ui, theme, typography
package com.homebox.lens.ui.theme package com.homebox.lens.ui.theme
// [IMPORTS]
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// [END_IMPORTS]
// Set of Material typography styles to start with // [CONTRACT]
val Typography = Typography( // [ENTITY: Constant('Typography')]
bodyLarge = TextStyle( // [RELATION: Constant('Typography') -> [CALLS] -> Function('Typography')]
// [RELATION: Constant('Typography') -> [CALLS] -> Function('TextStyle')]
// [RELATION: Constant('Typography') -> [DEPENDS_ON] -> Class('FontFamily')]
// [RELATION: Constant('Typography') -> [DEPENDS_ON] -> Class('FontWeight')]
/**
* Set of Material typography styles to start with
*/
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.5.sp letterSpacing = 0.5.sp,
),
) )
) // [END_ENTITY: Constant('Typography')]
// [END_CONTRACT]
// [END_FILE_Typography.kt] // [END_FILE_Typography.kt]

View File

@@ -3,6 +3,8 @@
<!-- Common --> <!-- Common -->
<string name="create">Create</string> <string name="create">Create</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="logout">Logout</string> <string name="logout">Logout</string>
<string name="no_location">No location</string> <string name="no_location">No location</string>
@@ -14,7 +16,36 @@
<string name="cd_scan_qr_code">Scan QR code</string> <string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Navigate back</string> <string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Add new location</string> <string name="cd_add_new_location">Add new location</string>
<string name="cd_add_new_label">Add new label</string> <string name="content_desc_add_label">Add new label</string>
<string name="content_desc_sync_inventory">Sync inventory</string>
<string name="content_desc_edit_item">Edit item</string>
<string name="content_desc_delete_item">Delete item</string>
<string name="content_desc_save_item">Save item</string>
<string name="content_desc_create_label">Create new label</string>
<string name="content_desc_label_icon">Label icon</string>
<string name="cd_more_options">More options</string>
<!-- Inventory List Screen -->
<string name="inventory_list_title">Inventory</string>
<!-- Item Details Screen -->
<string name="item_details_title">Details</string>
<string name="section_title_description">Description</string>
<string name="placeholder_no_description">No description</string>
<string name="section_title_details">Details</string>
<string name="label_quantity">Quantity</string>
<string name="label_location">Location</string>
<string name="section_title_labels">Labels</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Create item</string>
<string name="item_edit_title">Edit item</string>
<string name="label_name">Name</string>
<string name="label_description">Description</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Search items...</string>
<string name="search_title">Search</string>
<!-- Dashboard Screen --> <!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string> <string name="dashboard_title">Dashboard</string>
@@ -34,11 +65,32 @@
<string name="nav_locations">Locations</string> <string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string> <string name="nav_labels">Labels</string>
<!-- Screen Titles -->
<string name="labels_list_title">Labels</string>
<string name="locations_list_title">Locations</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Create location</string>
<string name="location_edit_title_edit">Edit location</string>
<!-- Locations List Screen -->
<string name="locations_not_found">Locations not found. Press + to add a new one.</string>
<string name="item_count">Items: %1$d</string>
<!-- Setup Screen --> <!-- Setup Screen -->
<string name="screen_title_setup">Setup</string>
<string name="setup_title">Server Setup</string> <string name="setup_title">Server Setup</string>
<string name="setup_server_url_label">Server URL</string> <string name="setup_server_url_label">Server URL</string>
<string name="setup_username_label">Username</string> <string name="setup_username_label">Username</string>
<string name="setup_password_label">Password</string> <string name="setup_password_label">Password</string>
<string name="setup_connect_button">Connect</string> <string name="setup_connect_button">Connect</string>
<!-- Labels List Screen -->
<string name="screen_title_labels">Labels</string>
<string name="no_labels_found">No labels found.</string>
<string name="dialog_title_create_label">Create label</string>
<string name="dialog_field_label_name">Label name</string>
<string name="dialog_button_create">Create</string>
<string name="dialog_button_cancel">Cancel</string>
</resources> </resources>

View File

@@ -16,7 +16,29 @@
<string name="cd_scan_qr_code">Сканировать QR-код</string> <string name="cd_scan_qr_code">Сканировать QR-код</string>
<string name="cd_navigate_back">Вернуться назад</string> <string name="cd_navigate_back">Вернуться назад</string>
<string name="cd_add_new_location">Добавить новую локацию</string> <string name="cd_add_new_location">Добавить новую локацию</string>
<string name="cd_add_new_label">Добавить новую метку</string> <string name="content_desc_add_label">Добавить новую метку</string>
<!-- Inventory List Screen -->
<string name="content_desc_sync_inventory">Синхронизировать инвентарь</string>
<!-- Item Details Screen -->
<string name="content_desc_edit_item">Редактировать элемент</string>
<string name="content_desc_delete_item">Удалить элемент</string>
<string name="section_title_description">Описание</string>
<string name="placeholder_no_description">Нет описания</string>
<string name="section_title_details">Детали</string>
<string name="label_quantity">Количество</string>
<string name="label_location">Местоположение</string>
<string name="section_title_labels">Метки</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Создать элемент</string>
<string name="content_desc_save_item">Сохранить элемент</string>
<string name="label_name">Название</string>
<string name="label_description">Описание</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Поиск элементов...</string>
<!-- Dashboard Screen --> <!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string> <string name="dashboard_title">Главная</string>
@@ -54,6 +76,7 @@
<string name="cd_more_options">Больше опций</string> <string name="cd_more_options">Больше опций</string>
<!-- Setup Screen --> <!-- Setup Screen -->
<string name="screen_title_setup">Настройка</string>
<string name="setup_title">Настройка сервера</string> <string name="setup_title">Настройка сервера</string>
<string name="setup_server_url_label">URL сервера</string> <string name="setup_server_url_label">URL сервера</string>
<string name="setup_username_label">Имя пользователя</string> <string name="setup_username_label">Имя пользователя</string>
@@ -62,15 +85,13 @@
<!-- Labels List Screen --> <!-- Labels List Screen -->
<string name="screen_title_labels">Метки</string> <string name="screen_title_labels">Метки</string>
<string name="content_desc_navigate_back">Вернуться назад</string> <string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
<string name="content_desc_create_label">Создать новую метку</string> <string name="content_desc_create_label">Создать новую метку</string>
<string name="content_desc_label_icon">Иконка метки</string> <string name="content_desc_label_icon">Иконка метки</string>
<string name="labels_list_empty">Метки еще не созданы.</string> <string name="no_labels_found">Метки не найдены.</string>
<string name="dialog_title_create_label">Создать метку</string> <string name="dialog_title_create_label">Создать метку</string>
<string name="dialog_field_label_name">Название метки</string> <string name="dialog_field_label_name">Название метки</string>
<string name="dialog_button_create">Создать</string> <string name="dialog_button_create">Создать</string>
<string name="dialog_button_cancel">Отмена</string> <string name="dialog_button_cancel">Отмена</string>
</resources> </resources>

1
data/semantic-ktlint-rules/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,18 @@
// Файл: /data/semantic-ktlint-rules/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
// Зависимость для RuleSetProviderV3
implementation("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.2.1")
// Зависимость для Rule, RuleId и psi-утилит
api("com.pinterest.ktlint:ktlint-rule-engine:1.2.1")
// Зависимости для тестирования остаются без изменений
testImplementation(kotlin("test"))
testImplementation("com.pinterest.ktlint:ktlint-test:1.2.1")
testImplementation("org.assertj:assertj-core:3.24.2")
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,24 @@
package com.busya.ktlint.rules
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.busya.ktlint.rules", appContext.packageName)
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HomeboxLens" />
</manifest>

View File

@@ -0,0 +1,16 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/CustomRuleSetProvider.kt
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId("custom")) {
override fun getRuleProviders(): Set<RuleProvider> {
return setOf(
RuleProvider { FileHeaderRule() },
RuleProvider { MandatoryEntityDeclarationRule() },
RuleProvider { NoStrayCommentsRule() }
)
}
}

View File

@@ -0,0 +1,33 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/FileHeaderRule.kt
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class FileHeaderRule : Rule(ruleId = RuleId("custom:file-header-rule"), about = About()) {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.FILE) {
val lines = node.text.lines()
if (lines.size < 3) {
emit(node.startOffset, "File must start with a 3-line semantic header.", false)
return
}
if (!lines[0].startsWith("// [PACKAGE]")) {
emit(node.startOffset, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.", false)
}
if (!lines[1].startsWith("// [FILE]")) {
emit(node.startOffset + lines[0].length + 1, "File header missing or incorrect. Line 2 must be '// [FILE] ...'.", false)
}
if (!lines[2].startsWith("// [SEMANTICS]")) {
emit(node.startOffset + lines[0].length + lines[1].length + 2, "File header missing or incorrect. Line 3 must be '// [SEMANTICS] ...'.", false)
}
}
}
}

View File

@@ -0,0 +1,40 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/MandatoryEntityDeclarationRule.kt
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
class MandatoryEntityDeclarationRule : Rule(ruleId = RuleId("custom:entity-declaration-rule"), about = About()) {
private val entityTypes = setOf(
ElementType.CLASS,
ElementType.OBJECT_DECLARATION,
ElementType.FUN
)
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType in entityTypes) {
val ktDeclaration = node.psi as? KtDeclaration ?: return
if (node.elementType == ElementType.FUN &&
(ktDeclaration.hasModifier(KtTokens.PRIVATE_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.PROTECTED_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.INTERNAL_KEYWORD))
) {
return
}
val prevComment = node.prevLeaf { it.elementType == ElementType.EOL_COMMENT }
if (prevComment == null || !prevComment.text.startsWith("// [ENTITY:")) {
emit(node.startOffset, "Missing or misplaced '// [ENTITY: ...]' declaration before '${node.elementType}'.", false)
}
}
}
}

View File

@@ -0,0 +1,24 @@
// Путь: data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/NoStrayCommentsRule.kt
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class NoStrayCommentsRule : Rule(ruleId = RuleId("custom:no-stray-comments-rule"), about = About()) {
private val allowedCommentPattern = Regex("""^//\s?\[([A-Z_]+|ENTITY:|RELATION:|AI_NOTE:)]""")
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.EOL_COMMENT) {
val commentText = node.text
if (!allowedCommentPattern.matches(commentText)) {
emit(node.startOffset, "Stray comment found. Use semantic anchors like '// [TAG]' or '// [AI_NOTE]:' instead.", false)
}
}
}
}

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.HomeboxLens" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">semantic-ktlint-rules</string>
</resources>

View File

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.HomeboxLens" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -0,0 +1 @@
com.busya.ktlint.rules.CustomRuleSetProvider

View File

@@ -0,0 +1,41 @@
package com.busya.ktlint.rules
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import org.junit.jupiter.api.Test
class FileHeaderRuleTest {
private val ruleAssertThat = assertThatRule { FileHeaderRule() }
@Test
fun `should pass on correct header`() {
val code = """
// [PACKAGE] com.example
// [FILE] Test.kt
// [SEMANTICS] test, example
package com.example
""".trimIndent()
ruleAssertThat(code).hasNoLintViolations()
}
@Test
fun `should fail on missing header`() {
val code = """
package com.example
""".trimIndent()
ruleAssertThat(code)
.hasLintViolation(1, 1, "File must start with a 3-line semantic header.")
}
@Test
fun `should fail on incorrect line 1`() {
val code = """
// [WRONG_TAG] com.example
// [FILE] Test.kt
// [SEMANTICS] test, example
package com.example
""".trimIndent()
ruleAssertThat(code)
.hasLintViolation(1, 1, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.")
}
}

View File

@@ -12,6 +12,7 @@ import java.math.BigDecimal
* @property id Уникальный идентификатор вещи. * @property id Уникальный идентификатор вещи.
* @property name Название вещи. * @property name Название вещи.
* @property description Описание вещи. * @property description Описание вещи.
* @property quantity Количество.
* @property image Url изображения. * @property image Url изображения.
* @property location Местоположение вещи. * @property location Местоположение вещи.
* @property labels Список меток, присвоенных вещи. * @property labels Список меток, присвоенных вещи.
@@ -22,6 +23,7 @@ data class Item(
val id: String, val id: String,
val name: String, val name: String,
val description: String?, val description: String?,
val quantity: Int,
val image: String?, val image: String?,
val location: Location?, val location: Location?,
val labels: List<Label>, val labels: List<Label>,

View File

@@ -18,7 +18,7 @@ distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
org.gradle.java.home=/usr/lib/jvm/java-25-openjdk-amd64 org.gradle.java.home=/usr/lib/jvm/java-21-openjdk-amd64
android.useAndroidX=true android.useAndroidX=true
# [ACTION] ??????????? ???????????? ????? ?????? (heap size) ??? Gradle ?? 4 ??. # [ACTION] ??????????? ???????????? ????? ?????? (heap size) ??? Gradle ?? 4 ??.

263
logs/communication_log.xml Normal file
View File

@@ -0,0 +1,263 @@
<LOG_ENTRY timestamp="2025-08-18T12:00:00Z">
<TASK_FILE>01.xml</TASK_FILE>
<FULL_PATH>/home/busya/dev/homebox_lens/tasks/completed/01.xml</FULL_PATH>
<STATUS>COMPLETED</STATUS>
<MESSAGE>Task completed successfully. Linter check was run and reported issues.</MESSAGE>
<DETAILS>
<LINTER_REPORT><![CDATA[Starting a Gradle Daemon, 1 busy and 2 incompatible Daemons could not be reused, use --status for details
> Task :buildSrc:checkKotlinGradlePluginConfigurationErrors SKIPPED
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :data:semantic-ktlint-rules:checkKotlinGradlePluginConfigurationErrors
> Task :data:semantic-ktlint-rules:compileKotlin UP-TO-DATE
> Task :data:semantic-ktlint-rules:compileJava NO-SOURCE
> Task :data:semantic-ktlint-rules:processResources NO-SOURCE
> Task :data:semantic-ktlint-rules:classes UP-TO-DATE
> Task :data:semantic-ktlint-rules:jar UP-TO-DATE
> Task :app:loadKtlintReporters UP-TO-DATE
> Task :app:runKtlintCheckOverAndroidTestDebugSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverAndroidTestReleaseSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverAndroidTestSourceSet NO-SOURCE
> Task :app:ktlintAndroidTestSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverDebugSourceSet NO-SOURCE
> Task :app:ktlintDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverKotlinScripts UP-TO-DATE
> Task :app:ktlintKotlinScriptCheck UP-TO-DATE
> Task :app:runKtlintCheckOverReleaseSourceSet NO-SOURCE
> Task :app:ktlintReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestDebugSourceSet NO-SOURCE
> Task :app:ktlintTestDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesDebugSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesDebugSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesReleaseSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestFixturesSourceSet NO-SOURCE
> Task :app:ktlintTestFixturesSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestReleaseSourceSet NO-SOURCE
> Task :app:ktlintTestReleaseSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverTestSourceSet NO-SOURCE
> Task :app:ktlintTestSourceSetCheck SKIPPED
> Task :app:runKtlintCheckOverMainSourceSet
> Task :app:ktlintMainSourceSetCheck FAILED
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:1:1 File must end with a newline (\n)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:43:1 Expected a blank line for this declaration
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:43:1 a KDoc may not be preceded by an EOL comment unless separated by a blank line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:56:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:59:31 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:73:106 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:76:18 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:86:99 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:89:10 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:95:64 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:100:42 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:112:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:112:23 Newline expected after opening parenthesis
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:112:38 Parameter should start on a newline
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:112:69 Newline expected before closing parenthesis
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:116:20 A multiline expression should start on a new line
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:120:80 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:130:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:133:32 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:143:60 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:165:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:167:24 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:174:20 A multiline expression should start on a new line
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt:177:42 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:1:1 File must end with a newline (\n)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:7:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:14:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:29:1 Expected a blank line for this declaration
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:29:1 a KDoc may not be preceded by an EOL comment unless separated by a blank line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:41:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:44:31 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:55:1 Exceeded max line length (140) (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:55:30 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:55:67 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:55:103 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:55:143 Missing newline before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:55:144 Missing newline before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:75:18 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:77:10 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:82:32 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:93:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:96:16 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:111:32 A multiline expression should start on a new line
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:115:70 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:155:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:155:28 Newline expected after opening parenthesis
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:155:43 Parameter should start on a newline
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:155:86 Newline expected before closing parenthesis
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:169:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:169:21 Newline expected after opening parenthesis
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:169:36 Parameter should start on a newline
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt:169:49 Newline expected before closing parenthesis
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:1:1 File must end with a newline (\n)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:7:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:13:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:39:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:41:31 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:57:1 Exceeded max line length (140) (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:57:30 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:57:67 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:57:145 Missing newline before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:57:146 Missing newline before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:60:1 Exceeded max line length (140) (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:60:30 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:60:67 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:60:103 Argument should be on a separate line (unless all arguments can fit a single line)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:60:143 Missing newline before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:60:144 Missing newline before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:70:18 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:72:10 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:79:66 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:90:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:95:39 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:99:20 A multiline expression should start on a new line
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:103:58 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:110:46 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:121:25 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt:129:50 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:1:1 File must end with a newline (\n)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:52:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:55:31 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:69:106 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:72:18 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:79:94 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:82:10 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:88:44 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:107:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:109:34 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:122:18 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:132:1 Expected a blank line for this declaration
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:132:1 a KDoc may not be preceded by an EOL comment unless separated by a blank line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:142:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:144:24 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:155:42 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:155:43 A comment in a 'value_argument_list' is only allowed when placed on a separate line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt:158:57 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:1:1 File must end with a newline (\n)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:7:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:12:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:13:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:17:1 Unused import
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:24:1 Expected a blank line for this declaration
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:24:1 a KDoc may not be preceded by an EOL comment unless separated by a blank line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:36:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:39:32 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:52:59 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:59:18 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:61:10 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:67:38 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:78:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:82:32 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt:101:80 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:1:1 File must end with a newline (\n)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:7:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:9:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:10:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:23:1 Expected a blank line for this declaration
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:23:1 a KDoc may not be preceded by an EOL comment unless separated by a blank line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:31:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:33:32 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:49:44 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:52:24 A multiline expression should start on a new line
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:56:62 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:66:48 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:75:48 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:88:55 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt:99:64 Missing trailing comma before ")"
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/MainActivity.kt:47:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/MainActivity.kt:60:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt:37:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt:16:5 a KDoc may not be preceded by an EOL comment unless separated by a blank line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/navigation/Screen.kt:30:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/navigation/Screen.kt:54:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/navigation/Screen.kt:74:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/navigation/Screen.kt:98:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt:34:14 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt:11:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt:34:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:7:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:15:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:27:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:43:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:59:89 A comment in a 'value_argument_list' is only allowed when placed on a separate line (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:90:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:139:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:193:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:210:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:243:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:274:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:305:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:330:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:409:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt:422:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt:13:1 Wildcard import (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt:50:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt:95:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt:111:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt:130:9 an EOL comment may not be preceded by a KDoc. Reversed order is allowed though when separated by a newline. (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationedit/LocationEditScreen.kt:27:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:65:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:115:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:179:13 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:237:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:257:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:271:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt:285:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
/home/busya/dev/homebox_lens/app/src/main/java/com/homebox/lens/ui/theme/Theme.kt:36:5 Function name should start with a lowercase letter (except factory methods) and use camel case (cannot be auto-corrected)
[Incubating] Problems report is available at: file:///home/busya/dev/homebox_lens/build/reports/problems/problems-report.html
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.13/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
11 actionable tasks: 3 executed, 8 up-to-date
Stderr:
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ":app:ktlintMainSourceSetCheck".
> A failure occurred while executing org.jlleitschuh.gradle.ktlint.worker.ConsoleReportWorkAction
> KtLint found code style violations. Please see the following reports:
- /home/busya/dev/homebox_lens/app/build/reports/ktlint/ktlintMainSourceSetCheck/ktlintMainSourceSetCheck.txt
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
BUILD FAILED in 10s
]]></LINTER_REPORT>
</DETAILS>
</LOG_ENTRY>
<LOG_ENTRY timestamp="2025-08-18T15:00:04+0300">
<TASK_FILE>20250818.xml</TASK_FILE>
<FULL_PATH>/home/busya/dev/homebox_lens/tasks/20250818.xml</FULL_PATH>
<STATUS>COMPLETED</STATUS>
<MESSAGE>File LabelsListUiState.kt already matches the specification. No changes applied.</MESSAGE>
<DETAILS>
<!-- No specific details needed as no changes were made -->
</DETAILS>
</LOG_ENTRY>
<LOG_ENTRY timestamp="2025-08-18T15:08:34+0300">
<TASK_FILE>002.xml</TASK_FILE>
<FULL_PATH>/home/busya/dev/homebox_lens/tasks/002.xml</FULL_PATH>
<WORK_ORDER_ID>intent-20250812-144001-LabelsListScreenUi</WORK_ORDER_ID>
<STATUS>COMPLETED</STATUS>
<MESSAGE>Implemented UI for LabelsListScreen.kt based on the intent specification.</MESSAGE>
<DETAILS>
<TARGET_FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</TARGET_FILE>
</DETAILS>
</LOG_ENTRY>

View File

@@ -23,3 +23,4 @@ include(":data")
include(":domain") include(":domain")
// [END_FILE_settings.gradle.kts] // [END_FILE_settings.gradle.kts]
include(":data:semantic-ktlint-rules")

View File

@@ -1,52 +0,0 @@
<!-- tasks/20250813_093000_clarify_logging_spec.xml -->
<TASK status="pending">
<WORK_ORDER id="task-20250813093000-002-spec-update">
<ACTION>MODIFY_SPECIFICATION</ACTION>
<TARGET_FILE>PROJECT_SPECIFICATION.xml</TARGET_FILE>
<GOAL>
Уточнить техническое решение по логированию (id="tech_logging"), добавив конкретный пример использования Timber.
Это устранит неоднозначность и предотвратит генерацию некорректного кода для логирования в будущем, предоставив ясный и копируемый образец.
</GOAL>
<CONTEXT_FILES>
<FILE>PROJECT_SPECIFICATION.xml</FILE>
</CONTEXT_FILES>
<PAYLOAD mode="APPEND_CHILD" target_node_xpath="//TECHNICAL_DECISIONS/DECISION[@id='tech_logging']">
<![CDATA[
<EXAMPLE lang="kotlin">
<summary>Пример корректного использования Timber</summary>
<code>
<![CDATA[
// Правильно: Прямой вызов статических методов Timber.
// Для информационных сообщений (INFO):
Timber.i("User logged in successfully. UserId: %s", userId)
// Для отладочных сообщений (DEBUG):
Timber.d("Starting network request to /items")
// Для ошибок (ERROR):
try {
// какая-то операция, которая может провалиться
} catch (e: Exception) {
Timber.e(e, "Failed to fetch user profile.")
}
// НЕПРАВИЛЬНО: Попытка создать экземпляр логгера.
// val logger = Timber.tag("MyScreen") // Избегать этого!
// logger.info("Some message") // Этот метод не существует в API Timber.
]]>
</code>
</EXAMPLE>
]]>
</PAYLOAD>
<IMPLEMENTATION_HINTS>
<HINT>Агент должен найти узел `<DECISION id="tech_logging">` в файле `PROJECT_SPECIFICATION.xml` с помощью XPath `//TECHNICAL_DECISIONS/DECISION[@id='tech_logging']`.</HINT>
<HINT>Затем он должен добавить XML-блок из секции `<PAYLOAD>` в качестве нового дочернего элемента к найденному узлу `<DECISION>`.</HINT>
<HINT>Операция `APPEND_CHILD` означает, что содержимое PAYLOAD добавляется в конец списка дочерних элементов целевого узла.</HINT>
</IMPLEMENTATION_HINTS>
</WORK_ORDER>
</TASK>

1532
tasks/completed/01.xml Normal file

File diff suppressed because it is too large Load Diff