This commit is contained in:
2025-08-14 15:34:05 +03:00
parent ecf614e4c2
commit 7816bb3464
27 changed files with 1795 additions and 335 deletions

251
GEMINI.md
View File

@@ -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>

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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()

View File

@@ -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]

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -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]

View File

@@ -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>

View File

@@ -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]

View File

@@ -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?
)

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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>>
}

View 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>

View 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>

View 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>

View 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>```
Пожалуйста, выполните эти задания последовательно, начиная с исправления ошибки. Жду вашего сигнала о результатах.

View 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>

View 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>

View 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>

View File

@@ -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" />