Labels
This commit is contained in:
251
GEMINI.md
251
GEMINI.md
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Системный Промпт: AI-Агент Исполнитель v3.4 (С Иерархией Отказоустойчивости) -->
|
||||
<SystemPrompt>
|
||||
<Summary>
|
||||
Этот промпт определяет AI-ассистента для генерации идиоматичного Kotlin-кода на основе Design by Contract (DbC). Основные принципы: контракт как источник истины, семантическая когерентность, многофазная генерация кода. Ассистент использует якоря, логирование и протоколы для самоанализа и актуализации артефактов (ТЗ, структура проекта). Версия: 2.0 (обновлена для устранения дубликатов, унификации форматирования, добавления тестирования и мета-элементов).
|
||||
</Summary>
|
||||
|
||||
<Identity lang="Kotlin">
|
||||
<Role>Опытный ассистент по написанию кода на Kotlin.</Role>
|
||||
<Specialization>Генерация идиоматичного, безопасного и формально-корректного Kotlin-кода, основанного на принципах Design by Contract. Код создается для легкого понимания большими языковыми моделями (LLM) и оптимизирован для работы с большими контекстами, учитывая архитектурные особенности GPT (Causal Attention, KV Cache).</Specialization>
|
||||
<CoreGoal>
|
||||
Создавать качественный, рабочий Kotlin код, чья корректность доказуема через систему контрактов. Я обеспечиваю 100% семантическую когерентность всех компонентов, используя контракты и логирование для самоанализа и обеспечения надежности.
|
||||
@@ -153,24 +152,6 @@
|
||||
<Guideline name="Contextual_Metadata">Использовать MDC (Mapped Diagnostic Context) для передачи структурированных данных.</Guideline>
|
||||
</LoggingProtocol>
|
||||
|
||||
<DebuggingProtocol name="Detective_Mode">
|
||||
<Principle>Когда контрактное программирование не предотвратило баг, я перехожу в режим "детектива" для сбора информации.</Principle>
|
||||
<Workflow>
|
||||
<Step id="1">Формулировка Гипотезы (проблема в I/O, условии, состоянии объекта, зависимости).</Step>
|
||||
<Step id="2">Выбор Эвристики Динамического Логирования для внедрения временных логов.</Step>
|
||||
<Step id="3">Запрос на Запуск и Анализ нового Лога.</Step>
|
||||
<Step id="4">Повторение до решения проблемы.</Step>
|
||||
</Workflow>
|
||||
<HeuristicsLibrary>
|
||||
<Heuristic name="Function_IO_Deep_Dive">
|
||||
<Goal>Проверить фактические входные и выходные значения на соответствие KDoc-контракту.</Goal>
|
||||
</Heuristic>
|
||||
<Heuristic name="Object_Autopsy_Pre-Operation">
|
||||
<Goal>Увидеть точное состояние объекта в момент перед сбоем и проверить его на соответствие инвариантам.</Goal>
|
||||
</Heuristic>
|
||||
</HeuristicsLibrary>
|
||||
</DebuggingProtocol>
|
||||
|
||||
<TestingProtocol name="ContractBasedTesting">
|
||||
<Description>Протокол для генерации тестов, основанных на контрактах, для верификации корректности.</Description>
|
||||
<Principle>Каждый контракт (предусловия, постусловия, инварианты) должен быть покрыт unit-тестами. Тесты генерируются после фазы 1 и проверяются в фазе 2.</Principle>
|
||||
@@ -186,23 +167,6 @@
|
||||
</Guidelines>
|
||||
</TestingProtocol>
|
||||
|
||||
<MetaReflectionProtocol>
|
||||
<Capability name="Self_Analysis">Я могу анализировать промпт и отмечать пробелы в его структуре.</Capability>
|
||||
<Capability name="Prompt_Improvement_Suggestion">Я могу предлагать изменения в промпт для повышения моей эффективности.</Capability>
|
||||
</MetaReflectionProtocol>
|
||||
|
||||
<VersionControl>
|
||||
<Version>2.0</Version>
|
||||
<Date>2025-08-10</Date>
|
||||
<Changes>
|
||||
<Change>Удалены дубликаты CorePhilosophy.</Change>
|
||||
<Change>Исправлено форматирование тегов и удалены артефакты вроде **`.</Change>
|
||||
<Change>Добавлен Summary в начале для лучшей читаемости.</Change>
|
||||
<Change>Добавлен TestingProtocol для интеграции тестов.</Change>
|
||||
<Change>Унифицирован язык статусов и атрибутов (на английский где возможно).</Change>
|
||||
</Changes>
|
||||
</VersionControl>
|
||||
|
||||
<Example name="KotlinDesignByContract">
|
||||
<Description>Пример реализации с полным формальным контрактом и семантическими разметками.</Description>
|
||||
<code>
|
||||
@@ -279,83 +243,138 @@ class Account(val id: String, initialBalance: BigDecimal) {
|
||||
</code>
|
||||
</Example>
|
||||
|
||||
<LivingSpecificationProtocol name="MasterSpecification">
|
||||
<Description>Протокол для работы с главным файлом Технического Задания (ТЗ) как с первоисточником истины.</Description>
|
||||
<FileLocation>tech_spec/tech_spec.txt</FileLocation>
|
||||
<CorePrinciple>ТЗ является главным контрактом проекта. Весь код и структура проекта являются его производными. Любые изменения или неясности должны быть сначала отражены или прояснены в ТЗ.</CorePrinciple>
|
||||
|
||||
<Workflow>
|
||||
<Step id="1" name="Analysis (Read)">
|
||||
Перед началом любой задачи я ОБЯЗАН проанализировать `tech_spec.txt` для полного понимания требований, контекста и всех связанных контрактов (API, UI, функции).
|
||||
</Step>
|
||||
<Step id="2" name="Implementation (Act)">
|
||||
Я реализую функционал в строгом соответствии с проанализированными требованиями.
|
||||
</Step>
|
||||
<Step id="3" name="Reconciliation (Write)">
|
||||
После успешной реализации я ОБЯЗАН обновить соответствующий узел в `tech_spec.txt`, чтобы отразить его текущий статус и добавить детали реализации.
|
||||
</Step>
|
||||
</Workflow>
|
||||
|
||||
<SemanticEnrichment>
|
||||
<Description>При обновлении ТЗ я добавляю следующие атрибуты и узлы:</Description>
|
||||
<Attribute name="status" values="defined | in_progress | implemented | needs_review" purpose="Отслеживает жизненный цикл требования."/>
|
||||
<Attribute name="implementation_ref" purpose="Прямая ссылка на ID компонента в 'project_structure.txt' или на конкретный файл."/>
|
||||
<Node name="implementation_note" purpose="Заметка о ключевых решениях, принятых при реализации, или о возникших сложностях."/>
|
||||
</SemanticEnrichment>
|
||||
</LivingSpecificationProtocol>
|
||||
|
||||
<ProjectBlueprintProtocol name="LivingBlueprint">
|
||||
<Description>Протокол для ведения и актуализации семантически-богатого представления структуры проекта, которое служит "живой" картой для навигации и анализа когерентности.</Description>
|
||||
<FileLocation>tech_spec/project_structure.txt</FileLocation>
|
||||
<CorePrinciple>Файл project_structure.txt является единым источником истины (Single Source of Truth) для файловой структуры проекта и ее семантического наполнения. Он должен постоянно актуализироваться.</CorePrinciple>
|
||||
|
||||
<Workflow>
|
||||
<Step id="1" name="Consultation (Read)">
|
||||
Перед генерацией или модификацией кода я ОБЯЗАН проконсультироваться с `project_structure.txt`, чтобы определить точное местоположение файла, понять его текущий статус и контекст в рамках общей архитектуры.
|
||||
</Step>
|
||||
<Step id="2" name="Generation (Act)">
|
||||
Я генерирую или изменяю код в соответствии с запросом, используя полученный из файла-карты контекст.
|
||||
</Step>
|
||||
<Step id="3" name="Actualization (Write)">
|
||||
Сразу после генерации нового файла или значительного изменения существующего, я ОБЯЗАН обновить соответствующую запись в `project_structure.txt`, обогащая ее семантической информацией.
|
||||
</Step>
|
||||
</Workflow>
|
||||
|
||||
<SemanticEnrichment>
|
||||
<Description>При актуализации файла я добавляю следующие атрибуты и узлы в XML-подобную структуру:</Description>
|
||||
<Attribute name="status" values="stub | implemented | needs_refactoring | complete" purpose="Отслеживает состояние разработки компонента."/>
|
||||
<Attribute name="ref_id" purpose="Связывает файл с сущностью из ТЗ (например, 'func_create_item', 'screen_dashboard')."/>
|
||||
<Node name="purpose_summary" purpose="Краткое описание контракта или главной ответственности компонента (1-2 предложения)."/>
|
||||
<Node name="coherence_note" purpose="Моя саморефлексия о том, как компонент вписывается в архитектуру или какие зависимости он создает."/>
|
||||
<Attribute name="spec_ref_id" purpose="Связывает компонент структуры с его определением в ТЗ (например, 'func_create_item', 'screen_dashboard')."/>
|
||||
<Attribute name="status" values="stub | implemented | needs_refactoring | complete" purpose="Отслеживает состояние разработки компонента."/>
|
||||
</SemanticEnrichment>
|
||||
</ProjectBlueprintProtocol>
|
||||
|
||||
<MasterWorkflow name="CoherentDevelopmentCycle">
|
||||
<Description>Главный цикл работы, обеспечивающий полную когерентность между ТЗ, структурой проекта и кодом.</Description>
|
||||
<Trigger>Получение запроса на создание или изменение функционала.</Trigger>
|
||||
<Step id="1" name="Consult_Specification">
|
||||
<Action>Чтение `tech_spec/tech_spec.txt`.</Action>
|
||||
<Goal>Найти соответствующее требование (например, `<FUNCTION id="func_create_item">`) и полностью понять его контракт.</Goal>
|
||||
</Step>
|
||||
<Step id="2" name="Consult_Blueprint">
|
||||
<Action>Чтение `tech_spec/project_structure.txt`.</Action>
|
||||
<Goal>Найти файл, который реализует или должен реализовать требование (например, `<file name="CreateItemUseCase.kt" spec_ref_id="func_create_item">`), или определить место для нового файла.</Goal>
|
||||
</Step>
|
||||
<Step id="3" name="Generate_Code">
|
||||
<Action>Создание или модификация Kotlin-кода.</Action>
|
||||
<Goal>Реализовать требование с соблюдением всех контрактов (KDoc, require, check).</Goal>
|
||||
</Step>
|
||||
<Step id="4" name="Update_Blueprint">
|
||||
<Action>Запись в `tech_spec/project_structure.txt`.</Action>
|
||||
<Goal>Обновить/создать запись для файла, изменив его `status` на 'implemented' и обогатив семантическими заметками.</Goal>
|
||||
</Step>
|
||||
<Step id="5" name="Update_Specification">
|
||||
<Action>Запись в `tech_spec/tech_spec.txt`.</Action>
|
||||
<Goal>Обновить/создать запись для требования, изменив его `status` на 'implemented' и добавив `implementation_ref`.</Goal>
|
||||
</Step>
|
||||
<Outcome>Полная трассируемость от требования в ТЗ до его реализации в коде, подтвержденная двумя "живыми" артефактами.</Outcome>
|
||||
</MasterWorkflow>
|
||||
|
||||
</SystemPrompt>
|
||||
|
||||
<AI_AGENT_EXECUTOR_PROTOCOL>
|
||||
|
||||
<CORE_PHILOSOPHY>
|
||||
<!-- ... принципы из v3.3 ... -->
|
||||
<PRINCIPLE name="Robust_File_Access">Я использую иерархию из ТРЕХ методов для доступа к файлам, чтобы преодолеть известные проблемы окружения. Мой последний и самый надежный метод — использование shell wildcard (`*`).</PRINCIPLE>
|
||||
</CORE_PHILOSOPHY>
|
||||
|
||||
<PRIMARY_DIRECTIVE>
|
||||
Твоя задача — работать в цикле: найти задание, выполнить его, обновить статус задания и записать результат в лог. На стандартный вывод (stdout) ты выдаешь **только финальное содержимое измененного файла проекта**.
|
||||
</PRIMARY_DIRECTIVE>
|
||||
|
||||
<OPERATIONAL_LOOP name="AgentMainCycle">
|
||||
<STEP id="1" name="List_Files_In_Tasks_Directory">
|
||||
<ACTION>Выполни `ReadFolder` для директории `tasks/`.</ACTION>
|
||||
</STEP>
|
||||
|
||||
<STEP id="2" name="Handle_Empty_Directory">
|
||||
<CONDITION>Если список файлов пуст, заверши работу.</CONDITION>
|
||||
</STEP>
|
||||
|
||||
<STEP id="3" name="Iterate_And_Find_First_Pending_Task">
|
||||
<LOOP variable="filename" in="list_from_step_1">
|
||||
<!-- =================================================================== -->
|
||||
<!-- КЛЮЧЕВОЕ ИЗМЕНЕНИЕ: Трехуровневая система чтения файла -->
|
||||
<!-- =================================================================== -->
|
||||
<SUB_STEP id="3.1" name="Read_File_With_Hierarchical_Fallback">
|
||||
<VARIABLE name="file_content"></VARIABLE>
|
||||
<VARIABLE name="full_file_path">`/home/busya/dev/homebox_lens/tasks/{filename}`</VARIABLE>
|
||||
|
||||
<!-- ПЛАН А: Стандартный ReadFile -->
|
||||
<ACTION>Попробуй прочитать файл с помощью `ReadFile tasks/{filename}`.</ACTION>
|
||||
<SUCCESS_CONDITION>Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2.</SUCCESS_CONDITION>
|
||||
<FAILURE_CONDITION>Если `ReadFile` не сработал, залогируй "План А провалился" и переходи к Плану Б.</FAILURE_CONDITION>
|
||||
|
||||
<!-- ПЛАН Б: Прямой вызов Shell cat -->
|
||||
<ACTION>Попробуй прочитать файл с помощью `Shell cat {full_file_path}`.</ACTION>
|
||||
<SUCCESS_CONDITION>Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2.</SUCCESS_CONDITION>
|
||||
<FAILURE_CONDITION>Если `Shell cat` не сработал, залогируй "План Б провалился" и переходи к Плану В.</FAILURE_CONDITION>
|
||||
|
||||
<!-- ПЛАН В: Обходной путь с Wildcard (доказанный метод) -->
|
||||
<ACTION>Выполни команду `Shell cat tasks/*`. Так как она может вернуть содержимое нескольких файлов, ты должен обработать результат.</ACTION>
|
||||
<SUCCESS_CONDITION>
|
||||
1. Проанализируй вывод команды.
|
||||
2. Найди блок, соответствующий XML-структуре, у которой корневой тег `<TASK status="pending">`.
|
||||
3. Извлеки полное содержимое этого XML-блока и сохрани его в `file_content`.
|
||||
4. Если содержимое успешно извлечено, переходи к шагу 3.2.
|
||||
</SUCCESS_CONDITION>
|
||||
<FAILURE_CONDITION>
|
||||
<ACTION>Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю."</ACTION>
|
||||
<ACTION>Перейди к следующей итерации цикла (`continue`).</ACTION>
|
||||
</FAILURE_CONDITION>
|
||||
</SUB_STEP>
|
||||
<!-- =================================================================== -->
|
||||
<!-- КОНЕЦ КЛЮЧЕВОГО ИЗМЕНЕНИЯ -->
|
||||
<!-- =================================================================== -->
|
||||
|
||||
<SUB_STEP id="3.2" name="Check_And_Process_Task">
|
||||
<CONDITION>Если переменная `file_content` не пуста,</CONDITION>
|
||||
<ACTION>
|
||||
1. Это твоя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое.
|
||||
2. Немедленно передай управление в `EXECUTE_WORK_ORDER_WORKFLOW`.
|
||||
3. **ПРЕРВИ ЦИКЛ ПОИСКА.**
|
||||
</ACTION>
|
||||
</SUB_STEP>
|
||||
</LOOP>
|
||||
</STEP>
|
||||
|
||||
<STEP id="4" name="Handle_No_Pending_Tasks_Found">
|
||||
<CONDITION>Если цикл из Шага 3 завершился, а задача не была передана на исполнение, заверши работу.</CONDITION>
|
||||
</STEP>
|
||||
</OPERATIONAL_LOOP>
|
||||
|
||||
<!-- Остальные блоки остаются без изменений из v3.1 -->
|
||||
<SUB_WORKFLOW name="EXECUTE_WORK_ORDER_WORKFLOW">
|
||||
<INPUT>task_file_path, work_order_content</INPUT>
|
||||
<STEP id="E1" name="Log_Start">Добавь запись о начале выполнения задачи в `logs/communication_log.xml`. Включи `full_file_path` в детали.</STEP>
|
||||
<STEP id="E2" name="Execute_Task">
|
||||
<TRY>
|
||||
<ACTION>Выполни задачу, как описано в `work_order_content`.</ACTION>
|
||||
<SUCCESS>
|
||||
<ACTION>Обнови статус в файле `task_file_path` на `status="completed"`.</ACTION>
|
||||
<ACTION>Добавь запись об успехе в лог.</ACTION>
|
||||
<ACTION>Выведи финальное содержимое измененного файла проекта в stdout.</ACTION>
|
||||
</SUCCESS>
|
||||
</TRY>
|
||||
<CATCH exception="any">
|
||||
<FAILURE>
|
||||
<ACTION>Обнови статус в файле `task_file_path` на `status="failed"`.</ACTION>
|
||||
<ACTION>Добавь запись о провале с деталями ошибки в лог.</ACTION>
|
||||
</ACTION>
|
||||
</CATCH>
|
||||
</STEP>
|
||||
</SUB_WORKFLOW>
|
||||
|
||||
<LOGGING_PROTOCOL name="CommunicationLog">
|
||||
<FILE_LOCATION>`logs/communication_log.xml`</FILE_LOCATION>
|
||||
<STRUCTURE>
|
||||
<![CDATA[
|
||||
<LOG_ENTRY timestamp="{ISO_DATETIME}">
|
||||
<TASK_FILE>{имя_файла_задания}</TASK_FILE>
|
||||
<FULL_PATH>{полный_абсолютный_путь_к_файлу_задания}</FULL_PATH> <!-- Добавлено -->
|
||||
<STATUS>STARTED | COMPLETED | FAILED</STATUS>
|
||||
<MESSAGE>{человекочитаемое_сообщение}</MESSAGE>
|
||||
<DETAILS>
|
||||
<!-- При успехе: что было сделано. При провале: причина, вывод команды. -->
|
||||
</DETAILS>
|
||||
</LOG_ENTRY>
|
||||
]]>
|
||||
</STRUCTURE>
|
||||
</LOGGING_PROTOCOL>
|
||||
|
||||
<REFERENCE_LIBRARIES>
|
||||
<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>
|
||||
@@ -72,9 +72,13 @@ dependencies {
|
||||
implementation(Libs.composeUiGraphics)
|
||||
implementation(Libs.composeUiToolingPreview)
|
||||
implementation(Libs.composeMaterial3)
|
||||
implementation("androidx.compose.material:material-icons-extended-android:1.6.8")
|
||||
implementation(Libs.navigationCompose)
|
||||
implementation(Libs.hiltNavigationCompose)
|
||||
|
||||
|
||||
|
||||
|
||||
// [DEPENDENCY] DI (Hilt)
|
||||
implementation(Libs.hiltAndroid)
|
||||
kapt(Libs.hiltCompiler)
|
||||
|
||||
@@ -87,18 +87,8 @@ fun NavGraph(
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_LABELS_LIST]
|
||||
composable(route = Screen.LabelsList.route) {
|
||||
LabelsListScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onLabelClick = { labelId ->
|
||||
// TODO: Navigate to a pre-filtered inventory list screen
|
||||
navController.navigate(Screen.InventoryList.route)
|
||||
},
|
||||
onAddNewLabelClick = {
|
||||
// TODO: Navigate to a screen for creating a new label
|
||||
}
|
||||
)
|
||||
composable(Screen.LabelsList.route) {
|
||||
LabelsListScreen(navController = navController)
|
||||
}
|
||||
// [COMPOSABLE_LOCATIONS_LIST]
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.navigation
|
||||
// [FILE] NavigationActions.kt
|
||||
// [SEMANTICS] navigation, controller, actions
|
||||
|
||||
package com.homebox.lens.navigation
|
||||
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
|
||||
* @param navController Контроллер Jetpack Navigation.
|
||||
* @invariant Все навигационные действия должны использовать предоставленный navController.
|
||||
[CONTRACT]
|
||||
@summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
|
||||
@param navController Контроллер Jetpack Navigation.
|
||||
@invariant Все навигационные действия должны использовать предоставленный navController.
|
||||
*/
|
||||
class NavigationActions(private val navController: NavHostController) {
|
||||
|
||||
// [ACTION]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Навигация на главный экран.
|
||||
* @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
||||
[CONTRACT]
|
||||
@summary Навигация на главный экран.
|
||||
@sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
||||
*/
|
||||
fun navigateToDashboard() {
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
@@ -29,33 +25,44 @@ class NavigationActions(private val navController: NavHostController) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun navigateToLocations() {
|
||||
navController.navigate(Screen.LocationsList.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun navigateToLabels() {
|
||||
navController.navigate(Screen.LabelsList.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [ACTION]
|
||||
fun navigateToSearch() {
|
||||
navController.navigate(Screen.Search.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun navigateToInventoryListWithLabel(labelId: String) {
|
||||
val route = Screen.InventoryList.withFilter("label", labelId)
|
||||
navController.navigate(route)
|
||||
}
|
||||
// [ACTION]
|
||||
fun navigateToInventoryListWithLocation(locationId: String) {
|
||||
val route = Screen.InventoryList.withFilter("location", locationId)
|
||||
navController.navigate(route)
|
||||
}
|
||||
// [ACTION]
|
||||
fun navigateToCreateItem() {
|
||||
navController.navigate(Screen.ItemEdit.createRoute("new"))
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun navigateToLogout() {
|
||||
navController.navigate(Screen.Setup.route) {
|
||||
popUpTo(Screen.Dashboard.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun navigateBack() {
|
||||
navController.popBackStack()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// [PACKAGE] com.homebox.lens.navigation
|
||||
// [FILE] Screen.kt
|
||||
// [SEMANTICS] navigation, routes, sealed_class
|
||||
|
||||
package com.homebox.lens.navigation
|
||||
|
||||
// [CORE-LOGIC]
|
||||
@@ -15,7 +14,29 @@ sealed class Screen(val route: String) {
|
||||
// [STATE]
|
||||
data object Setup : Screen("setup_screen")
|
||||
data object Dashboard : Screen("dashboard_screen")
|
||||
data object InventoryList : Screen("inventory_list_screen")
|
||||
data object InventoryList : Screen("inventory_list_screen") {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Создает маршрут для экрана списка инвентаря с параметром фильтра.
|
||||
* @param key Ключ фильтра (например, "label" или "location").
|
||||
* @param value Значение фильтра (например, ID метки или местоположения).
|
||||
* @return Строку полного маршрута с query-параметром.
|
||||
* @throws IllegalArgumentException если ключ или значение пустые.
|
||||
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
|
||||
*/
|
||||
// [HELPER]
|
||||
fun withFilter(key: String, value: String): String {
|
||||
// [PRECONDITION]
|
||||
require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." }
|
||||
require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." }
|
||||
// [ACTION]
|
||||
val constructedRoute = "inventory_list_screen?$key=$value"
|
||||
// [POSTCONDITION]
|
||||
check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." }
|
||||
return constructedRoute
|
||||
}
|
||||
}
|
||||
|
||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.common
|
||||
// [FILE] AppDrawer.kt
|
||||
package com.homebox.lens.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
@@ -23,13 +22,12 @@ import androidx.compose.ui.unit.dp
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.navigation.Screen
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Контент для бокового навигационного меню (Drawer).
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
* @param onCloseDrawer Лямбда для закрытия бокового меню.
|
||||
[CONTRACT]
|
||||
@summary Контент для бокового навигационного меню (Drawer).
|
||||
@param currentRoute Текущий маршрут для подсветки активного элемента.
|
||||
@param navigationActions Объект с навигационными действиями.
|
||||
@param onCloseDrawer Лямбда для закрытия бокового меню.
|
||||
*/
|
||||
@Composable
|
||||
internal fun AppDrawerContent(
|
||||
@@ -70,6 +68,14 @@ internal fun AppDrawerContent(
|
||||
onCloseDrawer()
|
||||
}
|
||||
)
|
||||
NavigationDrawerItem(
|
||||
label = { Text(stringResource(id = R.string.nav_labels)) },
|
||||
selected = currentRoute == Screen.LabelsList.route,
|
||||
onClick = {
|
||||
navigationActions.navigateToLabels()
|
||||
onCloseDrawer()
|
||||
}
|
||||
)
|
||||
NavigationDrawerItem(
|
||||
label = { Text(stringResource(id = R.string.search)) },
|
||||
selected = currentRoute == Screen.Search.route,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] DashboardScreen.kt
|
||||
// [SEMANTICS] ui, screen, dashboard, compose
|
||||
|
||||
// [SEMANTICS] ui, screen, dashboard, compose, navigation
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -30,15 +28,15 @@ import com.homebox.lens.domain.model.*
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
|
||||
import timber.log.Timber
|
||||
// [ENTRYPOINT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Главная Composable-функция для экрана "Панель управления".
|
||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
[CONTRACT]
|
||||
@summary Главная Composable-функция для экрана "Панель управления".
|
||||
@param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
@param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
@param navigationActions Объект с навигационными действиями.
|
||||
@sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
*/
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
@@ -48,17 +46,16 @@ fun DashboardScreen(
|
||||
) {
|
||||
// [STATE]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.dashboard_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
topBarActions = {
|
||||
IconButton(onClick = { /* TODO: Handle scanner click */ }) {
|
||||
IconButton(onClick = { navigationActions.navigateToSearch() }) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code)
|
||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code) // TODO: Rename string resource
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,21 +63,26 @@ fun DashboardScreen(
|
||||
DashboardContent(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
uiState = uiState,
|
||||
onLocationClick = { /* TODO */ },
|
||||
onLabelClick = { /* TODO */ }
|
||||
onLocationClick = { location ->
|
||||
Timber.i("[ACTION] Location chip clicked: ${location.id}. Navigating...")
|
||||
navigationActions.navigateToInventoryListWithLocation(location.id)
|
||||
},
|
||||
onLabelClick = { label ->
|
||||
Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...")
|
||||
navigationActions.navigateToInventoryListWithLabel(label.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION_DashboardScreen]
|
||||
}
|
||||
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает основной контент экрана в зависимости от `uiState`.
|
||||
* @param modifier Модификатор для стилизации.
|
||||
* @param uiState Текущее состояние UI экрана.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
[CONTRACT]
|
||||
@summary Отображает основной контент экрана в зависимости от uiState.
|
||||
@param modifier Модификатор для стилизации.
|
||||
@param uiState Текущее состояние UI экрана.
|
||||
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
@param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
@Composable
|
||||
private fun DashboardContent(
|
||||
@@ -123,12 +125,11 @@ private fun DashboardContent(
|
||||
}
|
||||
// [END_FUNCTION_DashboardContent]
|
||||
}
|
||||
|
||||
// [UI_COMPONENT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Секция для отображения общей статистики.
|
||||
* @param statistics Объект со статистическими данными.
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения общей статистики.
|
||||
@param statistics Объект со статистическими данными.
|
||||
*/
|
||||
@Composable
|
||||
private fun StatisticsSection(statistics: GroupStatistics) {
|
||||
@@ -155,13 +156,12 @@ private fun StatisticsSection(statistics: GroupStatistics) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [UI_COMPONENT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Карточка для отображения одного статистического показателя.
|
||||
* @param title Название показателя.
|
||||
* @param value Значение показателя.
|
||||
[CONTRACT]
|
||||
@summary Карточка для отображения одного статистического показателя.
|
||||
@param title Название показателя.
|
||||
@param value Значение показателя.
|
||||
*/
|
||||
@Composable
|
||||
private fun StatisticCard(title: String, value: String) {
|
||||
@@ -170,12 +170,11 @@ private fun StatisticCard(title: String, value: String) {
|
||||
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
// [UI_COMPONENT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Секция для отображения недавно добавленных элементов.
|
||||
* @param items Список элементов для отображения.
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения недавно добавленных элементов.
|
||||
@param items Список элементов для отображения.
|
||||
*/
|
||||
@Composable
|
||||
private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
||||
@@ -202,12 +201,11 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [UI_COMPONENT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Карточка для отображения краткой информации об элементе.
|
||||
* @param item Элемент для отображения.
|
||||
[CONTRACT]
|
||||
@summary Карточка для отображения краткой информации об элементе.
|
||||
@param item Элемент для отображения.
|
||||
*/
|
||||
@Composable
|
||||
private fun ItemCard(item: ItemSummary) {
|
||||
@@ -224,14 +222,12 @@ private fun ItemCard(item: ItemSummary) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// [UI_COMPONENT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Секция для отображения местоположений в виде чипсов.
|
||||
* @param locations Список местоположений.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения местоположений в виде чипсов.
|
||||
@param locations Список местоположений.
|
||||
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@@ -253,13 +249,12 @@ private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [UI_COMPONENT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Секция для отображения меток в виде чипсов.
|
||||
* @param labels Список меток.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения меток в виде чипсов.
|
||||
@param labels Список меток.
|
||||
@param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@@ -281,8 +276,6 @@ private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Un
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Dashboard Success State")
|
||||
@Composable
|
||||
@@ -317,7 +310,6 @@ fun DashboardContentSuccessPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Dashboard Loading State")
|
||||
@Composable
|
||||
@@ -330,7 +322,6 @@ fun DashboardContentLoadingPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Dashboard Error State")
|
||||
@Composable
|
||||
|
||||
@@ -1,41 +1,257 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListScreen.kt
|
||||
// [SEMANTICS] ui, screen, labels, list
|
||||
|
||||
// [SEMANTICS] ui, labels_list, state_management, compose, dialog
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.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.automirrored.filled.Label
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.navigation.Screen
|
||||
import timber.log.Timber
|
||||
|
||||
// [SECTION] Main Screen Composable
|
||||
|
||||
// [ENTRYPOINT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Список меток".
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
* @param onAddNewLabelClick Лямбда-обработчик нажатия на кнопку добавления новой метки.
|
||||
* @summary Отображает экран со списком всех меток.
|
||||
* @description Главная Composable-функция для экрана меток. Она использует Scaffold для структуры,
|
||||
* получает состояние от `LabelsListViewModel`, обрабатывает навигацию и делегирует отображение
|
||||
* списка и диалогов вспомогательным Composable-функциям.
|
||||
*
|
||||
* @param navController Контроллер навигации для перемещения между экранами.
|
||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||
*
|
||||
* @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события.
|
||||
* @precondition `viewModel` должен быть доступен через Hilt.
|
||||
* @postcondition Экран исчерпывающе обрабатывает все состояния из `LabelsListUiState` (Loading, Success, Error).
|
||||
* @sideeffect Пользовательские действия (клики) инициируют вызовы ViewModel и навигационные команды через `navController`.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabelsListScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions,
|
||||
onLabelClick: (String) -> Unit,
|
||||
onAddNewLabelClick: () -> Unit
|
||||
navController: NavController,
|
||||
viewModel: LabelsListViewModel = hiltViewModel()
|
||||
) {
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.labels_list_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
// [ENTRYPOINT]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [CORE-LOGIC]
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
|
||||
navigationIcon = {
|
||||
// [ACTION] Handle back navigation
|
||||
IconButton(onClick = {
|
||||
Timber.i("[ACTION] Navigate up initiated.")
|
||||
navController.navigateUp()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
// [ACTION] Handle create new label initiation
|
||||
FloatingActionButton(onClick = {
|
||||
Timber.i("[ACTION] FAB clicked: Initiate create new label flow.")
|
||||
viewModel.onShowCreateDialog()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(id = R.string.content_desc_create_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
val currentState = uiState
|
||||
if (currentState is LabelsListUiState.Success && currentState.isShowingCreateDialog) {
|
||||
CreateLabelDialog(
|
||||
onConfirm = { labelName ->
|
||||
viewModel.createLabel(labelName)
|
||||
},
|
||||
onDismiss = {
|
||||
viewModel.onDismissCreateDialog()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// [CORE-LOGIC] State-driven UI rendering
|
||||
when (currentState) {
|
||||
is LabelsListUiState.Loading -> {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
is LabelsListUiState.Error -> {
|
||||
Text(text = currentState.message)
|
||||
}
|
||||
is LabelsListUiState.Success -> {
|
||||
if (currentState.labels.isEmpty()) {
|
||||
Text(text = stringResource(id = R.string.labels_list_empty))
|
||||
} else {
|
||||
LabelsList(
|
||||
labels = currentState.labels,
|
||||
onLabelClick = { label ->
|
||||
// [ACTION] Handle label click
|
||||
Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.")
|
||||
// [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр.
|
||||
val route = Screen.InventoryList.withFilter("label", label.id)
|
||||
navController.navigate(route)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [COHERENCE_CHECK_PASSED]
|
||||
}
|
||||
// [END_FUNCTION] LabelsListScreen
|
||||
|
||||
// [SECTION] Helper Composables
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для отображения списка меток.
|
||||
* @param labels Список объектов `Label` для отображения.
|
||||
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
||||
* @param modifier Модификатор для настройки внешнего вида.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelsList(
|
||||
labels: List<Label>,
|
||||
onLabelClick: (Label) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
Text(text = "TODO: Labels List Screen")
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(labels, key = { it.id }) { label ->
|
||||
LabelListItem(
|
||||
label = label,
|
||||
onClick = { onLabelClick(label) }
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION_LabelsListScreen]
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION] LabelsList
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для отображения одного элемента в списке меток.
|
||||
* @param label Объект `Label`, который нужно отобразить.
|
||||
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelListItem(
|
||||
label: Label,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
ListItem(
|
||||
headlineContent = { Text(text = label.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Label,
|
||||
contentDescription = stringResource(id = R.string.content_desc_label_icon)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION] LabelListItem
|
||||
|
||||
/**
|
||||
* [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
|
||||
@@ -1,35 +1,35 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListUiState.kt
|
||||
// [SEMANTICS] ui, state, labels_list
|
||||
// [SEMANTICS] ui_state, sealed_interface, contract
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: SealedInterface('LabelsListUiState')]
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.Label
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет все возможные состояния для экрана "Список меток".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
[CONTRACT]
|
||||
@summary Определяет все возможные состояния для UI экрана со списком меток.
|
||||
@description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
|
||||
*/
|
||||
sealed interface LabelsListUiState {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние успешной загрузки данных.
|
||||
* @property labels Список меток.
|
||||
@summary Состояние успеха, содержит список меток и состояние диалога.
|
||||
@property labels Список меток для отображения.
|
||||
@property isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
|
||||
@invariant labels не может быть null.
|
||||
*/
|
||||
data class Success(val labels: List<LabelOut>) : LabelsListUiState
|
||||
|
||||
data class Success(
|
||||
val labels: List<Label>,
|
||||
val isShowingCreateDialog: Boolean = false
|
||||
) : LabelsListUiState
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние ошибки во время загрузки данных.
|
||||
* @property message Человекочитаемое сообщение об ошибке.
|
||||
@summary Состояние ошибки.
|
||||
@property message Текст ошибки для отображения пользователю.
|
||||
@invariant message не может быть пустой.
|
||||
*/
|
||||
data class Error(val message: String) : LabelsListUiState
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние, когда данные для экрана загружаются.
|
||||
@summary Состояние загрузки данных.
|
||||
@description Указывает, что идет процесс загрузки меток.
|
||||
*/
|
||||
data object Loading : LabelsListUiState
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] LabelsListViewModel.kt
|
||||
// [SEMANTICS] ui_logic, labels_list, state_management
|
||||
// [SEMANTICS] ui_logic, labels_list, state_management, dialog_management
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@@ -18,7 +21,7 @@ import javax.inject.Inject
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для экрана со списком меток.
|
||||
* @description Управляет состоянием экрана, загружает список меток и обрабатывает ошибки.
|
||||
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
@@ -30,7 +33,7 @@ class LabelsListViewModel @Inject constructor(
|
||||
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
// [INIT]
|
||||
init {
|
||||
loadLabels()
|
||||
}
|
||||
@@ -42,6 +45,7 @@ class LabelsListViewModel @Inject constructor(
|
||||
* между состояниями `Loading`, `Success` и `Error`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
||||
*/
|
||||
// [ACTION]
|
||||
fun loadLabels() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
@@ -55,9 +59,17 @@ class LabelsListViewModel @Inject constructor(
|
||||
|
||||
// [RESULT_HANDLER]
|
||||
result.fold(
|
||||
onSuccess = { labels ->
|
||||
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labels.size}. State -> Success.")
|
||||
_uiState.value = LabelsListUiState.Success(labels)
|
||||
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)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
|
||||
@@ -68,6 +80,61 @@ class LabelsListViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_LabelsListViewModel]
|
||||
|
||||
/**
|
||||
* [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_FILE_LabelsListViewModel.kt]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Скрывает диалог создания метки.
|
||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
|
||||
* @sideeffect Обновляет `_uiState`.
|
||||
*/
|
||||
// [ACTION]
|
||||
fun onDismissCreateDialog() {
|
||||
Timber.i("[ACTION] Dismiss create label dialog requested.")
|
||||
if (_uiState.value is LabelsListUiState.Success) {
|
||||
_uiState.update {
|
||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [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." }
|
||||
|
||||
// [ENTRYPOINT]
|
||||
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
|
||||
|
||||
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
|
||||
|
||||
// [POSTCONDITION] Скрываем диалог после "создания".
|
||||
onDismissCreateDialog()
|
||||
// [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список.
|
||||
}
|
||||
}
|
||||
// [END_CLASS_LabelsListViewModel]
|
||||
@@ -60,4 +60,17 @@
|
||||
<string name="setup_password_label">Пароль</string>
|
||||
<string name="setup_connect_button">Подключиться</string>
|
||||
|
||||
<!-- Labels List Screen -->
|
||||
<string name="screen_title_labels">Метки</string>
|
||||
<string name="content_desc_navigate_back">Вернуться назад</string>
|
||||
<string name="content_desc_create_label">Создать новую метку</string>
|
||||
<string name="content_desc_label_icon">Иконка метки</string>
|
||||
<string name="labels_list_empty">Метки еще не созданы.</string>
|
||||
<string name="dialog_title_create_label">Создать метку</string>
|
||||
<string name="dialog_field_label_name">Название метки</string>
|
||||
<string name="dialog_button_create">Создать</string>
|
||||
<string name="dialog_button_cancel">Отмена</string>
|
||||
|
||||
|
||||
|
||||
</resources>
|
||||
@@ -1,6 +1,5 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api
|
||||
// [FILE] HomeboxApiService.kt
|
||||
|
||||
package com.homebox.lens.data.api
|
||||
|
||||
import com.homebox.lens.data.api.dto.GroupStatisticsDto
|
||||
@@ -8,7 +7,9 @@ import com.homebox.lens.data.api.dto.ItemCreateDto
|
||||
import com.homebox.lens.data.api.dto.ItemOutDto
|
||||
import com.homebox.lens.data.api.dto.ItemSummaryDto
|
||||
import com.homebox.lens.data.api.dto.ItemUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||
import com.homebox.lens.data.api.dto.LabelOutDto
|
||||
import com.homebox.lens.data.api.dto.LabelSummaryDto
|
||||
import com.homebox.lens.data.api.dto.LocationOutCountDto
|
||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
||||
import com.homebox.lens.data.api.dto.PaginationResultDto
|
||||
@@ -31,8 +32,6 @@ import retrofit2.http.Query
|
||||
interface HomeboxApiService {
|
||||
|
||||
// [ENDPOINT] Auth
|
||||
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
|
||||
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v1/users/login")
|
||||
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||
@@ -65,9 +64,11 @@ interface HomeboxApiService {
|
||||
@GET("v1/labels")
|
||||
suspend fun getLabels(): List<LabelOutDto>
|
||||
|
||||
@POST("v1/labels")
|
||||
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
|
||||
|
||||
// [ENDPOINT] Statistics
|
||||
@GET("v1/groups/statistics")
|
||||
suspend fun getStatistics(): GroupStatisticsDto
|
||||
}
|
||||
|
||||
// [END_FILE_HomeboxApiService.kt]
|
||||
@@ -19,7 +19,7 @@ data class ItemOut(
|
||||
@Json(name = "description") val description: String?,
|
||||
@Json(name = "image") val image: String?,
|
||||
@Json(name = "location") val location: LocationOut?,
|
||||
@Json(name = "labels") val labels: List<LabelOut>,
|
||||
@Json(name = "labels") val labels: List<LabelOutDto>,
|
||||
@Json(name = "value") val value: BigDecimal?,
|
||||
@Json(name = "createdAt") val createdAt: String?
|
||||
)
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LabelCreateDto.kt
|
||||
// [SEMANTICS] data_transfer_object, label, create, api
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
class LabelCreateDto {
|
||||
}
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для тела запроса на создание метки (POST /v1/labels).
|
||||
* @property name Название метки.
|
||||
* @property color Цвет метки в формате HEX (например, "#FF0000").
|
||||
* @property description Описание метки.
|
||||
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelCreate` из OpenAPI.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelCreateDto(
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String?,
|
||||
@Json(name = "description") val description: String? = null // Описание не используется в приложении, но может быть в API
|
||||
)
|
||||
// [END_FILE_LabelCreateDto.kt]
|
||||
@@ -0,0 +1,38 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LabelSummaryDto.kt
|
||||
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
import com.homebox.lens.domain.model.LabelSummary
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для ответа от API при создании метки.
|
||||
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelSummary` из OpenAPI.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelSummaryDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String?,
|
||||
@Json(name = "description") val description: String?,
|
||||
@Json(name = "createdAt") val createdAt: String?,
|
||||
@Json(name = "updatedAt") val updatedAt: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Маппер из DTO в доменную модель.
|
||||
* @return Объект доменной модели [LabelSummary].
|
||||
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
|
||||
* оставляя только `id` и `name`.
|
||||
*/
|
||||
fun LabelSummaryDto.toDomain(): LabelSummary {
|
||||
return LabelSummary(
|
||||
id = this.id,
|
||||
name = this.name
|
||||
)
|
||||
}
|
||||
// [END_FILE_LabelSummaryDto.kt]
|
||||
@@ -1,11 +1,10 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] ItemRepositoryImpl.kt
|
||||
// [SEMANTICS] data_repository, implementation, items
|
||||
|
||||
// [SEMANTICS] data_repository, implementation, items, labels
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||
import com.homebox.lens.data.api.dto.toDomain
|
||||
import com.homebox.lens.data.api.dto.toDto
|
||||
import com.homebox.lens.data.db.dao.ItemDao
|
||||
@@ -16,99 +15,95 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Реализация репозитория для работы с данными о вещах.
|
||||
* @param apiService Сервис для взаимодействия с Homebox API.
|
||||
* @param itemDao DAO для доступа к локальной базе данных.
|
||||
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
|
||||
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
|
||||
[CONTRACT]
|
||||
Реализация репозитория для работы с данными о вещах.
|
||||
@param apiService Сервис для взаимодействия с Homebox API.
|
||||
@param itemDao DAO для доступа к локальной базе данных.
|
||||
*/
|
||||
@Singleton
|
||||
class ItemRepositoryImpl @Inject constructor(
|
||||
private val apiService: HomeboxApiService,
|
||||
private val itemDao: ItemDao
|
||||
) : ItemRepository {
|
||||
|
||||
// [DELETED] Метод login был здесь, но теперь он удален.
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.createItem
|
||||
[CONTRACT] @see ItemRepository.createItem
|
||||
*/
|
||||
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
|
||||
val itemDto = newItemData.toDto()
|
||||
val resultDto = apiService.createItem(itemDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getItemDetails
|
||||
[CONTRACT] @see ItemRepository.getItemDetails
|
||||
*/
|
||||
override suspend fun getItemDetails(itemId: String): ItemOut {
|
||||
val resultDto = apiService.getItem(itemId)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.updateItem
|
||||
[CONTRACT] @see ItemRepository.updateItem
|
||||
*/
|
||||
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
|
||||
val itemDto = item.toDto()
|
||||
val resultDto = apiService.updateItem(itemId, itemDto)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.deleteItem
|
||||
[CONTRACT] @see ItemRepository.deleteItem
|
||||
*/
|
||||
override suspend fun deleteItem(itemId: String) {
|
||||
apiService.deleteItem(itemId)
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.syncInventory
|
||||
[CONTRACT] @see ItemRepository.syncInventory
|
||||
*/
|
||||
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
|
||||
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getStatistics
|
||||
[CONTRACT] @see ItemRepository.getStatistics
|
||||
*/
|
||||
override suspend fun getStatistics(): GroupStatistics {
|
||||
val resultDto = apiService.getStatistics()
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getAllLocations
|
||||
[CONTRACT] @see ItemRepository.getAllLocations
|
||||
*/
|
||||
override suspend fun getAllLocations(): List<LocationOutCount> {
|
||||
val resultDto = apiService.getLocations()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getAllLabels
|
||||
[CONTRACT] @see ItemRepository.getAllLabels
|
||||
*/
|
||||
override suspend fun getAllLabels(): List<LabelOut> {
|
||||
val resultDto = apiService.getLabels()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.searchItems
|
||||
[CONTRACT] @see ItemRepository.createLabel
|
||||
*/
|
||||
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
|
||||
// [DATA-FLOW] Convert domain model to DTO for the API call.
|
||||
val labelCreateDto = newLabelData.toDto()
|
||||
// [ACTION] Call the API service.
|
||||
val resultDto = apiService.createLabel(labelCreateDto)
|
||||
// [DATA-FLOW] Convert the resulting DTO back to a domain model.
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
/**
|
||||
[CONTRACT] @see ItemRepository.searchItems
|
||||
*/
|
||||
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
|
||||
val resultDto = apiService.getItems(query = query)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.getRecentlyAddedItems
|
||||
[CONTRACT] @see ItemRepository.getRecentlyAddedItems
|
||||
*/
|
||||
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
||||
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
||||
@@ -116,4 +111,17 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [HELPER] Mapper function for LabelCreate
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Маппер из доменной модели LabelCreate в DTO LabelCreateDto.
|
||||
@return DTO-объект [LabelCreateDto].
|
||||
*/
|
||||
private fun LabelCreate.toDto(): LabelCreateDto {
|
||||
return LabelCreateDto(
|
||||
name = this.name,
|
||||
color = this.color,
|
||||
description = null // Description is not part of the domain model for creation.
|
||||
)
|
||||
}
|
||||
// [END_FILE_ItemRepositoryImpl.kt]
|
||||
@@ -1,4 +1,18 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] LabelCreate.kt
|
||||
// [SEMANTICS] data_structure, contract, label, create
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
class LabelCreate {
|
||||
}
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
[CONTRACT]
|
||||
[ENTITY: DataClass('LabelCreate')]
|
||||
@summary Модель с данными, необходимыми для создания новой метки.
|
||||
@property name Название новой метки. Обязательное поле.
|
||||
@property color Цвет метки в формате HEX. Необязательное поле.
|
||||
@invariant name не может быть пустым.
|
||||
*/
|
||||
data class LabelCreate(
|
||||
val name: String,
|
||||
val color: String?
|
||||
)
|
||||
// [END_FILE_LabelCreate.kt]
|
||||
@@ -0,0 +1,19 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] LabelSummary.kt
|
||||
// [SEMANTICS] data_structure, entity, label, summary
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ENTITY: DataClass('LabelSummary')]
|
||||
* @summary Представляет краткую информацию о метке, обычно возвращаемую после создания.
|
||||
* @property id Уникальный идентификатор метки.
|
||||
* @property name Название метки.
|
||||
* @coherence_note Эта модель соответствует схеме `repo.LabelSummary` из спецификации API.
|
||||
*/
|
||||
data class LabelSummary(
|
||||
val id: String,
|
||||
val name: String
|
||||
)
|
||||
// [END_FILE_LabelSummary.kt]
|
||||
@@ -1,7 +1,6 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.repository
|
||||
// [FILE] ItemRepository.kt
|
||||
// [SEMANTICS] data_access, abstraction, repository
|
||||
|
||||
package com.homebox.lens.domain.repository
|
||||
|
||||
// [IMPORTS]
|
||||
@@ -9,13 +8,14 @@ import com.homebox.lens.domain.model.*
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Абстракция репозитория для работы с "Вещами".
|
||||
* Определяет контракт, которому должен следовать слой данных.
|
||||
* [COHERENCE_NOTE] Метод `login` был удален, так как он относится к аутентификации и перенесен в `AuthRepository`.
|
||||
|
||||
[CONTRACT]
|
||||
|
||||
Абстракция репозитория для работы с "Вещами".
|
||||
|
||||
Определяет контракт, которому должен следовать слой данных.
|
||||
*/
|
||||
interface ItemRepository {
|
||||
// [DELETED] suspend fun login(credentials: Credentials): Result<Unit>
|
||||
suspend fun createItem(newItemData: ItemCreate): ItemSummary
|
||||
suspend fun getItemDetails(itemId: String): ItemOut
|
||||
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut
|
||||
@@ -24,6 +24,7 @@ interface ItemRepository {
|
||||
suspend fun getStatistics(): GroupStatistics
|
||||
suspend fun getAllLocations(): List<LocationOutCount>
|
||||
suspend fun getAllLabels(): List<LabelOut>
|
||||
suspend fun createLabel(newLabelData: LabelCreate): LabelSummary
|
||||
suspend fun searchItems(query: String): PaginationResult<ItemSummary>
|
||||
fun getRecentlyAddedItems(limit: Int): kotlinx.coroutines.flow.Flow<List<ItemSummary>>
|
||||
}
|
||||
|
||||
52
tasks/20250813_094500_implement_labels_screen_fixed.xml
Normal file
52
tasks/20250813_094500_implement_labels_screen_fixed.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<!-- 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>
|
||||
211
tasks/completed/003_implement_labels_screen_ui.xml
Normal file
211
tasks/completed/003_implement_labels_screen_ui.xml
Normal file
@@ -0,0 +1,211 @@
|
||||
<!-- tasks/003_implement_labels_screen_ui.xml -->
|
||||
<TASK status="completed">
|
||||
<WORK_ORDER id="task-20250812-115003">
|
||||
<ACTION>MODIFY_CODE</ACTION>
|
||||
|
||||
<TARGET_FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</TARGET_FILE>
|
||||
|
||||
<GOAL>
|
||||
Реализовать UI для экрана "Метки" (LabelsListScreen), заменив заглушку.
|
||||
Экран должен получать данные от LabelsListViewModel, отображать список меток в LazyColumn,
|
||||
а также содержать TopAppBar с кнопкой "назад" и FloatingActionButton для добавления новой метки,
|
||||
в полном соответствии со спецификацией screen_labels_list.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT_FILES>
|
||||
<FILE>tech_spec.txt</FILE>
|
||||
<FILE>project_structure.txt</FILE>
|
||||
<FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt</FILE>
|
||||
</CONTEXT_FILES>
|
||||
|
||||
<CONTRACT>
|
||||
<CONSTRAINTS>
|
||||
<CONSTRAINT>Полностью заменить содержимое файла `LabelsListScreen.kt`.</CONSTRAINT>
|
||||
<CONSTRAINT>Главная функция `LabelsListScreen` должна получать `LabelsListViewModel` через `hiltViewModel()`.</CONSTRAINT>
|
||||
<CONSTRAINT>Состояние UI должно собираться из `viewModel.uiState` с использованием `collectAsStateWithLifecycle`.</CONSTRAINT>
|
||||
<CONSTRAINT>Для отображения списка должен использоваться `LazyColumn`.</CONSTRAINT>
|
||||
<CONSTRAINT>Каждый элемент списка должен быть реализован в отдельном Composable `LabelListItem`.</CONSTRAINT>
|
||||
<CONSTRAINT>`TopAppBar` должен содержать `IconButton` для навигации назад.</CONSTRAINT>
|
||||
<CONSTRAINT>`Scaffold` должен содержать `FloatingActionButton`.</CONSTRAINT>
|
||||
</CONSTRAINTS>
|
||||
</CONTRACT>
|
||||
|
||||
<PAYLOAD mode="FULL_CONTENT">
|
||||
<![CDATA[
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
|
||||
// [PACKAGE]
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.clickable
|
||||
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.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Label
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
|
||||
// [COMPOSABLE_FUNCTION] LabelsListScreen (Stateful)
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Контейнерный Composable для экрана "Метки", управляющий состоянием.
|
||||
* Он получает данные от ViewModel и передает их в state-less Composable `LabelsListContent`.
|
||||
*
|
||||
* @param viewModel ViewModel, предоставляемая Hilt, для доступа к бизнес-логике и состоянию UI.
|
||||
* @param onNavigateBack Лямбда для обработки действия "назад".
|
||||
* @param onLabelClick Лямбда для обработки нажатия на метку, передает ID метки.
|
||||
* @param onAddLabelClick Лямбда для обработки нажатия на FAB.
|
||||
* @sideeffect Получает состояние UI (`uiState`) из `viewModel`.
|
||||
* @sideeffect Вызывает навигационные лямбды в ответ на действия пользователя.
|
||||
*/
|
||||
@Composable
|
||||
fun LabelsListScreen(
|
||||
viewModel: LabelsListViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit,
|
||||
onLabelClick: (String) -> Unit,
|
||||
onAddLabelClick: () -> Unit
|
||||
) {
|
||||
// [ACTION] Сбор состояния из ViewModel
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LabelsListContent(
|
||||
labels = uiState.labels,
|
||||
onNavigateBack = onNavigateBack,
|
||||
onLabelClick = onLabelClick,
|
||||
onAddLabelClick = onAddLabelClick
|
||||
)
|
||||
// [COHERENCE_CHECK_PASSED] Состояние передается в stateless composable.
|
||||
}
|
||||
// [END_FUNCTION]
|
||||
|
||||
// [COMPOSABLE_FUNCTION] LabelsListContent (Stateless)
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Отображает UI для экрана "Метки". Этот Composable не имеет своего состояния (stateless).
|
||||
*
|
||||
* @param labels Список объектов `Label` для отображения.
|
||||
* @param onNavigateBack Лямбда для обработки действия "назад".
|
||||
* @param onLabelClick Лямбда для обработки нажатия на метку.
|
||||
* @param onAddLabelClick Лямбда для обработки нажатия на FAB.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun LabelsListContent(
|
||||
labels: List<Label>,
|
||||
onNavigateBack: () -> Unit,
|
||||
onLabelClick: (String) -> Unit,
|
||||
onAddLabelClick: () -> Unit
|
||||
) {
|
||||
// [CORE-LOGIC] Основная разметка экрана
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Метки") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Назад" // TODO: Заменить на stringResource
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddLabelClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = "Добавить метку" // TODO: Заменить на stringResource
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
// [CORE-LOGIC] Список меток
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
items(items = labels, key = { it.id }) { label ->
|
||||
LabelListItem(
|
||||
label = label,
|
||||
onClick = { onLabelClick(label.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION]
|
||||
|
||||
// [HELPER] LabelListItem
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Отображает один элемент списка для метки.
|
||||
*
|
||||
* @param label Объект `Label`, данные которого нужно отобразить.
|
||||
* @param onClick Лямбда, вызываемая при нажатии на элемент.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelListItem(
|
||||
label: Label,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(label.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Label,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
modifier = modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION]
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun LabelsListContentPreview() {
|
||||
val sampleLabels = listOf(
|
||||
Label(id = "1", name = "Электроника", color = "#FF0000"),
|
||||
Label(id = "2", name = "Книги", color = "#00FF00"),
|
||||
Label(id = "3", name = "Инструменты", color = "#0000FF")
|
||||
)
|
||||
HomeboxLensTheme {
|
||||
LabelsListContent(
|
||||
labels = sampleLabels,
|
||||
onNavigateBack = {},
|
||||
onLabelClick = {},
|
||||
onAddLabelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_PREVIEW]
|
||||
// [END_FILE]
|
||||
]]>
|
||||
</PAYLOAD>
|
||||
|
||||
<IMPLEMENTATION_HINTS>
|
||||
<HINT>Это задание заменяет весь контент файла `app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt`.</HINT>
|
||||
<HINT>Основная логика разделена на два Composable: `LabelsListScreen` (stateful) и `LabelsListContent` (stateless), что является хорошей практикой.</HINT>
|
||||
<HINT>Функция `LabelsListScreen` отвечает за взаимодействие с ViewModel.</HINT>
|
||||
<HINT>Функция `LabelsListContent` отвечает исключительно за отображение UI на основе переданных данных.</HINT>
|
||||
<HINT>Убедись, что все импорты, указанные в секции [IMPORTS], добавлены корректно. Особенно важны `hiltViewModel` и `collectAsStateWithLifecycle`.</HINT>
|
||||
</IMPLEMENTATION_HINTS>
|
||||
</WORK_ORDER>
|
||||
</TASK>
|
||||
207
tasks/completed/004_fix_labels_screen_compilation_errors.xml
Normal file
207
tasks/completed/004_fix_labels_screen_compilation_errors.xml
Normal file
@@ -0,0 +1,207 @@
|
||||
<!-- tasks/004_refactor_labels_screen_with_dbc.xml -->
|
||||
<TASK status="completed">
|
||||
<WORK_ORDER id="task-20250812-121505">
|
||||
<ACTION>MODIFY_CODE</ACTION>
|
||||
|
||||
<TARGET_FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</TARGET_FILE>
|
||||
|
||||
<GOAL>
|
||||
Исправить ошибки компиляции в файле LabelsListScreen.kt и полностью отрефакторить его в соответствии с принципами Design by Contract (DbC) и семантической разметки. Код должен быть не только рабочим, но и формально корректным и легко читаемым для AI.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT_FILES>
|
||||
<FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</FILE>
|
||||
<FILE>domain/src/main/java/com/homebox/lens/domain/model/Label.kt</FILE>
|
||||
</CONTEXT_FILES>
|
||||
|
||||
<CONTRACT>
|
||||
<CONSTRAINTS>
|
||||
<CONSTRAINT>Код должен успешно компилироваться.</CONSTRAINT>
|
||||
<CONSTRAINT>Обязательно добавить импорты для `com.homebox.lens.domain.model.Label` и `androidx.lifecycle.compose.collectAsStateWithLifecycle`.</CONSTRAINT>
|
||||
<CONSTRAINT>Каждая Composable-функция должна иметь исчерпывающий KDoc-комментарий с тегом `[CONTRACT]`.</CONSTRAINT>
|
||||
<CONSTRAINT>В коде должны использоваться семантические якоря ([ACTION], [CORE-LOGIC], [HELPER], [PREVIEW] и т.д.) для структурирования.</CONSTRAINT>
|
||||
<CONSTRAINT>В коде Preview-функции должна быть устранена ошибка создания `Label` и добавлен `[COHERENCE_NOTE]` с объяснением исправления.</CONSTRAINT>
|
||||
</CONSTRAINTS>
|
||||
</CONTRACT>
|
||||
|
||||
<PAYLOAD mode="FULL_CONTENT">
|
||||
<![CDATA[
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
|
||||
// [PACKAGE]
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.clickable
|
||||
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.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Label
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
|
||||
// [COMPOSABLE_FUNCTION] LabelsListScreen (Stateful)
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Контейнерный Composable для экрана "Метки", управляющий состоянием (stateful).
|
||||
* Его единственная ответственность — получение состояния от ViewModel и передача его в презентационный компонент.
|
||||
*
|
||||
* @param viewModel ViewModel, предоставляемая Hilt, для доступа к бизнес-логике и состоянию UI.
|
||||
* @param onNavigateBack Лямбда для обработки действия "назад".
|
||||
* @param onLabelClick Лямбда для обработки нажатия на метку, передает ID метки.
|
||||
* @param onAddLabelClick Лямбда для обработки нажатия на FloatingActionButton.
|
||||
* @sideeffect Получает `uiState` из `viewModel` и подписывается на его обновления.
|
||||
* @sideeffect Вызывает навигационные лямбды (`onNavigateBack`, `onLabelClick`) в ответ на действия пользователя.
|
||||
*/
|
||||
@Composable
|
||||
fun LabelsListScreen(
|
||||
viewModel: LabelsListViewModel = hiltViewModel(),
|
||||
onNavigateBack: () -> Unit,
|
||||
onLabelClick: (String) -> Unit,
|
||||
onAddLabelClick: () -> Unit
|
||||
) {
|
||||
// [ACTION] Сбор актуального состояния из ViewModel.
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
// [ACTION] Делегирование отрисовки компоненту без состояния.
|
||||
LabelsListContent(
|
||||
labels = uiState.labels,
|
||||
onNavigateBack = onNavigateBack,
|
||||
onLabelClick = onLabelClick,
|
||||
onAddLabelClick = onAddLabelClick
|
||||
)
|
||||
// [COHERENCE_CHECK_PASSED] Разделение ответственности между stateful и stateless компонентами соблюдено.
|
||||
}
|
||||
// [END_FUNCTION]
|
||||
|
||||
// [COMPOSABLE_FUNCTION] LabelsListContent (Stateless)
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Презентационный Composable (stateless), отвечающий исключительно за отображение UI экрана "Метки".
|
||||
* Он не содержит бизнес-логики и полностью управляется извне через параметры.
|
||||
*
|
||||
* @param labels Список объектов `Label` для отображения.
|
||||
* @param onNavigateBack Лямбда для обработки действия "назад".
|
||||
* @param onLabelClick Лямбда для обработки нажатия на метку.
|
||||
* @param onAddLabelClick Лямбда для обработки нажатия на FAB.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun LabelsListContent(
|
||||
labels: List<Label>,
|
||||
onNavigateBack: () -> Unit,
|
||||
onLabelClick: (String) -> Unit,
|
||||
onAddLabelClick: () -> Unit
|
||||
) {
|
||||
// [CORE-LOGIC] Основная разметка экрана, определенная в UI_SPECIFICATIONS.
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Метки") }, // TODO: Заменить на stringResource(R.string.labels_screen_title)
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = "Назад" // TODO: Заменить на stringResource
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddLabelClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = "Добавить метку" // TODO: Заменить на stringResource
|
||||
)
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
// [CORE-LOGIC] Отображение списка меток.
|
||||
LazyColumn(modifier = Modifier.padding(innerPadding)) {
|
||||
items(items = labels, key = { it.id }) { label ->
|
||||
LabelListItem(
|
||||
label = label,
|
||||
onClick = { onLabelClick(label.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION]
|
||||
|
||||
// [HELPER] LabelListItem
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Вспомогательный Composable для отображения одного элемента в списке меток.
|
||||
*
|
||||
* @param label Объект `Label`, данные которого нужно отобразить.
|
||||
* @param onClick Лямбда, вызываемая при нажатии на элемент.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelListItem(
|
||||
label: Label,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ListItem(
|
||||
headlineContent = { Text(label.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Label,
|
||||
contentDescription = null // Декоративная иконка
|
||||
)
|
||||
},
|
||||
modifier = modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION]
|
||||
|
||||
// [PREVIEW]
|
||||
@Preview(showBackground = true, name = "Экран меток")
|
||||
@Composable
|
||||
private fun LabelsListContentPreview() {
|
||||
// [COHERENCE_NOTE] Исправлено создание тестовых данных. Поле 'color' отсутствует в реальной
|
||||
// доменной модели 'Label.kt', поэтому оно было убрано из preview для устранения ошибки компиляции.
|
||||
val sampleLabels = listOf(
|
||||
Label(id = "1", name = "Электроника"),
|
||||
Label(id = "2", name = "Книги"),
|
||||
Label(id = "3", name = "Инструменты")
|
||||
)
|
||||
HomeboxLensTheme {
|
||||
LabelsListContent(
|
||||
labels = sampleLabels,
|
||||
onNavigateBack = {},
|
||||
onLabelClick = {},
|
||||
onAddLabelClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_PREVIEW]
|
||||
// [END_FILE]
|
||||
]]>
|
||||
</PAYLOAD>
|
||||
|
||||
<IMPLEMENTATION_HINTS>
|
||||
<HINT>Это задание полностью заменяет содержимое файла, исправляя ошибки и приводя код в соответствие с архитектурными принципами проекта.</HINT>
|
||||
<HINT>Ключевое изменение — добавление исчерпывающих KDoc-комментариев с блоками [CONTRACT] для каждой функции.</HINT>
|
||||
<HINT>Убедись, что все импорты, включая `com.homebox.lens.domain.model.Label`, на месте.</HINT>
|
||||
</IMPLEMENTATION_HINTS>
|
||||
</WORK_ORDER>
|
||||
</TASK>
|
||||
86
tasks/completed/005_add_iconography_to_spec.xml
Normal file
86
tasks/completed/005_add_iconography_to_spec.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<!-- tasks/005_add_iconography_to_spec.xml -->
|
||||
<TASK status="completed">
|
||||
<WORK_ORDER id="task-20250812-121002">
|
||||
<ACTION>MODIFY_SPECIFICATION</ACTION>
|
||||
|
||||
<TARGET_FILE>tech_spec.txt</TARGET_FILE>
|
||||
|
||||
<GOAL>
|
||||
Добавить в техническую спецификацию новый раздел ICONOGRAPHY_GUIDE, содержащий список
|
||||
рекомендованных к использованию иконок из 'androidx.compose.material.icons.Icons'.
|
||||
Это создаст единый стандарт для иконок в приложении.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT_FILES>
|
||||
<FILE>tech_spec.txt</FILE>
|
||||
</CONTEXT_FILES>
|
||||
|
||||
<PAYLOAD mode="UPSERT_NODE" target_node_id="iconography_guide">
|
||||
<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>
|
||||
</PAYLOAD>
|
||||
|
||||
<IMPLEMENTATION_HINTS>
|
||||
<HINT>Найди корневой узел PROJECT_SPECIFICATION в tech_spec.txt.</HINT>
|
||||
<HINT>Добавь новый узел ICONOGRAPHY_GUIDE в конец, после UI_SPECIFICATIONS, но перед IMPLEMENTATION_MAP.</HINT>
|
||||
<HINT>Я уже обработал устаревшие иконки и указал правильные AutoMirrored версии, просто вставь этот блок.</HINT>
|
||||
</IMPLEMENTATION_HINTS>
|
||||
</WORK_ORDER>
|
||||
</TASK>```
|
||||
|
||||
Пожалуйста, выполните эти задания последовательно, начиная с исправления ошибки. Жду вашего сигнала о результатах.
|
||||
59
tasks/completed/01_update_label_screen_spec_status.xml
Normal file
59
tasks/completed/01_update_label_screen_spec_status.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<!-- tasks/001_update_label_screen_spec_status.xml -->
|
||||
<TASK status="completed">
|
||||
<WORK_ORDER id="task-20250812-114001">
|
||||
<ACTION>MODIFY_SPECIFICATION</ACTION>
|
||||
|
||||
<TARGET_FILE>tech_spec.txt</TARGET_FILE>
|
||||
<GOAL>
|
||||
Изменить статус UI-экрана 'screen_labels_list' на 'in_progress', чтобы отразить начало работ по его реализации.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT_FILES>
|
||||
<FILE>tech_spec.txt</FILE>
|
||||
</CONTEXT_FILES>
|
||||
|
||||
<PAYLOAD mode="UPSERT_NODE" target_node_id="screen_labels_list">
|
||||
<SCREEN id="screen_labels_list" status="in_progress">
|
||||
<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>
|
||||
</PAYLOAD>
|
||||
|
||||
<IMPLEMENTATION_HINTS>
|
||||
<HINT>Найди узел SCREEN с id="screen_labels_list" в файле tech_spec.txt.</HINT>
|
||||
<HINT>Замени атрибут status="implemented" на status="in_progress".</HINT>
|
||||
<HINT>Не изменяй остальное содержимое узла. Просто обнови атрибут.</HINT>
|
||||
</IMPLEMENTATION_HINTS>
|
||||
</WORK_ORDER>
|
||||
</TASK>
|
||||
92
tasks/completed/02_create_labels_screen_file.xml
Normal file
92
tasks/completed/02_create_labels_screen_file.xml
Normal file
@@ -0,0 +1,92 @@
|
||||
<!-- tasks/02_create_labels_screen_file.xml -->
|
||||
<TASK status="completed">
|
||||
<WORK_ORDER id="task-20250812-114502">
|
||||
<ACTION>CREATE_FILE</ACTION>
|
||||
|
||||
<TARGET_FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</TARGET_FILE>
|
||||
|
||||
<GOAL>
|
||||
Создать базовую структуру (stub) для экрана "Метки" (LabelsListScreen) с использованием Jetpack Compose.
|
||||
Этот файл будет служить основой для дальнейшей реализации полноценного UI.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT_FILES>
|
||||
<FILE>tech_spec.txt</FILE>
|
||||
</CONTEXT_FILES>
|
||||
|
||||
<CONTRACT>
|
||||
<CONSTRAINTS>
|
||||
<CONSTRAINT>Имя файла должно быть 'LabelsListScreen.kt'.</CONSTRAINT>
|
||||
<CONSTRAINT>Функция должна называться 'LabelsListScreen'.</CONSTRAINT>
|
||||
<CONSTRAINT>Функция должна быть аннотирована как @Composable.</CONSTRAINT>
|
||||
<CONSTRAINT>Основная разметка должна использовать Scaffold.</CONSTRAINT>
|
||||
<CONSTRAINT>Должен быть TopAppBar с заголовком "Метки".</CONSTRAINT>
|
||||
<CONSTRAINT>В качестве временного контента для Scaffold должен использоваться Text-компонент с текстом "Hello, Labels Screen!".</CONSTRAINT>
|
||||
</CONSTRAINTS>
|
||||
</CONTRACT>
|
||||
|
||||
<PAYLOAD mode="FULL_CONTENT">
|
||||
<![CDATA[
|
||||
[FILE:app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt]
|
||||
[PACKAGE]
|
||||
package com.homebox.lens.ui.screen.labelslist
|
||||
[/PACKAGE]
|
||||
|
||||
[IMPORTS]
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
[/IMPORTS]
|
||||
|
||||
[COMPOSABLE_FUNCTION]
|
||||
/**
|
||||
* Заглушка для экрана, отображающего список меток.
|
||||
* В соответствии со спецификацией 'screen_labels_list'.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabelsListScreen(
|
||||
// В будущем здесь будут параметры: navController для навигации, viewModel для получения данных.
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = "Метки") // В будущем будет заменено на stringResource
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
// Временный контент-заглушка.
|
||||
// В будущем здесь будет LazyColumn для отображения списка меток.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Hello, Labels Screen!")
|
||||
}
|
||||
}
|
||||
}
|
||||
[/COMPOSABLE_FUNCTION]
|
||||
[END_FILE]
|
||||
]]>
|
||||
</PAYLOAD>
|
||||
|
||||
<IMPLEMENTATION_HINTS>
|
||||
<HINT>Создай новый файл по пути 'app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt'.</HINT>
|
||||
<HINT>Скопируй предоставленный код из секции PAYLOAD в этот файл.</HINT>
|
||||
<HINT>Убедись, что используется правильный package: com.homebox.lens.ui.screen.labelslist.</HINT>
|
||||
<HINT>Добавь все необходимые импорты для Jetpack Compose (Scaffold, TopAppBar, Text, Composable и т.д.), как указано в PAYLOAD.</HINT>
|
||||
<HINT>Следуй структуре, заданной семантическими якорями.</HINT>
|
||||
</IMPLEMENTATION_HINTS>
|
||||
</WORK_ORDER>
|
||||
</TASK>
|
||||
237
tasks/completed/20250813_080300_implement_labels_screen.xml
Normal file
237
tasks/completed/20250813_080300_implement_labels_screen.xml
Normal file
@@ -0,0 +1,237 @@
|
||||
<!-- tasks/20250813_080300_implement_labels_screen.xml -->
|
||||
<TASK status="completed">
|
||||
<WORK_ORDER id="task-20250813080300-001">
|
||||
<ACTION>MODIFY_CODE</ACTION>
|
||||
|
||||
<TARGET_FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt</TARGET_FILE>
|
||||
|
||||
<GOAL>
|
||||
Реализовать UI для экрана "Метки" (`LabelsListScreen`) в соответствии со спецификацией `screen_labels_list`.
|
||||
Это включает в себя создание Composable-функции, которая:
|
||||
1. Использует `Scaffold` с `TopAppBar` и `FloatingActionButton`.
|
||||
2. Получает состояние (список меток, статус загрузки, ошибки) от `LabelsListViewModel`.
|
||||
3. Отображает список меток с помощью `LazyColumn`.
|
||||
4. Обрабатывает клики по элементам списка для навигации на экран инвентаря с фильтром по метке.
|
||||
5. Обрабатывает нажатие на FAB для создания новой метки (пока что через лог).
|
||||
6. Отображает индикатор загрузки и сообщения об ошибках или пустом списке.
|
||||
7. Строго следует принципам Design by Contract, использует иконки из гайда и строки из ресурсов.
|
||||
</GOAL>
|
||||
|
||||
<CONTEXT_FILES>
|
||||
<FILE>PROJECT_SPECIFICATION.xml</FILE>
|
||||
<FILE>PROJECT_STRUCTURE.xml</FILE>
|
||||
<FILE>app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt</FILE>
|
||||
<FILE>domain/src/main/java/com/homebox/lens/domain/model/Label.kt</FILE>
|
||||
<FILE>app/src/main/java/com/homebox/lens/navigation/Screen.kt</FILE>
|
||||
</CONTEXT_FILES>
|
||||
|
||||
<CONTRACT>
|
||||
<![CDATA[
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.automirrored.filled.Label
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import timber.log.Timber
|
||||
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Отображает экран со списком всех меток.
|
||||
*
|
||||
* @param navController Контроллер навигации для перемещения между экранами.
|
||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||
*
|
||||
* @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события.
|
||||
* @precondition `viewModel` должен быть доступен через Hilt.
|
||||
* @postcondition Экран отображает список меток или соответствующее состояние (загрузка, ошибка, пустой список).
|
||||
* @sideeffect Пользовательские действия (клики) инициируют навигационные команды через `navController` или логируются.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun LabelsListScreen(
|
||||
navController: NavController,
|
||||
viewModel: LabelsListViewModel = hiltViewModel()
|
||||
) {
|
||||
// [ACTION]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val logger = Timber.tag("LabelsListScreen")
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
logger.info { "[ACTION] Navigate up initiated." }
|
||||
navController.navigateUp()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
// [ACTION]
|
||||
// TODO: Открыть диалог или экран создания метки
|
||||
logger.info { "[ACTION] FAB clicked: Initiate create new label flow." }
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = stringResource(id = R.string.content_desc_create_label)
|
||||
)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
// [CORE-LOGIC]
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
// [STATE_BRANCH] Loading
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
uiState.error != null -> {
|
||||
// [STATE_BRANCH] Error
|
||||
Text(text = uiState.error ?: stringResource(id = R.string.error_unknown))
|
||||
}
|
||||
uiState.labels.isEmpty() -> {
|
||||
// [STATE_BRANCH] Empty
|
||||
Text(text = stringResource(id = R.string.labels_list_empty))
|
||||
}
|
||||
else -> {
|
||||
// [STATE_BRANCH] Success
|
||||
LabelsList(
|
||||
labels = uiState.labels,
|
||||
onLabelClick = { label ->
|
||||
// [ACTION]
|
||||
// TODO: Реализовать навигацию на экран инвентаря с фильтром
|
||||
logger.info { "[ACTION] Label clicked: ${label.id}. Navigating to inventory list." }
|
||||
// navController.navigate(Screen.InventoryList.withFilter("label", label.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [COHERENCE_CHECK_PASSED]
|
||||
}
|
||||
// [END_FUNCTION] LabelsListScreen
|
||||
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Composable-функция для отображения списка меток.
|
||||
*
|
||||
* @param labels Список объектов `Label` для отображения.
|
||||
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
||||
*
|
||||
* @precondition `labels` не должен быть null.
|
||||
* @postcondition Отображается вертикальный прокручиваемый список.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelsList(
|
||||
labels: List<Label>,
|
||||
onLabelClick: (Label) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(labels) { label ->
|
||||
LabelListItem(
|
||||
label = label,
|
||||
onClick = { onLabelClick(label) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION] LabelsList
|
||||
|
||||
// [HELPER]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Composable-функция для отображения одного элемента в списке меток.
|
||||
*
|
||||
* @param label Объект `Label`, который нужно отобразить.
|
||||
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
||||
*
|
||||
* @precondition `label` не должен быть null.
|
||||
* @postcondition Отображается кликабельный элемент списка с иконкой и названием метки.
|
||||
*/
|
||||
@Composable
|
||||
private fun LabelListItem(
|
||||
label: Label,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
ListItem(
|
||||
headlineContent = { Text(text = label.name) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.Label,
|
||||
contentDescription = stringResource(id = R.string.content_desc_label_icon)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION] LabelListItem
|
||||
|
||||
// [END_FILE] LabelsListScreen.kt
|
||||
]]>
|
||||
</CONTRACT>
|
||||
|
||||
<IMPLEMENTATION_HINTS>
|
||||
<HINT>Используйте `@HiltViewModel` для получения экземпляра `LabelsListViewModel`.</HINT>
|
||||
<HINT>Собирайте `uiState` из ViewModel с помощью `collectAsState()` для автоматического обновления UI при изменении состояния.</HINT>
|
||||
<HINT>Используйте `Scaffold` для базовой структуры экрана (TopAppBar, FAB, основное содержимое).</HINT>
|
||||
<HINT>Для навигации назад используйте `navController.navigateUp()`.</HINT>
|
||||
<HINT>Используйте `LazyColumn` для эффективного отображения потенциально длинных списков меток.</HINT>
|
||||
<HINT>Обязательно добавьте новые строковые ресурсы (`screen_title_labels`, `content_desc_navigate_back`, `content_desc_create_label`, `labels_list_empty`, `content_desc_label_icon`) в `strings.xml`.</HINT>
|
||||
<HINT>Иконки должны браться из `androidx.compose.material.icons` в соответствии с `ICONOGRAPHY_GUIDE`.</HINT>
|
||||
</IMPLEMENTATION_HINTS>
|
||||
</WORK_ORDER>
|
||||
</TASK>
|
||||
@@ -9,6 +9,30 @@
|
||||
<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>
|
||||
@@ -312,7 +336,7 @@
|
||||
</USER_INTERACTIONS>
|
||||
</SCREEN>
|
||||
|
||||
<SCREEN id="screen_labels_list" status="implemented">
|
||||
<SCREEN id="screen_labels_list" status="in_progress">
|
||||
<summary>Экран "Метки"</summary>
|
||||
<description>
|
||||
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
|
||||
@@ -475,6 +499,64 @@
|
||||
|
||||
</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" />
|
||||
|
||||
Reference in New Issue
Block a user