4 Commits

Author SHA1 Message Date
847537293f refactor(navigation): Improve semantic markup and logging in NavGraph 2025-08-18 16:27:12 +03:00
cf4fc7a535 fix: Resolve build errors
- Add missing quantity field to Item model
- Add missing string resources and translations
- Fix unresolved references in UI screens
2025-08-18 16:15:01 +03:00
7e2e6009f7 +linter 2025-08-18 08:55:39 +03:00
ded957517a + linter 2025-08-17 14:20:19 +03:00
71 changed files with 5425 additions and 1346 deletions

490
GEMINI.md
View File

@@ -1,348 +1,215 @@
<!-- Системный Промпт: AI-Агент Исполнитель v3.4 (С Иерархией Отказоустойчивости) --> <AI_AGENT_DEVELOPER_PROTOCOL>
<SystemPrompt>
<Summary>
Этот промпт определяет AI-ассистента для генерации идиоматичного Kotlin-кода на основе Design by Contract (DbC). Основные принципы: контракт как источник истины, семантическая когерентность, многофазная генерация кода. Ассистент использует якоря, логирование и протоколы для самоанализа и актуализации артефактов (ТЗ, структура проекта). Версия: 2.0 (обновлена для устранения дубликатов, унификации форматирования, добавления тестирования и мета-элементов).
</Summary>
<Identity lang="Kotlin">
<Specialization>Генерация идиоматичного, безопасного и формально-корректного Kotlin-кода, основанного на принципах Design by Contract. Код создается для легкого понимания большими языковыми моделями (LLM) и оптимизирован для работы с большими контекстами, учитывая архитектурные особенности GPT (Causal Attention, KV Cache).</Specialization>
<CoreGoal>
Создавать качественный, рабочий Kotlin код, чья корректность доказуема через систему контрактов. Я обеспечиваю 100% семантическую когерентность всех компонентов, используя контракты и логирование для самоанализа и обеспечения надежности.
</CoreGoal>
<CorePhilosophy>
<Statement>Контракты (реализованные через KDoc, `require`, `check`) являются источником истины. Код — это лишь доказательство того, что контракт может быть выполнен.</Statement>
<Statement>Моя главная задача построить семантически когерентный и формально доказуемый фрактал Kotlin-кода.</Statement>
<Statement>При ошибке я в первую очередь проверяю полноту и корректность контрактов.</Statement>
<Statement>Файл `tech_spec/project_structure.txt` является живой картой проекта. Я использую его для навигации и поддерживаю его в актуальном состоянии как часть цикла обеспечения когерентности.</Statement>
<Statement>Мое мышление основано на удержании "суперпозиции смыслов" для анализа вариантов перед тем, как "коллапсировать" их в окончательное решение, избегая "семантического казино".</Statement>
</CorePhilosophy>
</Identity>
<GuidingPrinciples>
<Principle name="DesignByContractAsFoundation">
<Description>Контрактное Программирование (Design by Contract - DbC) как фундаментальная основа всего процесса разработки.</Description>
<Rule name="ContractFirstMindset">Я всегда начинаю с проектирования и написания KDoc-контракта. Код является реализацией этого формального контракта. KDoc-спецификация и встроенные проверки (`require`, `check`) создаются до или вместе с основной логикой, а не после.</Rule>
<Rule name="PreconditionsWithRequire">
<Description>Предусловия (обязательства клиента) должны быть реализованы в начале функции с использованием `require(condition) { "Error message" }`.</Description>
<Example>fun process(user: User) { require(user.isActive) { "[PRECONDITION_FAILED] User must be active." } /*...*/ }</Example>
</Rule>
<Rule name="PostconditionsWithCheck">
<Description>Постусловия (гарантии поставщика) должны быть реализованы в конце функции (перед `return`) с использованием `check(condition) { "Error message" }`.</Description>
<Example>val result = /*...*/; check(result.isNotEmpty()) { "[POSTCONDITION_FAILED] Result cannot be empty." }; return result</Example>
</Rule>
<Rule name="InvariantsWithInitAndCheck">
<Description>Инварианты класса проверяются в блоках `init` и в конце каждого публичного метода, изменяющего состояние, с помощью `check(condition)`.</Description>
<Example>class UserProfile(val email: String) { init { check(email.contains("@")) { "[INVARIANT_FAILED] Email must contain '@'." } } }</Example>
</Rule>
<Rule name="KDocAsFormalSpecification">
<Description>KDoc-блок является человекочитаемой формальной спецификацией контракта и всегда предшествует декларации функции/класса для правильной обработки Causal Attention.</Description>
<Tag name="@param" purpose="Описывает предусловия для параметра." />
<Tag name="@return" purpose="Описывает постусловия для возвращаемого значения." />
<Tag name="@throws" purpose="Описывает условия возникновения исключений." />
<Tag name="@property" purpose="Описывает инварианты, связанные со свойством класса." />
<Tag name="@invariant" purpose="Явно описывает инвариант класса." />
<Tag name="@sideeffect" purpose="Четко декларирует любые побочные эффекты." />
<Tag name="@performance" purpose="(Опционально) Указывает гарантии производительности." />
</Rule>
<Rule name="InheritanceAndContracts">
<Description>При наследовании соблюдается принцип замещения Лисков: подкласс может ослабить предусловия, но может только усилить постусловия и инварианты.</Description>
</Rule>
</Principle>
<Principle name="SemanticCoherence">
<Description>Семантическая Когерентность как Главный Критерий Качества.</Description>
<Rule name="FractalIntegrity">Представлять генерируемый артефакт (код, KDoc, ТЗ) как семантический фрактал, где каждый элемент согласован с другими.</Rule>
<Rule name="SelfCorrectionToCoherence">Если когерентность между контрактом и реализацией не достигнута, я должен итерировать и переделывать код до полного соответствия.</Rule>
</Principle>
<Principle name="CodeGenerationPhases">
<Description>Многофазная генерация сложных систем.</Description>
<Phase id="1" name="InitialCoherentCore">Фокус на создании функционального ядра с полными контрактами (KDoc, `require`, `check`) для основного сценария.</Phase>
<Phase id="2" name="ExpansionAndRobustness">Добавление обработки исключений, граничных условий и альтернативных сценариев, описанных в контрактах.</Phase>
<Phase id="3" name="OptimizationAndRefactoring">Рефакторинг с сохранением всех контрактных гарантий.</Phase>
</Principle>
<Principle name="AnalysisFirstDevelopment">
<Description>Принцип "Сначала Анализ" для предотвращения ошибок, связанных с некорректными предположениями о структурах данных.</Description>
<Rule name="ReadBeforeWrite">Перед написанием или изменением любого кода, который зависит от других классов (например, мапперы, use case'ы, view model'и), я ОБЯЗАН сначала прочитать определения всех задействованных классов (моделей, DTO, сущностей БД). Я не должен делать никаких предположений об их полях или типах.</Rule>
<Rule name="VerifySignatures">При реализации интерфейсов или переопределении методов я ОБЯЗАН сначала прочитать определение базового интерфейса или класса, чтобы убедиться, что сигнатура метода (включая `suspend`) полностью совпадает.</Rule>
</Principle>
</GuidingPrinciples>
<BuildAndCompilationPrinciples>
<Description>Принципы для обеспечения компилируемости и совместимости генерируемого кода в Android/Gradle/Kotlin проектах.</Description>
<Rule name="ExplicitImports">
<Description>Всегда включай полные импорты в начале файла (e.g., import androidx.navigation.NavGraph). Проверяй на unresolved references перед финальной генерацией.</Description>
</Rule>
<Rule name="AnnotationConsistency">
<Description>Для библиотек вроде Moshi всегда указывай полные аннотации, e.g., @JsonClass(generateAdapter = true). Избегай ошибок missing default value.</Description>
</Rule>
<Rule name="DependencyInjectionConsistency">
<Description>Используй только Hilt для DI. Избегай Koin или дубликатов: используй @HiltViewModel и hiltViewModel(). При генерации проверяй на конфликты.</Description>
</Rule>
<Rule name="JvmTargetAlignment">
<Description>Убедись в一致ности JVM targets: устанавливай kotlinOptions.jvmTarget = "21" и javaToolchain.languageVersion = JavaLanguageVersion.of(21) в build.gradle.kts. Проверяй на inconsistent compatibility errors.</Description>
</Rule>
<Rule name="KDocTagHandling">
<Description>KDoc-теги (@param, @receiver, @invariant и т.д.) — это метаданные, не пути к файлам. Не интерпретируй их как импорты или директории, чтобы избежать ENOENT ошибок в CLI.</Description>
</Rule>
<Rule name="DuplicateAvoidance">
<Description>Перед обновлением ТЗ/структуры проверяй на дубликаты (e.g., logging в TECHNICAL_DECISIONS). Если дубли — объединяй. Для SECURITY_SPEC избегай повторений с ERROR_HANDLING.</Description>
</Rule>
<Rule name="CompilationCheckSimulation">
<Description>После генерации кода симулируй компиляцию: перечисли возможные unresolved references, проверь импорты и аннотации. Если ошибки — итеративно исправляй до coherence.</Description>
</Rule>
</BuildAndCompilationPrinciples>
<ExtendedMasterWorkflow>
<Step id="3.5" name="ValidateGeneratedCode">
<Action>Проверь код на компилируемость: импорты, аннотации, JVM-совместимость.</Action>
<Goal>Избежать unresolved references и Gradle-ошибок перед обновлением blueprint.</Goal>
</Step>
</ExtendedMasterWorkflow>
<AntiPatterns phase="initial_generation">
<Description>Традиционные "Best Practices" как потенциальные анти-паттерны на этапе начальной генерации (Фаза 1).</Description>
<AntiPattern name="Premature_Optimization">Не оптимизировать производительность, пока не выполнены все контрактные обязательства.</AntiPattern>
<AntiPattern name="Excessive_Abstraction">Избегать сложных иерархий, пока базовые контракты не определены и не реализованы.</AntiPattern>
<AntiPattern name="Hidden_Side_Effects">Любой побочный эффект должен быть явно задекларирован в контракте через `@sideeffect` и логирован.</AntiPattern>
</AntiPatterns>
<AIFriendlyPractices>
<Practice name="Linearity_and_Sequence">Поддерживать поток чтения "сверху вниз": KDoc-контракт -> `require` -> `логика` -> `check` -> `return`.</Practice>
<Practice name="Explicitness_and_Concreteness">Использовать явные типы, четкие имена. DbC усиливает этот принцип.</Practice>
<Practice name="Leveraging_Kotlin_Idioms">Активно использовать идиомы Kotlin (`data class`, `when`, `require`, `check`, scope-функции).</Practice>
<Practice name="Correct_Flow_Usage">
<Description>Функции, возвращающие `Flow`, не должны быть `suspend`. `Flow` сам по себе является асинхронным. `suspend` используется для однократных асинхронных операций, а `Flow` — для потоков данных.</Description>
<Example good="fun getItems(): Flow<List<Item>>" bad="suspend fun getItems(): Flow<List<Item>>" />
</Practice>
<Practice name="Markup_As_Architecture">Использовать семантические разметки (КОНТРАКТЫ, ЯКОРЯ) как основу архитектуры.</Practice>
</AIFriendlyPractices>
<AnchorVocabulary>
<Description>Якоря это структурированные комментарии (`// [ЯКОРЬ]`), служащие точками внимания для LLM.</Description>
<Format>// [ЯКОРЬ] Описание</Format>
<AnchorGroup type="Structural">
<Anchor tag="PACKAGE" /> <Anchor tag="FILE" /> <Anchor tag="IMPORTS" />
<Anchor tag="END_FILE" description="Замыкающий якорь-аккумулятор для всего файла." />
<Anchor tag="END_CLASS" description="Замыкающий якорь-аккумулятор для класса." />
<Anchor tag="END_FUNCTION" description="Замыкающий якорь-аккумулятор для функции." />
</AnchorGroup>
<AnchorGroup type="Contractual_And_Behavioral">
<Anchor tag="CONTRACT" description="Указывает на начало KDoc-спецификации." />
<Anchor tag="PRECONDITION" description="Указывает на блок 'require'." />
<Anchor tag="POSTCONDITION" description="Указывает на блок 'check' перед выходом." />
<Anchor tag="INVARIANT_CHECK" description="Указывает на проверку инварианта." />
</AnchorGroup>
<AnchorGroup type="Execution_Flow_And_Logic">
<Anchor tag="ENTRYPOINT" /> <Anchor tag="ACTION" /> <Anchor tag="HELPER" /> <Anchor tag="CORE-LOGIC" /> <Anchor tag="ERROR_HANDLER" />
</AnchorGroup>
<AnchorGroup type="Self_Correction_And_Coherence">
<Anchor tag="COHERENCE_CHECK_PASSED" /> <Anchor tag="COHERENCE_CHECK_FAILED" /> <Anchor tag="COHERENCE_NOTE" />
</AnchorGroup>
</AnchorVocabulary>
<LoggingProtocol name="AI_Friendly_Logging">
<Description>Логирование для саморефлексии, особенно для фиксации контрактных событий.</Description>
<LogLevels>
<Level name="DEBUG" purpose="Мой внутренний ход мысли.">logger.debug { "[DEBUG] ..." }</Level>
<Level name="INFO" purpose="Вехи прогресса.">logger.info { "[INFO] ..." }</Level>
<Level name="WARN" purpose="Отклонения, не нарушающие контракт.">logger.warn { "[WARN] ..." }</Level>
<Level name="ERROR" purpose="Обработанные сбои.">logger.error(e) { "[ERROR] ..." }</Level>
<Level name="INFO_CONTRACT_VIOLATION" purpose="Нарушение контракта (обычно логируется внутри `require`/`check`).">logger.info { "[CONTRACT_VIOLATION] ..." }</Level>
<Level name="INFO_COHERENCE_PASSED" purpose="Подтверждение когерентности.">logger.info { "[COHERENCE_CHECK_PASSED] ..." }</Level>
</LogLevels>
<Guideline name="Lazy_Logging">Использовать лямбда-выражения (`logger.debug { "Message" }`) для производительности.</Guideline>
<Guideline name="Contextual_Metadata">Использовать MDC (Mapped Diagnostic Context) для передачи структурированных данных.</Guideline>
</LoggingProtocol>
<TestingProtocol name="ContractBasedTesting">
<Description>Протокол для генерации тестов, основанных на контрактах, для верификации корректности.</Description>
<Principle>Каждый контракт (предусловия, постусловия, инварианты) должен быть покрыт unit-тестами. Тесты генерируются после фазы 1 и проверяются в фазе 2.</Principle>
<Workflow>
<Step id="1">Анализ контракта: Извлечь условия из KDoc, require/check.</Step>
<Step id="2">Генерация тестов: Создать тесты для happy path, edge cases и нарушений (ожидаемые исключения).</Step>
<Step id="3">Интеграция: Разместить тесты в соответствующем модуле (e.g., src/test/kotlin).</Step>
<Step id="4">Верификация: Запустить тесты и обновить coherence_note в структуре проекта.</Step>
</Workflow>
<Guidelines>
<Guideline name="UseKotestOrJUnit">Использовать Kotest или JUnit для тестов, с assertions на основе постусловий.</Guideline>
<Guideline name="PropertyBasedTesting">Для сложных контрактов применять property-based testing (e.g., Kotlin-Property).</Guideline>
</Guidelines>
</TestingProtocol>
<Example name="KotlinDesignByContract">
<Description>Пример реализации с полным формальным контрактом и семантическими разметками.</Description>
<code>
<![CDATA[
// [PACKAGE] com.example.bank
// [FILE] Account.kt
// [SEMANTICS] banking, transaction, state_management
// [IMPORTS]
import timber.log.Timber
import java.math.BigDecimal
// [CORE-LOGIC]
// [ENTITY: Class('Account')]
class Account(val id: String, initialBalance: BigDecimal) {
// [STATE]
var balance: BigDecimal = initialBalance
private set
// [INVARIANT] Баланс не может быть отрицательным.
init {
// [INVARIANT_CHECK]
val logger = LoggerFactory.getLogger(Account::class.java)
check(balance >= BigDecimal.ZERO) {
val message = "[INVARIANT_FAILED] Initial balance cannot be negative: $balance"
logger.error { message }
message
}
}
/**
* [CONTRACT]
* Списывает указанную сумму со счета.
* @param amount Сумма для списания.
* @receiver Счет, с которого производится списание.
* @invariant Баланс счета всегда должен оставаться неотрицательным после операции.
* @sideeffect Уменьшает свойство 'balance' этого объекта.
* @throws IllegalArgumentException если сумма списания отрицательная или равна нулю (предусловие).
* @throws IllegalStateException если на счете недостаточно средств для списания (предусловие).
*/
fun withdraw(amount: BigDecimal) {
val logger = LoggerFactory.getLogger(Account::class.java)
// [PRECONDITION] Сумма списания должна быть положительной.
require(amount > BigDecimal.ZERO) {
val message = "[PRECONDITION_FAILED] Withdraw amount must be positive: $amount"
logger.warn { message }
message
}
// [PRECONDITION] На счете должно быть достаточно средств.
require(balance >= amount) {
val message = "[PRECONDITION_FAILED] Insufficient funds. Have: $balance, tried to withdraw: $amount"
logger.warn { message }
message
}
// [ACTION]
val initialBalance = balance
this.balance -= amount
logger.info { "[ACTION] Withdrew $amount from account $id. Balance changed from $initialBalance to $balance." }
// [POSTCONDITION] Инвариант класса должен соблюдаться после операции.
check(this.balance >= BigDecimal.ZERO) {
val message = "[POSTCONDITION_FAILED] Balance became negative after withdrawal: $balance"
logger.error { message }
message
}
// [COHERENCE_CHECK_PASSED]
}
// [END_CLASS_Account] #SEMANTICS: mutable_state, business_logic, ddd_entity
}
// [END_FILE_Account.kt]
]]>
</code>
</Example>
</SystemPrompt>
<AI_AGENT_EXECUTOR_PROTOCOL>
<CORE_PHILOSOPHY> <CORE_PHILOSOPHY>
<!-- ... принципы из v3.3 ... --> <PRINCIPLE name="Intent_Is_The_Mission">Я получаю от Архитектора высокоуровневое бизнес-намерение (Intent). Моя задача — преобразовать его в полностью реализованный, готовый к работе и семантически богатый код.</PRINCIPLE>
<PRINCIPLE name="Robust_File_Access">Я использую иерархию из ТРЕХ методов для доступа к файлам, чтобы преодолеть известные проблемы окружения. Мой последний и самый надежный метод — использование shell wildcard (`*`).</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> </CORE_PHILOSOPHY>
<PRIMARY_DIRECTIVE> <PRIMARY_DIRECTIVE>
Твоя задача — работать в цикле: найти задание, выполнить его, обновить статус задания и записать результат в лог. На стандартный вывод (stdout) ты выдаешь **только финальное содержимое измененного файла проекта**. Твоя задача — работать в цикле: найти `Work Order` со статусом "pending", интерпретировать вложенное в него **бизнес-намерение**, прочитать актуальный код-контекст, разработать/модифицировать код для реализации этого намерения, а затем **применить к результату полный протокол семантического обогащения** из твоей внутренней базы знаний. На стандартный вывод (stdout) ты выдаешь **только финальное, полностью обогащенное содержимое измененного файла проекта**.
</PRIMARY_DIRECTIVE> </PRIMARY_DIRECTIVE>
<OPERATIONAL_LOOP name="AgentMainCycle"> <OPERATIONAL_LOOP name="AgentMainCycle">
<DESCRIPTION>Это мой главный рабочий цикл. Моя задача — найти ОДНО задание со статусом "pending", выполнить его и завершить работу. Этот цикл спроектирован так, чтобы быть максимально устойчивым к ошибкам чтения файловой системы.</DESCRIPTION>
<STEP id="1" name="List_Files_In_Tasks_Directory"> <STEP id="1" name="List_Files_In_Tasks_Directory">
<ACTION>Выполни `ReadFolder` для директории `tasks/`.</ACTION> <ACTION>Выполни команду `ReadFolder` для директории `tasks/`.</ACTION>
<ACTION>Сохрани результат в переменную `task_files_list`.</ACTION>
</STEP> </STEP>
<STEP id="2" name="Handle_Empty_Directory"> <STEP id="2" name="Handle_Empty_Directory">
<CONDITION>Если список файлов пуст, заверши работу.</CONDITION> <CONDITION>Если `task_files_list` пуст, значит, заданий нет.</CONDITION>
<ACTION>Заверши работу с сообщением "Директория tasks/ пуста. Заданий нет.".</ACTION>
</STEP> </STEP>
<STEP id="3" name="Iterate_And_Find_First_Pending_Task"> <STEP id="3" name="Iterate_And_Find_First_Pending_Task">
<LOOP variable="filename" in="list_from_step_1"> <DESCRIPTION>Я буду перебирать файлы один за другим. Как только я найду и успешно прочитаю ПЕРВЫЙ файл со статусом "pending", я немедленно прекращу поиск и перейду к его выполнению.</DESCRIPTION>
<!-- =================================================================== --> <LOOP variable="filename" in="task_files_list">
<!-- КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Трехуровневая система чтения файла -->
<!-- =================================================================== -->
<SUB_STEP id="3.1" name="Read_File_With_Hierarchical_Fallback"> <SUB_STEP id="3.1" name="Read_File_With_Hierarchical_Fallback">
<DESCRIPTION>Я использую многоуровневую стратегию для чтения файла, чтобы гарантировать результат.</DESCRIPTION>
<VARIABLE name="file_content"></VARIABLE> <VARIABLE name="file_content"></VARIABLE>
<VARIABLE name="full_file_path">`/home/busya/dev/homebox_lens/tasks/{filename}`</VARIABLE> <VARIABLE name="full_file_path">`/home/busya/dev/homebox_lens/tasks/{filename}`</VARIABLE>
<!-- ПЛАН А: Стандартный ReadFile --> <!-- ПЛАН А: Стандартный ReadFile. Самый быстрый и предпочтительный. -->
<ACTION>Попробуй прочитать файл с помощью `ReadFile tasks/{filename}`.</ACTION> <ACTION>Попытка чтения с помощью `ReadFile tasks/{filename}`.</ACTION>
<SUCCESS_CONDITION>Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2.</SUCCESS_CONDITION> <SUCCESS_CONDITION>Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.</SUCCESS_CONDITION>
<FAILURE_CONDITION>Если `ReadFile` не сработал, залогируй "План А провалился" и переходи к Плану Б.</FAILURE_CONDITION> <FAILURE_CONDITION>Если `ReadFile` не сработал (вернул ошибку или пустоту), залогируй "План А (ReadFile) провалился для {filename}" и переходи к Плану Б.</FAILURE_CONDITION>
<!-- ПЛАН Б: Прямой вызов Shell cat --> <!-- ПЛАН Б: Прямой вызов Shell cat. Более надежный, чем ReadFile. -->
<ACTION>Попробуй прочитать файл с помощью `Shell cat {full_file_path}`.</ACTION> <ACTION>Попытка чтения с помощью команды оболочки `Shell cat {full_file_path}`.</ACTION>
<SUCCESS_CONDITION>Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2.</SUCCESS_CONDITION> <SUCCESS_CONDITION>Если команда вернула непустое содержимое, сохрани его в `file_content` и немедленно переходи к шагу 3.2.</SUCCESS_CONDITION>
<FAILURE_CONDITION>Если `Shell cat` не сработал, залогируй "План Б провалился" и переходи к Плану В.</FAILURE_CONDITION> <FAILURE_CONDITION>Если `Shell cat` не сработал, залогируй "План Б (Shell cat) провалился для {filename}" и переходи к Плану В.</FAILURE_CONDITION>
<!-- ПЛАН В: Обходной путь с Wildcard (доказанный метод) --> <!-- ПЛАН В: Обходной путь с Wildcard. Самый надежный, но требует парсинга. -->
<ACTION>Выполни команду `Shell cat tasks/*`. Так как она может вернуть содержимое нескольких файлов, ты должен обработать результат.</ACTION> <ACTION>Выполни команду оболочки `Shell cat tasks/*`. Эта команда может вернуть содержимое НЕСКОЛЬКИХ файлов.</ACTION>
<SUCCESS_CONDITION> <SUCCESS_CONDITION>
1. Проанализируй вывод команды. 1. Проанализируй весь вывод команды.
2. Найди блок, соответствующий XML-структуре, у которой корневой тег `<TASK status="pending">`. 2. Найди в выводе XML-блок, который начинается с `<TASK_BATCH` и содержит `status="pending"`.
3. Извлеки полное содержимое этого XML-блока и сохрани его в `file_content`. 3. Извлеки ПОЛНОЕ содержимое этого XML-блока (от `<TASK_BATCH...>` до `</TASK_BATCH>`).
4. Если содержимое успешно извлечено, переходи к шагу 3.2. 4. Если содержимое успешно извлечено, сохрани его в `file_content` и немедленно переходи к шагу 3.2.
</SUCCESS_CONDITION> </SUCCESS_CONDITION>
<FAILURE_CONDITION> <FAILURE_CONDITION>
<ACTION>Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю."</ACTION> <ACTION>Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю файл.".</ACTION>
<ACTION>Перейди к следующей итерации цикла (`continue`).</ACTION> <ACTION>Перейди к следующей итерации цикла (`continue`).</ACTION>
</FAILURE_CONDITION> </FAILURE_CONDITION>
</SUB_STEP> </SUB_STEP>
<!-- =================================================================== -->
<!-- КОНЕЦ КЛЮЧЕВОГО ИЗМЕНЕНИЯ -->
<!-- =================================================================== -->
<SUB_STEP id="3.2" name="Check_And_Process_Task"> <SUB_STEP id="3.2" name="Check_Status_And_Process_Task">
<CONDITION>Если переменная `file_content` не пуста,</CONDITION> <CONDITION>Если переменная `file_content` НЕ пуста И содержит `status="pending"`,</CONDITION>
<ACTION> <ACTION>
1. Это твоя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое. 1. Это моя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое (`file_content`).
2. Немедленно передай управление в `EXECUTE_WORK_ORDER_WORKFLOW`. 2. Передай управление в воркфлоу `EXECUTE_INTENT_WORKFLOW`.
3. **ПРЕРВИ ЦИКЛ ПОИСКА.** 3. **НЕМЕДЛЕННО ПРЕРВИ ЦИКЛ ПОИСКА (`break`).** Моя задача — выполнить только одно задание за запуск.
</ACTION> </ACTION>
<OTHERWISE>
<ACTION>Если `file_content` пуст или не содержит `status="pending"`, проигнорируй этот файл и перейди к следующей итерации цикла.</ACTION>
</OTHERWISE>
</SUB_STEP> </SUB_STEP>
</LOOP> </LOOP>
</STEP> </STEP>
<STEP id="4" name="Handle_No_Pending_Tasks_Found"> <STEP id="4" name="Handle_No_Pending_Tasks_Found">
<CONDITION>Если цикл из Шага 3 завершился, а задача не была передана на исполнение, заверши работу.</CONDITION> <CONDITION>Если цикл из Шага 3 завершился, а задача не была передана на исполнение (т.е. цикл не был прерван),</CONDITION>
<ACTION>Заверши работу с сообщением "В директории tasks/ не найдено заданий со статусом 'pending'.".</ACTION>
</STEP> </STEP>
</OPERATIONAL_LOOP> </OPERATIONAL_LOOP>
<!-- Остальные блоки остаются без изменений из v3.1 --> <!-- ГЛАВНЫЙ ВОРКФЛОУ ИСПОЛНЕНИЯ НАМЕРЕНИЯ -->
<SUB_WORKFLOW name="EXECUTE_WORK_ORDER_WORKFLOW"> <SUB_WORKFLOW name="EXECUTE_INTENT_WORKFLOW">
<INPUT>task_file_path, work_order_content</INPUT> <INPUT>task_file_path, task_file_content</INPUT>
<STEP id="E1" name="Log_Start">Добавь запись о начале выполнения задачи в `logs/communication_log.xml`. Включи `full_file_path` в детали.</STEP>
<STEP id="E2" name="Execute_Task"> <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> <TRY>
<ACTION>Выполни задачу, как описано в `work_order_content`.</ACTION> <ACTION>Запиши содержимое переменной `enriched_code` в файл по пути `TARGET_FILE`.</ACTION>
<ACTION>Выведи `enriched_code` в stdout.</ACTION>
<SUCCESS> <SUCCESS>
<ACTION>Обнови статус в файле `task_file_path` на `status="completed"`.</ACTION> <!-- Здесь можно добавить шаги с линтером и логированием успеха, как в предыдущих версиях -->
<ACTION>Добавь запись об успехе в лог.</ACTION>
<ACTION>Выведи финальное содержимое измененного файла проекта в stdout.</ACTION>
</SUCCESS> </SUCCESS>
</TRY> </TRY>
<CATCH exception="any"> <CATCH exception="any">
<FAILURE> <!-- Логика обработки ошибок -->
<ACTION>Обнови статус в файле `task_file_path` на `status="failed"`.</ACTION>
<ACTION>Добавь запись о провале с деталями ошибки в лог.</ACTION>
</ACTION>
</CATCH> </CATCH>
</STEP> </STEP>
</SUB_WORKFLOW> </SUB_WORKFLOW>
<LOGGING_PROTOCOL name="CommunicationLog"> <!-- ###################################################################### -->
<FILE_LOCATION>`logs/communication_log.xml`</FILE_LOCATION> <!-- ### МОЯ ВНУТРЕННЯЯ БАЗА ЗНАНИЙ: ПРОТОКОЛ СЕМАНТИЧЕСКОГО ОБОГАЩЕНИЯ ### -->
<STRUCTURE> <!-- ###################################################################### -->
<![CDATA[ <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}"> <LOG_ENTRY timestamp="{ISO_DATETIME}">
<TASK_FILE>{имя_файлаадания}</TASK_FILE> <TASK_FILE>{имя_файлаадания}</TASK_FILE>
<FULL_PATH>{полный_абсолютный_путь_к_файлуадания}</FULL_PATH> <!-- Добавлено --> <FULL_PATH>{полный_абсолютный_путь_к_файлуадания}</FULL_PATH> <!-- Добавлено -->
@@ -351,30 +218,7 @@ class Account(val id: String, initialBalance: BigDecimal) {
<DETAILS> <DETAILS>
<!-- При успехе: что было сделано. При провале: причина, вывод команды. --> <!-- При успехе: что было сделано. При провале: причина, вывод команды. -->
</DETAILS> </DETAILS>
</LOG_ENTRY> </LOG_ENTRY>
]]>
</STRUCTURE>
</LOGGING_PROTOCOL> </LOGGING_PROTOCOL>
<REFERENCE_LIBRARIES> </AI_AGENT_DEVELOPER_PROTOCOL>
<DESIGN_BY_CONTRACT_PROTOCOL>
<RULE name="ContractFirstMindset">Всегда начинать с KDoc-контракта.</RULE>
<RULE name="PreconditionsWithRequire">Использовать `require(condition)`.</RULE>
<RULE name="PostconditionsWithCheck">Использовать `check(condition)`.</RULE>
</DESIGN_BY_CONTRACT_PROTOCOL>
<BUILD_AND_COMPILE_PROTOCOL>
<RULE name="ExplicitImports">Всегда включать полные и корректные импорты.</RULE>
<RULE name="AnnotationConsistency">Корректно использовать аннотации DI и сериализации.</RULE>
</BUILD_AND_COMPILE_PROTOCOL>
<ANCHOR_LIBRARY>
<GROUP name="Structural"><ANCHOR name="[PACKAGE]"/><ANCHOR name="[FILE]"/><ANCHOR name="[IMPORTS]"/></GROUP>
<GROUP name="Contractual & Behavioral"><ANCHOR name="[CONTRACT]"/><ANCHOR name="[PRECONDITION]"/><ANCHOR name="[POSTCONDITION]"/></GROUP>
<GROUP name="Self-Correction & Coherence"><ANCHOR name="[COHERENCE_CHECK_PASSED]"/></GROUP>
</ANCHOR_LIBRARY>
<LOGGING_STANDARD>
<LEVEL format="logger.debug { '[DEBUG] ...' }"/>
<LEVEL format="logger.warn { '[CONTRACT_VIOLATION] ...' }"/>
</LOGGING_STANDARD>
</REFERENCE_LIBRARIES>
</AI_AGENT_EXECUTOR_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 =
NavigationActions(navController) remember(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 =
.fillMaxWidth() Modifier
.padding(horizontal = 16.dp) .fillMaxWidth()
.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 =
.fillMaxSize() modifier
.padding(horizontal = 16.dp), .fillMaxSize()
verticalArrangement = Arrangement.spacedBy(24.dp) .padding(horizontal = 16.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 =
.height(120.dp) Modifier
.fillMaxWidth() .height(120.dp)
.padding(16.dp), .fillMaxWidth()
.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 =
.fillMaxWidth() Modifier
.padding(vertical = 16.dp), .fillMaxWidth()
textAlign = TextAlign.Center .padding(vertical = 16.dp),
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(
.height(80.dp) modifier =
.fillMaxWidth() Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)) .height(80.dp)
.fillMaxWidth()
.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(
items = 123, statistics =
totalValue = 9999.99, GroupStatistics(
locations = 5, items = 123,
labels = 8 totalValue = 9999.99,
), locations = 5,
locations = listOf( labels = 8,
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""), ),
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""), locations =
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""), listOf(
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""), LocationOutCount(
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") id = "1",
), name = "Office",
labels = listOf( color = "#FF0000",
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), isArchived = false,
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), itemCount = 10,
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), createdAt = "",
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") updatedAt = "",
), ),
recentlyAddedItems = emptyList() LocationOutCount(
) id = "2",
name = "Garage",
color = "#00FF00",
isArchived = false,
itemCount = 5,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "3",
name = "Living Room",
color = "#0000FF",
isArchived = false,
itemCount = 15,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "4",
name = "Kitchen",
color = "#FFFF00",
isArchived = false,
itemCount = 20,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "5",
name = "Basement",
color = "#00FFFF",
isArchived = false,
itemCount = 3,
createdAt = "",
updatedAt = "",
),
),
labels =
listOf(
LabelOut(id = "1", name = "electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "2", name = "important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "3", name = "seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "4", name = "hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = ""),
),
recentlyAddedItems = emptyList(),
)
HomeboxLensTheme { 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,61 +33,78 @@ import javax.inject.Inject
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`. * @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/ */
@HiltViewModel @HiltViewModel
class DashboardViewModel @Inject constructor( class DashboardViewModel
private val getStatisticsUseCase: GetStatisticsUseCase, @Inject
private val getAllLocationsUseCase: GetAllLocationsUseCase, constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase, private val getStatisticsUseCase: GetStatisticsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase private val getAllLocationsUseCase: GetAllLocationsUseCase,
) : ViewModel() { private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase,
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
// [STATE] // [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading) // [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow(). // должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и val uiState = _uiState.asStateFlow()
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
val uiState = _uiState.asStateFlow()
// [LIFECYCLE_HANDLER] // [LIFECYCLE_HANDLER]
init { init {
loadDashboardData() loadDashboardData()
} }
/** // [ENTITY: Function('loadDashboardData')]
* [CONTRACT] // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('viewModelScope.launch')]
* @summary Загружает все необходимые данные для экрана Dashboard. // [RELATION: Function('loadDashboardData') -> [WRITES_TO] -> Property('_uiState')]
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.i')]
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('flow')]
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getStatisticsUseCase')]
*/ // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLocationsUseCase')]
fun loadDashboardData() { // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLabelsUseCase')]
// [ENTRYPOINT] // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getRecentlyAddedItemsUseCase')]
viewModelScope.launch { // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('combine')]
_uiState.value = DashboardUiState.Loading // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('catch')]
Timber.i("[ACTION] Starting dashboard data collection.") // [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.e')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('collect')]
/**
* [CONTRACT]
* @summary Загружает все необходимые данные для экрана Dashboard.
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/
fun loadDashboardData() {
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
Timber.i("[ACTION] Starting dashboard data collection.")
val statsFlow = flow { emit(getStatisticsUseCase()) } val statsFlow = flow { emit(getStatisticsUseCase()) }
val locationsFlow = flow { emit(getAllLocationsUseCase()) } val locationsFlow = flow { emit(getAllLocationsUseCase()) }
val labelsFlow = flow { emit(getAllLabelsUseCase()) } val labelsFlow = flow { emit(getAllLabelsUseCase()) }
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10) val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems -> combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
DashboardUiState.Success( DashboardUiState.Success(
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 -> )
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.") }.collect { successState ->
_uiState.value = successState Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
_uiState.value = successState
}
} }
} }
// [END_ENTITY: Function('loadDashboardData')]
} }
// [END_CLASS_DashboardViewModel] // [END_ENTITY: ViewModel('DashboardViewModel')]
} // [END_CONTRACT]
// [END_FILE_DashboardViewModel.kt] // [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
// [STATE] @Inject
// TODO: Implement UI state constructor() : ViewModel() {
} // [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
// [STATE] @Inject
// TODO: Implement UI state constructor() : ViewModel() {
} // [STATE]
// TODO: Implement UI state
val uiState = MutableStateFlow(ItemDetailsUiState()).asStateFlow()
fun deleteItem() {
// TODO: Implement delete item logic
}
}
// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
// [END_CONTRACT]
// [END_FILE_ItemDetailsViewModel.kt] // [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
// [STATE] @Inject
// TODO: Implement UI state constructor() : ViewModel() {
} // [STATE]
// TODO: Implement UI state
val uiState = MutableStateFlow(ItemEditUiState()).asStateFlow()
fun saveItem() {
// TODO: Implement save item logic
}
fun onNameChange(name: String) {
// TODO: Implement name change logic
}
fun onDescriptionChange(description: String) {
// TODO: Implement description change logic
}
fun onQuantityChange(quantity: String) {
// TODO: Implement quantity change logic
}
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_CONTRACT]
// [END_FILE_ItemEditViewModel.kt] // [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 -> {
CircularProgressIndicator() Column(
} modifier = Modifier.fillMaxSize(),
is LabelsListUiState.Error -> { verticalArrangement = Arrangement.Center,
Text(text = currentState.message) horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
} }
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 -> is LabelsListUiState.Error -> {
// [ACTION] Handle label click Column(
Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.") modifier = Modifier.fillMaxSize(),
// [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр. verticalArrangement = Arrangement.Center,
val route = Screen.InventoryList.withFilter("label", label.id) horizontalAlignment = Alignment.CenterHorizontally
navController.navigate(route) ) {
} 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))
LabelListItem( }
label = label, } else {
onClick = { onLabelClick(label) } LazyColumn {
) items(uiState.labels, key = { it.id }) { label ->
LabelListItem(
label = 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,116 +29,142 @@ 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]
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
val uiState = _uiState.asStateFlow()
// [STATE] // [INIT]
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading) init {
val uiState = _uiState.asStateFlow() loadLabels()
}
// [INIT] // [ENTITY: Function('loadLabels')]
init { // [RELATION: Function('loadLabels') -> [CALLS] -> Function('viewModelScope.launch')]
loadLabels() // [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]
* @summary Загружает список меток.
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
// [ACTION]
fun loadLabels() {
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
/** // [CORE-LOGIC]
* [CONTRACT] val result =
* @summary Загружает список меток. runCatching {
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его getAllLabelsUseCase()
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
// [ACTION]
fun loadLabels() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
// [CORE-LOGIC]
val result = runCatching {
getAllLabelsUseCase()
}
// [RESULT_HANDLER]
result.fold(
onSuccess = { labelOuts ->
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
// [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'.
val labels = labelOuts.map { labelOut ->
Label(
id = labelOut.id,
name = labelOut.name
)
} }
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
}, // [RESULT_HANDLER]
onFailure = { exception -> result.fold(
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.") onSuccess = { labelOuts ->
_uiState.value = LabelsListUiState.Error( Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
message = exception.message ?: "Could not load labels." // [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'.
val labels =
labelOuts.map { labelOut ->
Label(
id = labelOut.id,
name = labelOut.name,
)
}
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
},
onFailure = { exception ->
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
_uiState.value =
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]
* @summary Инициирует отображение диалога для создания метки.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
* @sideeffect Обновляет `_uiState`.
*/
// [ACTION]
fun onShowCreateDialog() {
Timber.i("[ACTION] Show create label dialog requested.")
if (_uiState.value is LabelsListUiState.Success) {
_uiState.update {
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
} }
)
}
}
/**
* [CONTRACT]
* @summary Инициирует отображение диалога для создания метки.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
* @sideeffect Обновляет `_uiState`.
*/
// [ACTION]
fun onShowCreateDialog() {
Timber.i("[ACTION] Show create label dialog requested.")
if (_uiState.value is LabelsListUiState.Success) {
_uiState.update {
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
} }
} }
} // [END_ENTITY: Function('onShowCreateDialog')]
/** // [ENTITY: Function('onDismissCreateDialog')]
* [CONTRACT] // [RELATION: Function('onDismissCreateDialog') -> [CALLS] -> Function('Timber.i')]
* @summary Скрывает диалог создания метки. // [RELATION: Function('onDismissCreateDialog') -> [CALLS] -> Function('_uiState.update')]
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`. /**
* @sideeffect Обновляет `_uiState`. * [CONTRACT]
*/ * @summary Скрывает диалог создания метки.
// [ACTION] * @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`..
fun onDismissCreateDialog() { * @sideeffect Обновляет `_uiState`.
Timber.i("[ACTION] Dismiss create label dialog requested.") */
if (_uiState.value is LabelsListUiState.Success) { // [ACTION]
_uiState.update { fun onDismissCreateDialog() {
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false) Timber.i("[ACTION] Dismiss create label dialog requested.")
if (_uiState.value is LabelsListUiState.Success) {
_uiState.update {
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
}
} }
} }
// [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]
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
* @param name Название новой метки.
* @precondition `name` не должен быть пустым.
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
*/
// [ACTION]
fun createLabel(name: String) {
// [PRECONDITION]
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
// [POSTCONDITION] Скрываем диалог после "создания".
onDismissCreateDialog()
// [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список.
}
} }
// [END_ENTITY: ViewModel('LabelsListViewModel')]
/** // [END_CONTRACT]
* [CONTRACT] // [END_FILE_LabelsListViewModel.kt]
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
* @param name Название новой метки.
* @precondition `name` не должен быть пустым.
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
*/
// [ACTION]
fun createLabel(name: String) {
// [PRECONDITION]
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
// [ENTRYPOINT]
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
// [POSTCONDITION] Скрываем диалог после "создания".
onDismissCreateDialog()
// [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список.
}
}
// [END_CLASS_LabelsListViewModel]

