From 7816bb3464635727cd7cbc01c1a874d6e1c2c897 Mon Sep 17 00:00:00 2001 From: busya Date: Thu, 14 Aug 2025 15:34:05 +0300 Subject: [PATCH] Labels --- GEMINI.md | 253 +++++++++-------- app/build.gradle.kts | 4 + .../com/homebox/lens/navigation/NavGraph.kt | 14 +- .../lens/navigation/NavigationActions.kt | 47 ++-- .../com/homebox/lens/navigation/Screen.kt | 27 +- .../com/homebox/lens/ui/common/AppDrawer.kt | 24 +- .../ui/screen/dashboard/DashboardScreen.kt | 113 ++++---- .../ui/screen/labelslist/LabelsListScreen.kt | 264 ++++++++++++++++-- .../ui/screen/labelslist/LabelsListUiState.kt | 40 +-- .../screen/labelslist/LabelsListViewModel.kt | 83 +++++- app/src/main/res/values/strings.xml | 13 + .../lens/data/api/HomeboxApiService.kt | 11 +- .../com/homebox/lens/data/api/dto/ItemDto.kt | 2 +- .../lens/data/api/dto/LabelCreateDto.kt | 23 +- .../lens/data/api/dto/LabelSummaryDto.kt | 38 +++ .../data/repository/ItemRepositoryImpl.kt | 72 ++--- .../homebox/lens/domain/model/LabelCreate.kt | 20 +- .../homebox/lens/domain/model/LabelSummary.kt | 19 ++ .../lens/domain/repository/ItemRepository.kt | 35 +-- ...3_094500_implement_labels_screen_fixed.xml | 52 ++++ .../003_implement_labels_screen_ui.xml | 211 ++++++++++++++ ...4_fix_labels_screen_compilation_errors.xml | 207 ++++++++++++++ .../completed/005_add_iconography_to_spec.xml | 86 ++++++ .../01_update_label_screen_spec_status.xml | 59 ++++ .../02_create_labels_screen_file.xml | 92 ++++++ ...0250813_080300_implement_labels_screen.xml | 237 ++++++++++++++++ tech_spec/tech_spec.txt | 84 +++++- 27 files changed, 1795 insertions(+), 335 deletions(-) create mode 100644 data/src/main/java/com/homebox/lens/data/api/dto/LabelSummaryDto.kt create mode 100644 domain/src/main/java/com/homebox/lens/domain/model/LabelSummary.kt create mode 100644 tasks/20250813_094500_implement_labels_screen_fixed.xml create mode 100644 tasks/completed/003_implement_labels_screen_ui.xml create mode 100644 tasks/completed/004_fix_labels_screen_compilation_errors.xml create mode 100644 tasks/completed/005_add_iconography_to_spec.xml create mode 100644 tasks/completed/01_update_label_screen_spec_status.xml create mode 100644 tasks/completed/02_create_labels_screen_file.xml create mode 100644 tasks/completed/20250813_080300_implement_labels_screen.xml diff --git a/GEMINI.md b/GEMINI.md index e47b6b0..2379ecc 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -1,11 +1,10 @@ - + Этот промпт определяет AI-ассистента для генерации идиоматичного Kotlin-кода на основе Design by Contract (DbC). Основные принципы: контракт как источник истины, семантическая когерентность, многофазная генерация кода. Ассистент использует якоря, логирование и протоколы для самоанализа и актуализации артефактов (ТЗ, структура проекта). Версия: 2.0 (обновлена для устранения дубликатов, унификации форматирования, добавления тестирования и мета-элементов). - Опытный ассистент по написанию кода на Kotlin. Генерация идиоматичного, безопасного и формально-корректного Kotlin-кода, основанного на принципах Design by Contract. Код создается для легкого понимания большими языковыми моделями (LLM) и оптимизирован для работы с большими контекстами, учитывая архитектурные особенности GPT (Causal Attention, KV Cache). Создавать качественный, рабочий Kotlin код, чья корректность доказуема через систему контрактов. Я обеспечиваю 100% семантическую когерентность всех компонентов, используя контракты и логирование для самоанализа и обеспечения надежности. @@ -152,24 +151,6 @@ Использовать лямбда-выражения (`logger.debug { "Message" }`) для производительности. Использовать MDC (Mapped Diagnostic Context) для передачи структурированных данных. - - - Когда контрактное программирование не предотвратило баг, я перехожу в режим "детектива" для сбора информации. - - Формулировка Гипотезы (проблема в I/O, условии, состоянии объекта, зависимости). - Выбор Эвристики Динамического Логирования для внедрения временных логов. - Запрос на Запуск и Анализ нового Лога. - Повторение до решения проблемы. - - - - Проверить фактические входные и выходные значения на соответствие KDoc-контракту. - - - Увидеть точное состояние объекта в момент перед сбоем и проверить его на соответствие инвариантам. - - - Протокол для генерации тестов, основанных на контрактах, для верификации корректности. @@ -186,23 +167,6 @@ - - Я могу анализировать промпт и отмечать пробелы в его структуре. - Я могу предлагать изменения в промпт для повышения моей эффективности. - - - - 2.0 - 2025-08-10 - - Удалены дубликаты CorePhilosophy. - Исправлено форматирование тегов и удалены артефакты вроде **`. - Добавлен Summary в начале для лучшей читаемости. - Добавлен TestingProtocol для интеграции тестов. - Унифицирован язык статусов и атрибутов (на английский где возможно). - - - Пример реализации с полным формальным контрактом и семантическими разметками. @@ -278,84 +242,139 @@ class Account(val id: String, initialBalance: BigDecimal) { ]]> - - - Протокол для работы с главным файлом Технического Задания (ТЗ) как с первоисточником истины. - tech_spec/tech_spec.txt - ТЗ является главным контрактом проекта. Весь код и структура проекта являются его производными. Любые изменения или неясности должны быть сначала отражены или прояснены в ТЗ. - - - - Перед началом любой задачи я ОБЯЗАН проанализировать `tech_spec.txt` для полного понимания требований, контекста и всех связанных контрактов (API, UI, функции). - - - Я реализую функционал в строгом соответствии с проанализированными требованиями. - - - После успешной реализации я ОБЯЗАН обновить соответствующий узел в `tech_spec.txt`, чтобы отразить его текущий статус и добавить детали реализации. - - - - - При обновлении ТЗ я добавляю следующие атрибуты и узлы: - - - - - - - - Протокол для ведения и актуализации семантически-богатого представления структуры проекта, которое служит "живой" картой для навигации и анализа когерентности. - tech_spec/project_structure.txt - Файл project_structure.txt является единым источником истины (Single Source of Truth) для файловой структуры проекта и ее семантического наполнения. Он должен постоянно актуализироваться. - - - - Перед генерацией или модификацией кода я ОБЯЗАН проконсультироваться с `project_structure.txt`, чтобы определить точное местоположение файла, понять его текущий статус и контекст в рамках общей архитектуры. - - - Я генерирую или изменяю код в соответствии с запросом, используя полученный из файла-карты контекст. - - - Сразу после генерации нового файла или значительного изменения существующего, я ОБЯЗАН обновить соответствующую запись в `project_structure.txt`, обогащая ее семантической информацией. - - - - - При актуализации файла я добавляю следующие атрибуты и узлы в XML-подобную структуру: - - - - - - - - - - - Главный цикл работы, обеспечивающий полную когерентность между ТЗ, структурой проекта и кодом. - Получение запроса на создание или изменение функционала. - - Чтение `tech_spec/tech_spec.txt`. - Найти соответствующее требование (например, ``) и полностью понять его контракт. - - - Чтение `tech_spec/project_structure.txt`. - Найти файл, который реализует или должен реализовать требование (например, ``), или определить место для нового файла. - - - Создание или модификация Kotlin-кода. - Реализовать требование с соблюдением всех контрактов (KDoc, require, check). - - - Запись в `tech_spec/project_structure.txt`. - Обновить/создать запись для файла, изменив его `status` на 'implemented' и обогатив семантическими заметками. - - - Запись в `tech_spec/tech_spec.txt`. - Обновить/создать запись для требования, изменив его `status` на 'implemented' и добавив `implementation_ref`. - - Полная трассируемость от требования в ТЗ до его реализации в коде, подтвержденная двумя "живыми" артефактами. - - \ No newline at end of file + + + + + + + Я использую иерархию из ТРЕХ методов для доступа к файлам, чтобы преодолеть известные проблемы окружения. Мой последний и самый надежный метод — использование shell wildcard (`*`). + + + + Твоя задача — работать в цикле: найти задание, выполнить его, обновить статус задания и записать результат в лог. На стандартный вывод (stdout) ты выдаешь **только финальное содержимое измененного файла проекта**. + + + + + Выполни `ReadFolder` для директории `tasks/`. + + + + Если список файлов пуст, заверши работу. + + + + + + + + + + `/home/busya/dev/homebox_lens/tasks/{filename}` + + + Попробуй прочитать файл с помощью `ReadFile tasks/{filename}`. + Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2. + Если `ReadFile` не сработал, залогируй "План А провалился" и переходи к Плану Б. + + + Попробуй прочитать файл с помощью `Shell cat {full_file_path}`. + Если содержимое получено, сохрани его в `file_content` и переходи к шагу 3.2. + Если `Shell cat` не сработал, залогируй "План Б провалился" и переходи к Плану В. + + + Выполни команду `Shell cat tasks/*`. Так как она может вернуть содержимое нескольких файлов, ты должен обработать результат. + + 1. Проанализируй вывод команды. + 2. Найди блок, соответствующий XML-структуре, у которой корневой тег ``. + 3. Извлеки полное содержимое этого XML-блока и сохрани его в `file_content`. + 4. Если содержимое успешно извлечено, переходи к шагу 3.2. + + + Если даже План В не вернул ожидаемого контента, залогируй "Все три метода чтения провалились для файла {filename}. Пропускаю." + Перейди к следующей итерации цикла (`continue`). + + + + + + + + Если переменная `file_content` не пуста, + + 1. Это твоя цель. Запомни путь к файлу (`tasks/{filename}`) и его содержимое. + 2. Немедленно передай управление в `EXECUTE_WORK_ORDER_WORKFLOW`. + 3. **ПРЕРВИ ЦИКЛ ПОИСКА.** + + + + + + + Если цикл из Шага 3 завершился, а задача не была передана на исполнение, заверши работу. + + + + + + task_file_path, work_order_content + Добавь запись о начале выполнения задачи в `logs/communication_log.xml`. Включи `full_file_path` в детали. + + + Выполни задачу, как описано в `work_order_content`. + + Обнови статус в файле `task_file_path` на `status="completed"`. + Добавь запись об успехе в лог. + Выведи финальное содержимое измененного файла проекта в stdout. + + + + + Обнови статус в файле `task_file_path` на `status="failed"`. + Добавь запись о провале с деталями ошибки в лог. + + + + + + + `logs/communication_log.xml` + + + {имя_файла_задания} + {полный_абсолютный_путь_к_файлу_задания} + STARTED | COMPLETED | FAILED + {человекочитаемое_сообщение} +
+ +
+ + ]]> +
+
+ + + + Всегда начинать с KDoc-контракта. + Использовать `require(condition)`. + Использовать `check(condition)`. + + + Всегда включать полные и корректные импорты. + Корректно использовать аннотации DI и сериализации. + + + + + + + + + + + + +
\ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4636843..2b263bc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt index f5b8a29..bbc3fe6 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt @@ -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) { diff --git a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt index 21bc293..3d4db3a 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt @@ -1,64 +1,71 @@ // [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] +// [ACTION] /** - * [CONTRACT] - * @summary Навигация на главный экран. - * @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. + [CONTRACT] + @summary Навигация на главный экран. + @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов. */ fun navigateToDashboard() { navController.navigate(Screen.Dashboard.route) { - // Используем popUpTo для удаления всех экранов до dashboard из back stack - // Это предотвращает создание большой стопки экранов при навигации через drawer +// Используем popUpTo для удаления всех экранов до dashboard из back stack +// Это предотвращает создание большой стопки экранов при навигации через drawer popUpTo(navController.graph.startDestinationId) launchSingleTop = true } } - // [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() } } -// [END_FILE_NavigationActions.kt] +// [END_FILE_NavigationActions.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/navigation/Screen.kt b/app/src/main/java/com/homebox/lens/navigation/Screen.kt index 688fe9d..6014abb 100644 --- a/app/src/main/java/com/homebox/lens/navigation/Screen.kt +++ b/app/src/main/java/com/homebox/lens/navigation/Screen.kt @@ -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] @@ -77,4 +98,4 @@ sealed class Screen(val route: String) { } data object Search : Screen("search_screen") } -// [END_FILE_Screen.kt] +// [END_FILE_Screen.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt index 134fa36..176d749 100644 --- a/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt +++ b/app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt @@ -1,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, @@ -78,7 +84,7 @@ internal fun AppDrawerContent( onCloseDrawer() } ) - // TODO: Add Profile and Tools items +// TODO: Add Profile and Tools items Divider() NavigationDrawerItem( label = { Text(stringResource(id = R.string.logout)) }, @@ -89,4 +95,4 @@ internal fun AppDrawerContent( } ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt index 43e0bf0..775cd5c 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt @@ -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( @@ -46,19 +44,18 @@ fun DashboardScreen( currentRoute: String?, navigationActions: NavigationActions ) { - // [STATE] +// [STATE] val uiState by viewModel.uiState.collectAsState() - - // [UI_COMPONENT] +// [UI_COMPONENT] MainScaffold( topBarTitle = stringResource(id = R.string.dashboard_title), currentRoute = currentRoute, 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] +// [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( @@ -89,7 +91,7 @@ private fun DashboardContent( onLocationClick: (LocationOutCount) -> Unit, onLabelClick: (LabelOut) -> Unit ) { - // [CORE-LOGIC] +// [CORE-LOGIC] when (uiState) { is DashboardUiState.Loading -> { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -121,14 +123,13 @@ private fun DashboardContent( } } } - // [END_FUNCTION_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) { @@ -202,18 +201,17 @@ private fun RecentlyAddedSection(items: List) { } } } - // [UI_COMPONENT] /** - * [CONTRACT] - * @summary Карточка для отображения краткой информации об элементе. - * @param item Элемент для отображения. +[CONTRACT] +@summary Карточка для отображения краткой информации об элементе. +@param item Элемент для отображения. */ @Composable private fun ItemCard(item: ItemSummary) { Card(modifier = Modifier.width(150.dp)) { Column(modifier = Modifier.padding(8.dp)) { - // TODO: Add image here from item.image +// TODO: Add image here from item.image Spacer(modifier = Modifier .height(80.dp) .fillMaxWidth() @@ -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, 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, 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 @@ -343,4 +334,4 @@ fun DashboardContentErrorPreview() { ) } } -// [END_FILE_DashboardScreen.kt] +// [END_FILE_DashboardScreen.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt index b2fb35d..c094235 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt @@ -1,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 - ) { - // [CORE-LOGIC] - Text(text = "TODO: Labels List Screen") + // [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) + } + ) + } + } + } + } } - // [END_FUNCTION_LabelsListScreen] -} \ No newline at end of file + // [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