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