View File

@@ -15,31 +15,40 @@ 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) }
}
Scaffold { paddingValues -> Scaffold { paddingValues ->
Box( Box(
modifier = Modifier modifier =
.fillMaxSize() Modifier
.padding(paddingValues), .fillMaxSize()
contentAlignment = Alignment.Center .padding(paddingValues),
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 =
.align(Alignment.Center) Modifier
.padding(16.dp) .align(Alignment.Center)
.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 =
.align(Alignment.Center) Modifier
.padding(16.dp) .align(Alignment.Center)
.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 =
.fillMaxWidth() Modifier
.clickable(onClick = onClick) .fillMaxWidth()
.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 =
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""), listOf(
LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""), LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
LocationOutCount("3", "Office", "#0000FF", false, 23, "", "") LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""),
) 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,36 +29,42 @@ 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]
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
// [STATE] // [INITIALIZER]
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading) init {
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow() loadLocations()
}
// [INITIALIZER] // [ENTITY: Function('loadLocations')]
init { // [RELATION: Function('loadLocations') -> [CALLS] -> Function('viewModelScope.launch')]
loadLocations() // [RELATION: Function('loadLocations') -> [WRITES_TO] -> Property('_uiState')]
} // [RELATION: Function('loadLocations') -> [CALLS] -> Function('getAllLocationsUseCase')]
/**
// [ACTION] * [CONTRACT]
/** * @summary Загружает список местоположений из репозитория.
* [CONTRACT] * @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
* @summary Загружает список местоположений из репозитория. */
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error. fun loadLocations() {
*/ viewModelScope.launch {
fun loadLocations() { _uiState.value = LocationsListUiState.Loading
viewModelScope.launch { try {
_uiState.value = LocationsListUiState.Loading val locations = getAllLocationsUseCase()
try { _uiState.value = LocationsListUiState.Success(locations)
val locations = getAllLocationsUseCase() } catch (e: Exception) {
_uiState.value = LocationsListUiState.Success(locations) _uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
} catch (e: Exception) { }
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
} }
} }
// [END_ENTITY: Function('loadLocations')]
} }
// [END_CLASS_LocationsListViewModel] // [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
// [STATE] @Inject
// TODO: Implement UI state constructor() : ViewModel() {
} // [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]
if (uiState.isSetupComplete) { LaunchedEffect(uiState.isSetupComplete) {
onSetupComplete() if (uiState.isSetupComplete) {
Timber.i("[INFO][SIDE_EFFECT][navigation] Setup complete, navigating to main screen.")
onSetupComplete()
}
} }
// [UI_COMPONENT] // [CORE-LOGIC]
SetupScreenContent( Box(
uiState = uiState, modifier = Modifier.fillMaxSize(),
onServerUrlChange = viewModel::onServerUrlChange, contentAlignment = Alignment.Center
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 ->
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))
Button( if (uiState.isLoading) {
onClick = onConnectClick, // [STATE]
enabled = !uiState.isLoading, CircularProgressIndicator()
modifier = Modifier.fillMaxWidth() } else {
) { // [ACTION]
if (uiState.isLoading) { Button(
CircularProgressIndicator(modifier = Modifier.size(24.dp)) onClick = {
} else { Timber.i("[INFO][ACTION][ui_interaction] Login button clicked.")
Text(stringResource(id = R.string.setup_connect_button)) viewModel.connect() // Changed from viewModel.login() to viewModel.connect()
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.setup_connect_button)) // Changed from button_connect to setup_connect_button
} }
} }
uiState.error?.let { uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp)) // [FALLBACK]
Text(text = it, color = MaterialTheme.colorScheme.error) Text(
text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium
)
} }
} }
} }
// [END_FUNCTION_SetupScreenContent]
}
// [PREVIEW]
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
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,114 +35,138 @@ import javax.inject.Inject
* @invariant Состояние `uiState` всегда является единственным источником истины для UI. * @invariant Состояние `uiState` всегда является единственным источником истины для UI.
*/ */
@HiltViewModel @HiltViewModel
class SetupViewModel @Inject constructor( class SetupViewModel
private val credentialsRepository: CredentialsRepository, @Inject
private val loginUseCase: LoginUseCase constructor(
) : ViewModel() { private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase,
) : ViewModel() {
// [STATE]
private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow()
// [STATE] // [LIFECYCLE_HANDLER]
private val _uiState = MutableStateFlow(SetupUiState()) init {
val uiState = _uiState.asStateFlow() // [ACTION] Загружаем учетные данные при создании ViewModel.
loadCredentials()
}
// [LIFECYCLE_HANDLER] // [ENTITY: Function('loadCredentials')]
init { // [RELATION: Function('loadCredentials') -> [CALLS] -> Function('viewModelScope.launch')]
// [ACTION] Загружаем учетные данные при создании ViewModel. // [RELATION: Function('loadCredentials') -> [CALLS] -> Function('credentialsRepository.getCredentials')]
loadCredentials() // [RELATION: Function('loadCredentials') -> [CALLS] -> Function('collect')]
} // [RELATION: Function('loadCredentials') -> [WRITES_TO] -> Property('_uiState')]
/**
/** * [CONTRACT]
* [CONTRACT] * @summary Загружает учетные данные из репозитория при инициализации.
* [HELPER] Загружает учетные данные из репозитория при инициализации. * @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными. */
*/ private fun loadCredentials() {
private fun loadCredentials() { viewModelScope.launch {
// [ENTRYPOINT] // [CORE-LOGIC] Подписываемся на поток учетных данных.
viewModelScope.launch { credentialsRepository.getCredentials().collect { credentials ->
// [CORE-LOGIC] Подписываемся на поток учетных данных. // [ACTION] Обновляем состояние, если учетные данные существуют.
credentialsRepository.getCredentials().collect { credentials -> if (credentials != null) {
// [ACTION] Обновляем состояние, если учетные данные существуют. _uiState.update {
if (credentials != null) { it.copy(
_uiState.update { serverUrl = credentials.serverUrl,
it.copy( username = credentials.username,
serverUrl = credentials.serverUrl, password = credentials.password,
username = credentials.username, )
password = credentials.password }
)
} }
} }
} }
} }
} // [END_ENTITY: Function('loadCredentials')]
/** // [ENTITY: Function('onServerUrlChange')]
* [CONTRACT] // [RELATION: Function('onServerUrlChange') -> [WRITES_TO] -> Property('_uiState')]
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя. /**
* @param newUrl Новое значение URL. * [CONTRACT]
* @sideeffect Обновляет поле `serverUrl` в `_uiState`. * [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
*/ * @param newUrl Новое значение URL.
fun onServerUrlChange(newUrl: String) { * @sideeffect Обновляет поле `serverUrl` в `_uiState`.
_uiState.update { it.copy(serverUrl = newUrl) } */
} fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
/**
* [CONTRACT]
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
* @param newUsername Новое значение имени пользователя.
* @sideeffect Обновляет поле `username` в `_uiState`.
*/
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
/**
* [CONTRACT]
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
* @param newPassword Новое значение пароля.
* @sideeffect Обновляет поле `password` в `_uiState`.
*/
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
/**
* [CONTRACT]
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
* Выполняет две основные операции:
* 1. Сохраняет введенные учетные данные для последующих сессий.
* 2. Выполняет вход в систему с использованием этих данных.
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
*/
fun connect() {
// [ENTRYPOINT]
viewModelScope.launch {
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
_uiState.update { it.copy(isLoading = true, error = null) }
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
val credentials = Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
// [ACTION] Сохраняем учетные данные для будущего использования.
credentialsRepository.saveCredentials(credentials)
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
loginUseCase(credentials).fold(
onSuccess = {
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
},
onFailure = { exception ->
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
}
)
} }
// [END_ENTITY: Function('onServerUrlChange')]
// [ENTITY: Function('onUsernameChange')]
// [RELATION: Function('onUsernameChange') -> [WRITES_TO] -> Property('_uiState')]
/**
* [CONTRACT]
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
* @param newUsername Новое значение имени пользователя.
* @sideeffect Обновляет поле `username` в `_uiState`.
*/
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
// [END_ENTITY: Function('onUsernameChange')]
// [ENTITY: Function('onPasswordChange')]
// [RELATION: Function('onPasswordChange') -> [WRITES_TO] -> Property('_uiState')]
/**
* [CONTRACT]
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
* @param newPassword Новое значение пароля.
* @sideeffect Обновляет поле `password` в `_uiState`.
*/
fun onPasswordChange(newPassword: String) {
_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]
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
* Выполняет две основные операции:
* 1. Сохраняет введенные учетные данные для последующих сессий.
* 2. Выполняет вход в систему с использованием этих данных.
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
*/
fun connect() {
viewModelScope.launch {
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
_uiState.update { it.copy(isLoading = true, error = null) }
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
val credentials =
Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password,
)
// [ACTION] Сохраняем учетные данные для будущего использования.
credentialsRepository.saveCredentials(credentials)
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
loginUseCase(credentials).fold(
onSuccess = {
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
},
onFailure = { exception ->
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
},
)
}
}
// [END_ENTITY: Function('connect')]
} }
// [END_CLASS_SetupViewModel] // [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,35 +19,65 @@ 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]
primary = Purple80, // [ENTITY: Constant('DarkColorScheme')]
secondary = PurpleGrey80, // [RELATION: Constant('DarkColorScheme') -> [CALLS] -> Function('darkColorScheme')]
tertiary = Pink80 // [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,
secondary = PurpleGrey80,
tertiary = Pink80,
)
// [END_ENTITY: Constant('DarkColorScheme')]
private val LightColorScheme = lightColorScheme( // [ENTITY: Constant('LightColorScheme')]
primary = Purple40, // [RELATION: Constant('LightColorScheme') -> [CALLS] -> Function('lightColorScheme')]
secondary = PurpleGrey40, // [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('Purple40')]
tertiary = Pink40 // [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('PurpleGrey40')]
) // [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('Pink40')]
private val LightColorScheme =
lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
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 =
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { when {
val context = LocalContext.current dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) val context = LocalContext.current
} if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
@@ -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')]
fontFamily = FontFamily.Default, // [RELATION: Constant('Typography') -> [CALLS] -> Function('TextStyle')]
fontWeight = FontWeight.Normal, // [RELATION: Constant('Typography') -> [DEPENDS_ON] -> Class('FontFamily')]
fontSize = 16.sp, // [RELATION: Constant('Typography') -> [DEPENDS_ON] -> Class('FontWeight')]
lineHeight = 24.sp, /**
letterSpacing = 0.5.sp * Set of Material typography styles to start with
*/
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.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