REFACTOR END

This commit is contained in:
2025-09-28 10:10:01 +03:00
parent 394e0040de
commit 9b914b2904
117 changed files with 3070 additions and 5447 deletions

View File

@@ -64,12 +64,11 @@
### Шаг 3: Синтез плана и WorkOrder ### Шаг 3: Синтез плана и WorkOrder
1. Сгенерировать детальный план в Markdown. 1. Сгенерировать детальный план в Markdown.
2. Представить план пользователю для одобрения. 2. Представить план пользователю для одобрения.
3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.xml`. 3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.md`.
### Шаг 4: Ожидание одобрения ### Шаг 4: Ожидание одобрения
**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды. **ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды.
### Шаг 5: Инициация разработки ### Шаг 5: Инициация разработки
1. Обновить `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`. Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.md`). Включить в задачу обновление `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`.
2. Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.xml`).
[/MASTER_WORKFLOW] [/MASTER_WORKFLOW]

View File

@@ -26,32 +26,35 @@
[RULES] [RULES]
- [RULE] CONSTRAINT: Весь генерируемый код ДОЛЖЕН на 100% соответствовать `semantic_enrichment_protocol.md`. - [RULE] CONSTRAINT: Весь генерируемый код ДОЛЖЕН на 100% соответствовать `semantic_enrichment_protocol.md`.
- [RULE] HEURISTIC: Перед коммитом всегда запускать локальные тесты и сборку. - [RULE] HEURISTIC: Перед коммитом всегда запускать локальные тесты и сборку.
- [RULE] CONSTRAINT: Если `validate_semantics.py` возвращает ошибку, ИСПРАВЛЕНИЕ ЭТОЙ ОШИБКИ ЯВЛЯЕТСЯ ЗАДАЧЕЙ №1. Агент ДОЛЖЕН прочитать отчет об ошибке, сравнить его с `semantic_enrichment_protocol.md` и исправить код. НИКАКИЕ ДРУГИЕ ДЕЙСТВИЯ НЕ ДОПУСКАЮТСЯ до тех пор, пока семантическая валидация не будет пройдена успешно.
[/RULES] [/RULES]
[/GRACE_FRAMEWORK] [/GRACE_FRAMEWORK]
[MASTER_WORKFLOW] [MASTER_WORKFLOW]
### Шаг 1: Поиск и принятие задачи ### Шаг 1: Поиск и Принятие Задачи
1. Найти следующую задачу для `agent-developer` путем поиска файла в директории `tasks/` со статусом `pending`. 1. Найти `WorkOrder` в `tasks/` со статусом `pending`.
2. Прочитать файл задачи (`WorkOrder`) с помощью `read_file`. 2. Прочитать `WorkOrder` и изменить его статус на `in-progress`.
3. Изменить статус задачи на `in-progress` с помощью `apply_diff`. 3. Создать новую ветку для разработки.
### Шаг 2: Реализация ### Шаг 2: Автоматизированный Цикл Разработки и Ревью (Automated Code & Review Loop)
1. Изучить протокол `agent_promts/protocols/semantic_enrichment_protocol.md`. **Этот цикл повторяется до тех пор, пока все проверки не будут пройдены.**
2. Создать новую ветку для разработки, используя `execute_command` (`git branch ...`).
3. Реализовать код согласно `WorkOrder`, используя инструменты `write_to_file`, `apply_diff`, `insert_content`.
4. **Автоматизированная семантическая валидация:** Для КАЖДОГО созданного или измененного `.kt` файла запустить скрипт валидации: `python validate_semantics.py path/to/your/file.kt`.
5. **Цикл исправления:** Если скрипт валидации обнаруживает ошибки, НЕОБХОДИМО войти в цикл исправления:
a. Проанализировать отчет об ошибках.
b. Внести исправления в код с помощью `apply_diff`.
c. Повторно запустить валидацию (`python validate_semantics.py ...`).
d. Повторять шаги a-c, пока скрипт не выполнится без ошибок.
6. Запустить тесты и сборку через `execute_command` (`./gradlew build`).
### Шаг 3: Создание Pull Request и задачи для QA 1. **Реализация Кода:** Внести изменения в кодовую базу согласно `WorkOrder`.
1. Закоммитить изменения (`execute_command git commit ...`).
2. Создать Pull Request (через `execute_command`, если есть CLI для Gitea, или отметить как шаг для человека). 2. **Семантическая Валидация:**
3. Создать задачу для QA (написать файл `tasks/qa_task_...xml` с помощью `write_to_file`). a. Для каждого измененного файла запустить `python validate_semantics.py <file_path>`.
4. Обновить статус основной задачи на `pending-qa` (`apply_diff`). b. Если есть ошибки, проанализировать отчет и немедленно исправить код. **Вернуться к шагу 1.**
3. **Функциональное Тестирование (Reviewer Sub-Agent):**
a. Запустить полный набор тестов (`./gradlew build`).
b. Если тесты провалились, проанализировать отчет о сбое как **структурированный фидбэк от Reviewer'а**.
c. Интерпретировать отчет и попытаться исправить код. **Вернуться к шагу 1.**
### Шаг 3: Завершение и Передача на QA
1. **Все проверки пройдены.** Закоммитить финальные изменения.
2. Создать Pull Request.
3. Создать задачу для QA агента (например, `tasks/qa_task_...xml`).
4. Обновить статус `WorkOrder` на `pending-qa`.
[/MASTER_WORKFLOW] [/MASTER_WORKFLOW]
[SELF_REFLECTION_PROTOCOL] [SELF_REFLECTION_PROTOCOL]

View File

@@ -39,25 +39,21 @@
[/GRACE_FRAMEWORK] [/GRACE_FRAMEWORK]
[MASTER_WORKFLOW] [MASTER_WORKFLOW]
### Шаг 1: Поиск задачи на тестирование ### Шаг 1: Поиск и Принятие Задачи
1. Найти в директории `tasks/` файл задачи со статусом `pending-qa`. 1. Найти `WorkOrder` в `tasks/` со статусом `pending-qa`.
2. Прочитать файл задачи с помощью `read_file` чтобы получить ID `WorkOrder` и имя feature-ветки. 2. Прочитать `WorkOrder` и информацию о Pull Request.
3. Изменить статус задачи на `final-review`.
### Шаг 2: Сбор контекста и подготовка ### Шаг 2: Финальное Утверждение
1. Прочитать исходный `WorkOrder` (`tasks/workorder_{id}.xml`). 1. **Проверка Pull Request:** Провести высокоуровневый обзор изменений в PR. Детальная проверка кода и тесты уже выполнены `Code` агентом в рамках его автоматизированного цикла.
2. Переключиться на feature-ветку (`execute_command git checkout ...`). 2. **Основная задача QA** — подтвердить, что работа в целом соответствует бизнес-требованиям, изложенным в `WorkOrder`, и что автоматизированные проверки (`validate_semantics`, `build`) в CI/CD пайплайне успешно пройдены.
3. Прочитать измененные файлы.
### Шаг 3: Статический и динамический анализ ### Шаг 3: Завершение
1. Проверить код на соответствие `semantic_enrichment_protocol.md`. 1. **Если все в порядке:**
2. Запустить тесты и сборку (`execute_command ./gradlew build`). a. Влить (merge) Pull Request в основную ветку.
b. Обновить статус `WorkOrder` на `completed`.
### Шаг 4: Вынесение вердикта c. Удалить ветку разработки.
**ЕСЛИ** анализ на шаге 3 успешен: 2. **Если обнаружены критические проблемы:**
1. Обновить статус задачи на `approved` с помощью `apply_diff`. a. Отклонить Pull Request с четким объяснением.
2. Опционально: инициировать слияние ветки (`execute_command git merge ...`). b. Вернуть `WorkOrder` в статус `pending` для `Code` агента.
**ИНАЧЕ (если есть проблемы):**
1. Создать детальный отчет `reports/defect_report_{id}.md` с помощью `write_to_file`, описав все найденные проблемы и шаги для их воспроизведения.
2. Обновить статус задачи на `rejected` и добавить в нее ссылку на отчет о дефекте с помощью `apply_diff`.
[/MASTER_WORKFLOW] [/MASTER_WORKFLOW]

View File

@@ -7,7 +7,6 @@ plugins {
id("org.jetbrains.kotlin.plugin.compose") id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android") id("com.google.dagger.hilt.android")
id("kotlin-kapt") id("kotlin-kapt")
// id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
} }
android { android {
@@ -62,6 +61,16 @@ dependencies {
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":feature:scan")) implementation(project(":feature:scan"))
implementation(project(":feature:dashboard")) implementation(project(":feature:dashboard"))
implementation(project(":feature:inventorylist"))
implementation(project(":feature:itemdetails"))
implementation(project(":feature:itemedit"))
implementation(project(":feature:labeledit"))
implementation(project(":feature:labelslist"))
implementation(project(":feature:locationedit"))
implementation(project(":feature:locationslist"))
implementation(project(":feature:search"))
implementation(project(":feature:settings"))
implementation(project(":feature:setup"))
// [DEPENDENCY] AndroidX // [DEPENDENCY] AndroidX
implementation(Libs.coreKtx) implementation(Libs.coreKtx)
@@ -74,11 +83,10 @@ dependencies {
implementation(Libs.composeUiGraphics) implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview) implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3) implementation(Libs.composeMaterial3)
implementation("androidx.compose.material:material-icons-extended-android:1.6.8") implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose) implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose) implementation(Libs.hiltNavigationCompose)
// ktlint(project(":data:semantic-ktlint-rules"))
// [DEPENDENCY] DI (Hilt) // [DEPENDENCY] DI (Hilt)
implementation(Libs.hiltAndroid) implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler) kapt(Libs.hiltCompiler)

View File

@@ -1,5 +1,4 @@
// [PACKAGE] com.homebox.lens // [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt
// [FILE] MainActivity.kt
// [SEMANTICS] ui, activity, entrypoint // [SEMANTICS] ui, activity, entrypoint
package com.homebox.lens package com.homebox.lens
@@ -14,21 +13,31 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.feature.dashboard.navigation.navGraph
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Activity('MainActivity')] // [ENTITY: Activity('MainActivity')]
/** /**
* @summary Главная и единственная Activity в приложении. * @summary Главная и единственная Activity в приложении.
*/ */
// [ANCHOR:MainActivity:Class]
// [CONTRACT:MainActivity]
// [PURPOSE] Главная и единственная Activity в приложении.
// [END_CONTRACT:MainActivity]
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
// [ENTITY: Function('onCreate')] // [ANCHOR:onCreate:Function]
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')] // [CONTRACT:onCreate]
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')] // [PURPOSE] Инициализация Activity.
// [PARAM:savedInstanceState:Bundle?] Сохраненное состояние.
// [RELATION: CALLS:HomeboxLensTheme]
// [RELATION: CALLS:NavGraph]
// [RELATION: CALLS:Timber.d]
// [END_CONTRACT:onCreate]
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.") Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
@@ -36,35 +45,48 @@ class MainActivity : ComponentActivity() {
HomeboxLensTheme { HomeboxLensTheme {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background,
) { ) {
NavGraph() navGraph()
} }
} }
} }
} }
// [END_ENTITY: Function('onCreate')] // [END_ANCHOR:onCreate]
} }
// [END_ENTITY: Activity('MainActivity')] // [END_ANCHOR:MainActivity]
// [ENTITY: Function('Greeting')] // [ENTITY: Function('Greeting')]
// [ANCHOR:greeting:Function]
// [CONTRACT:greeting]
// [PURPOSE] Отображает приветствие.
// [PARAM:name:String] Имя для приветствия.
// [PARAM:modifier:Modifier] Модификатор для элемента.
// [END_CONTRACT:greeting]
@Composable @Composable
fun Greeting(name: String, modifier: Modifier = Modifier) { fun greeting(
name: String,
modifier: Modifier = Modifier,
) {
Text( Text(
text = "Hello $name!", text = "Hello $name!",
modifier = modifier modifier = modifier,
) )
} }
// [END_ENTITY: Function('Greeting')] // [END_ANCHOR:greeting]
// [ENTITY: Function('GreetingPreview')] // [ENTITY: Function('GreetingPreview')]
// [ANCHOR:greetingPreview:Function]
// [CONTRACT:greetingPreview]
// [PURPOSE] Предварительный просмотр функции greeting.
// [END_CONTRACT:greetingPreview]
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
fun GreetingPreview() { fun greetingPreview() {
HomeboxLensTheme { HomeboxLensTheme {
Greeting("Android") greeting("Android")
} }
} }
// [END_ENTITY: Function('GreetingPreview')] // [END_ANCHOR:greetingPreview]
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
// [END_FILE_MainActivity.kt] // [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]

View File

@@ -10,12 +10,12 @@ import timber.log.Timber
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Application('MainApplication')] // [ENTITY: Application('MainApplication')]
/** /**
* @summary Точка входа в приложение. Инициализирует Hilt и Timber. * @summary Точка входа в приложение. Инициализирует Hilt и Timber.
*/ */
@HiltAndroidApp @HiltAndroidApp
class MainApplication : Application() { class MainApplication : Application() {
// [ENTITY: Function('onCreate')] // [ENTITY: Function('onCreate')]
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View File

@@ -1,132 +0,0 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavigationActions.kt
// [SEMANTICS] navigation, controller, actions
package com.homebox.lens.navigation
// [IMPORTS]
import androidx.navigation.NavHostController
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Class('NavigationActions')]
// [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
/**
* @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
* @param navController Контроллер Jetpack Navigation.
* @invariant Все навигационные действия должны использовать предоставленный navController.
*/
class NavigationActions(val navController: NavHostController) {
// [ENTITY: Function('navigateToDashboard')]
/**
* @summary Навигация на главный экран.
* @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
*/
fun navigateToDashboard() {
Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.")
navController.navigate(Screen.Dashboard.route) {
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToDashboard')]
// [ENTITY: Function('navigateToLocations')]
fun navigateToLocations() {
Timber.i("[INFO][ACTION][navigate_to_locations] Navigating to Locations.")
navController.navigate(Screen.LocationsList.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToLocations')]
// [ENTITY: Function('navigateToLabels')]
fun navigateToLabels() {
Timber.i("[INFO][ACTION][navigate_to_labels] Navigating to Labels.")
navController.navigate(Screen.LabelsList.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToLabels')]
// [ENTITY: Function('navigateToLabelEdit')]
fun navigateToLabelEdit(labelId: String? = null) {
Timber.i("[INFO][ACTION][navigate_to_label_edit] Navigating to Label Edit with ID: %s", labelId)
navController.navigate(Screen.LabelEdit.createRoute(labelId))
}
// [END_ENTITY: Function('navigateToLabelEdit')]
// [ENTITY: Function('navigateToSearch')]
fun navigateToSearch() {
Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.")
navController.navigate(Screen.Search.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToSearch')]
// [ENTITY: Function('navigateToScan')]
/**
* @summary Навигация на экран сканирования QR/штрих-кодов.
*/
fun navigateToScan() {
Timber.i("[INFO][ACTION][navigate_to_scan] Navigating to Scan screen.")
navController.navigate(Screen.Scan.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToScan')]
// [ENTITY: Function('navigateToSettings')]
/**
* @summary Навигация на экран настроек.
*/
fun navigateToSettings() {
Timber.i("[INFO][ACTION][navigate_to_settings] Navigating to Settings.")
navController.navigate(Screen.Settings.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToSettings')]
// [ENTITY: Function('navigateToInventoryListWithLabel')]
fun navigateToInventoryListWithLabel(labelId: String) {
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId)
val route = Screen.InventoryList.withFilter("label", labelId)
navController.navigate(route)
}
// [END_ENTITY: Function('navigateToInventoryListWithLabel')]
// [ENTITY: Function('navigateToInventoryListWithLocation')]
fun navigateToInventoryListWithLocation(locationId: String) {
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Navigating to Inventory with location: %s", locationId)
val route = Screen.InventoryList.withFilter("location", locationId)
navController.navigate(route)
}
// [END_ENTITY: Function('navigateToInventoryListWithLocation')]
// [ENTITY: Function('navigateToCreateItem')]
fun navigateToCreateItem() {
Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.")
navController.navigate(Screen.ItemEdit.createRoute())
}
// [END_ENTITY: Function('navigateToCreateItem')]
// [ENTITY: Function('navigateToLogout')]
fun navigateToLogout() {
Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.")
navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
}
}
// [END_ENTITY: Function('navigateToLogout')]
// [ENTITY: Function('navigateBack')]
fun navigateBack() {
Timber.i("[INFO][ACTION][navigate_back] Navigating back.")
navController.popBackStack()
}
// [END_ENTITY: Function('navigateBack')]
}
// [END_ENTITY: Class('NavigationActions')]
// [END_FILE_NavigationActions.kt]

View File

@@ -1,131 +0,0 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] Screen.kt
// [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation
// [ENTITY: SealedClass('Screen')]
/**
* @summary Запечатанный класс для определения маршрутов навигации в приложении.
* @description Обеспечивает типобезопасность при навигации.
* @param route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
// [ENTITY: Object('Setup')]
data object Setup : Screen("setup_screen")
// [END_ENTITY: Object('Setup')]
// [ENTITY: Object('Dashboard')]
data object Dashboard : Screen("dashboard_screen")
// [END_ENTITY: Object('Dashboard')]
// [ENTITY: Object('InventoryList')]
data object InventoryList : Screen("inventory_list_screen") {
// [ENTITY: Function('withFilter')]
/**
* @summary Создает маршрут для экрана списка инвентаря с параметром фильтра.
* @param key Ключ фильтра (например, "label" или "location").
* @param value Значение фильтра (например, ID метки или местоположения).
* @return Строку полного маршрута с query-параметром.
* @throws IllegalArgumentException если ключ или значение пустые.
*/
fun withFilter(key: String, value: String): String {
require(key.isNotBlank()) { "Filter key cannot be blank." }
require(value.isNotBlank()) { "Filter value cannot be blank." }
val constructedRoute = "inventory_list_screen?$key=$value"
check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." }
return constructedRoute
}
// [END_ENTITY: Function('withFilter')]
}
// [END_ENTITY: Object('InventoryList')]
// [ENTITY: Object('ItemDetails')]
data object ItemDetails : Screen("item_details_screen/{itemId}") {
// [ENTITY: Function('createRoute')]
/**
* @summary Создает маршрут для экрана деталей элемента с указанным ID.
* @param itemId ID элемента для отображения.
* @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой.
*/
fun createRoute(itemId: String): String {
require(itemId.isNotBlank()) { "itemId не может быть пустым." }
val route = "item_details_screen/$itemId"
check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." }
return route
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: Object('ItemDetails')]
// [ENTITY: Object('ItemEdit')]
data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") {
// [ENTITY: Function('createRoute')]
/**
* @summary Создает маршрут для экрана редактирования элемента с указанным ID.
* @param itemId ID элемента для редактирования. Null, если создается новый элемент.
* @return Строку полного маршрута.
*/
fun createRoute(itemId: String? = null): String {
return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen"
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: Object('ItemEdit')]
// [ENTITY: Object('LabelsList')]
data object LabelsList : Screen("labels_list_screen")
// [END_ENTITY: Object('LabelsList')]
// [ENTITY: Object('LabelEdit')]
data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") {
// [ENTITY: Function('createRoute')]
/**
* @summary Создает маршрут для экрана редактирования метки с указанным ID.
* @param labelId ID метки для редактирования. Null, если создается новая метка.
* @return Строку полного маршрута.
*/
fun createRoute(labelId: String? = null): String {
return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen"
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: Object('LabelEdit')]
// [ENTITY: Object('LocationsList')]
data object LocationsList : Screen("locations_list_screen")
// [END_ENTITY: Object('LocationsList')]
// [ENTITY: Object('LocationEdit')]
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
// [ENTITY: Function('createRoute')]
/**
* @summary Создает маршрут для экрана редактирования местоположения с указанным ID.
* @param locationId ID местоположения для редактирования.
* @return Строку полного маршрута.
* @throws IllegalArgumentException если locationId пустой.
*/
fun createRoute(locationId: String): String {
require(locationId.isNotBlank()) { "locationId не может быть пустым." }
val route = "location_edit_screen/$locationId"
check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." }
return route
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: Object('LocationEdit')]
// [ENTITY: Object('Search')]
data object Search : Screen("search_screen")
// [END_ENTITY: Object('Search')]
// [ENTITY: Object('Settings')]
data object Settings : Screen("settings_screen")
// [END_ENTITY: Object('Settings')]
// [ENTITY: Object('Scan')]
data object Scan : Screen("scan_screen")
// [END_ENTITY: Object('Scan')]
}
// [END_ENTITY: SealedClass('Screen')]
// [END_FILE_Screen.kt]

View File

@@ -1,116 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.common
// [FILE] AppDrawer.kt
// [SEMANTICS] ui, common, navigation_drawer
package com.homebox.lens.ui.common
// [IMPORTS]
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.navigation.Screen
// [END_IMPORTS]
// [ENTITY: Function('AppDrawerContent')]
// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
/**
* @summary Контент для бокового навигационного меню (Drawer).
* @param currentRoute Текущий маршрут для подсветки активного элемента.
* @param navigationActions Объект с навигационными действиями.
* @param onCloseDrawer Лямбда для закрытия бокового меню.
*/
@Composable
internal fun AppDrawerContent(
currentRoute: String?,
navigationActions: NavigationActions,
onCloseDrawer: () -> Unit
) {
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
Button(
onClick = {
navigationActions.navigateToCreateItem()
onCloseDrawer()
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text(stringResource(id = R.string.create))
}
Spacer(Modifier.height(12.dp))
Divider()
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.dashboard_title)) },
selected = currentRoute == Screen.Dashboard.route,
onClick = {
navigationActions.navigateToDashboard()
onCloseDrawer()
}
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_locations)) },
selected = currentRoute == Screen.LocationsList.route,
onClick = {
navigationActions.navigateToLocations()
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,
onClick = {
navigationActions.navigateToSearch()
onCloseDrawer()
}
)
NavigationDrawerItem(
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
label = { Text("Настройки") },
selected = false,
onClick = {
navigationActions.navigateToSettings()
onCloseDrawer()
}
)
// [AI_NOTE]: Add Profile and Tools items
Divider()
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.logout)) },
selected = false,
onClick = {
navigationActions.navigateToLogout()
onCloseDrawer()
}
)
}
}
// [END_ENTITY: Function('AppDrawerContent')]
// [END_FILE_AppDrawer.kt]

View File

@@ -1,91 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.common
// [FILE] MainScaffold.kt
// [SEMANTICS] ui, common, scaffold, navigation_drawer
package com.homebox.lens.ui.common
// [IMPORTS]
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import kotlinx.coroutines.launch
// [END_IMPORTS]
// [ENTITY: Function('MainScaffold')]
// [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')]
/**
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
* @param topBarTitle Заголовок для TopAppBar.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* @param topBarActions Composable-функция для отображения действий (иконок) в TopAppBar.
* @param content Основное содержимое экрана, которое будет отображено внутри Scaffold.
* @sideeffect Управляет состоянием (открыто/закрыто) бокового меню (ModalNavigationDrawer).
* @invariant TopAppBar всегда отображается с иконкой меню.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScaffold(
topBarTitle: String,
currentRoute: String?,
navigationActions: NavigationActions,
onNavigateUp: (() -> Unit)? = null,
topBarActions: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {},
content: @Composable (PaddingValues) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
AppDrawerContent(
currentRoute = currentRoute,
navigationActions = navigationActions,
onCloseDrawer = { scope.launch { drawerState.close() } }
)
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(topBarTitle) },
navigationIcon = {
if (onNavigateUp != null) {
IconButton(onClick = onNavigateUp) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.cd_navigate_up)
)
}
} else {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(
Icons.Default.Menu,
contentDescription = stringResource(id = R.string.cd_open_navigation_drawer)
)
}
}
},
actions = { topBarActions() }
)
},
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton
) { paddingValues ->
content(paddingValues)
}
}
}
// [END_ENTITY: Function('MainScaffold')]
// [END_FILE_MainScaffold.kt]

View File

@@ -1,76 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.components
// [FILE] ColorPicker.kt
// [SEMANTICS] ui, component, color_selection
package com.homebox.lens.ui.components
// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.homebox.lens.R
// [END_IMPORTS]
// [ENTITY: Function('ColorPicker')]
/**
* @summary Компонент для выбора цвета.
* @param selectedColor Текущий выбранный цвет в формате HEX строки (например, "#FFFFFF").
* @param onColorSelected Лямбда-функция, вызываемая при выборе нового цвета.
* @param modifier Модификатор для настройки внешнего вида.
*/
@Composable
fun ColorPicker(
selectedColor: String,
onColorSelected: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Box(
modifier = Modifier
.size(48.dp)
.background(
if (selectedColor.isEmpty()) Color.Transparent else Color(android.graphics.Color.parseColor(selectedColor)),
CircleShape
)
.border(2.dp, MaterialTheme.colorScheme.onSurface, CircleShape)
.clickable { /* TODO: Implement a more advanced color selection dialog */ }
)
Spacer(modifier = Modifier.width(16.dp))
OutlinedTextField(
value = selectedColor,
onValueChange = { newValue ->
// Basic validation for hex color
if (newValue.matches(Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"))) {
onColorSelected(newValue)
} else if (newValue.isEmpty() || newValue == "#") {
onColorSelected("#FFFFFF") // Default to white if input is cleared
}
},
label = { Text(stringResource(R.string.label_hex_color)) },
singleLine = true,
modifier = Modifier.weight(1f)
)
}
}
}
// [END_ENTITY: Function('ColorPicker')]
// [END_FILE_ColorPicker.kt]

View File

@@ -1,35 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.components
// [FILE] LoadingOverlay.kt
// [SEMANTICS] ui, component, loading
package com.homebox.lens.ui.components
// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
// [END_IMPORTS]
// [ENTITY: Function('LoadingOverlay')]
/**
* @summary Полноэкранный оверлей с индикатором загрузки.
*/
@Composable
fun LoadingOverlay() {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface.copy(alpha = 0.6f)),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
// [END_ENTITY: Function('LoadingOverlay')]
// [END_FILE_LoadingOverlay.kt]

View File

@@ -1,39 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
// [SEMANTICS] ui, screen, inventory, list
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('InventoryListScreen')]
// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Список инвентаря".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
*/
@Composable
fun InventoryListScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.inventory_list_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) {
// [AI_NOTE]: Implement Inventory List Screen UI
Text(text = "Inventory List Screen")
}
}
// [END_ENTITY: Function('InventoryListScreen')]
// [END_FILE_InventoryListScreen.kt]

View File

@@ -1,21 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt
// [SEMANTICS] ui, viewmodel, inventory_list
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('InventoryListViewModel')]
/**
* @summary ViewModel for the inventory list screen.
*/
@HiltViewModel
class InventoryListViewModel @Inject constructor() : ViewModel() {
// [AI_NOTE]: Implement UI state
}
// [END_ENTITY: ViewModel('InventoryListViewModel')]
// [END_FILE_InventoryListViewModel.kt]

View File

@@ -1,39 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt
// [SEMANTICS] ui, screen, item, details
package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('ItemDetailsScreen')]
// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Детали элемента".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
*/
@Composable
fun ItemDetailsScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.item_details_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) {
// [AI_NOTE]: Implement Item Details Screen UI
Text(text = "Item Details Screen")
}
}
// [END_ENTITY: Function('ItemDetailsScreen')]
// [END_FILE_ItemDetailsScreen.kt]

View File

@@ -1,21 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsViewModel.kt
// [SEMANTICS] ui, viewmodel, item_details
package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('ItemDetailsViewModel')]
/**
* @summary ViewModel for the item details screen.
*/
@HiltViewModel
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
// [AI_NOTE]: Implement UI state
}
// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
// [END_FILE_ItemDetailsViewModel.kt]

View File

@@ -1,436 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt
// [SEMANTICS] ui, screen, item, edit
package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
// [END_IMPORTS]
// [ENTITY: Function('ItemEditScreen')]
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [ViewModel('ItemEditViewModel')]
// [RELATION: Function('ItemEditScreen')] -> [CONSUMES_STATE] -> [DataClass('ItemEditUiState')]
// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Редактирование элемента".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* @param itemId ID элемента для редактирования. Null, если создается новый элемент.
* @param viewModel ViewModel для управления состоянием экрана.
* @param onSaveSuccess Callback, вызываемый после успешного сохранения товара.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEditScreen(
currentRoute: String?,
navigationActions: NavigationActions,
itemId: String?,
viewModel: ItemEditViewModel = hiltViewModel(),
onSaveSuccess: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
val navBackStackEntry = navigationActions.navController.currentBackStackEntry
LaunchedEffect(itemId) {
Timber.i("[INFO][ENTRYPOINT][item_edit_screen_init] Initializing ItemEditScreen for item ID: %s", itemId)
viewModel.loadItem(itemId)
}
LaunchedEffect(navBackStackEntry) {
navBackStackEntry?.savedStateHandle?.get<String>("barcodeResult")?.let { barcode ->
viewModel.updateAssetId(barcode)
navBackStackEntry.savedStateHandle?.remove<String>("barcodeResult")
Timber.i("[INFO][ACTION][barcode_received] Received barcode: %s", barcode)
}
}
LaunchedEffect(uiState.error) {
uiState.error?.let {
snackbarHostState.showSnackbar(it)
Timber.e("[ERROR][UI_ERROR][item_edit_error] Displaying error: %s", it)
}
}
LaunchedEffect(Unit) {
viewModel.saveCompleted.collect {
Timber.i("[INFO][ACTION][save_completed_callback] Item save completed. Triggering onSaveSuccess.")
onSaveSuccess()
}
}
MainScaffold(
topBarTitle = stringResource(id = R.string.item_edit_title),
currentRoute = currentRoute,
navigationActions = navigationActions,
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
FloatingActionButton(onClick = {
Timber.i("[INFO][ACTION][save_button_click] Save button clicked.")
viewModel.saveItem()
}) {
Icon(Icons.Filled.Save, contentDescription = stringResource(R.string.save_item))
}
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.fillMaxWidth())
} else {
uiState.item?.let { item ->
OutlinedTextField(
value = item.name,
onValueChange = { viewModel.updateName(it) },
label = { Text(stringResource(R.string.item_name)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.description ?: "",
onValueChange = { viewModel.updateDescription(it) },
label = { Text(stringResource(R.string.item_description)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.quantity.toString(),
onValueChange = { viewModel.updateQuantity(it.toIntOrNull() ?: 0) },
label = { Text(stringResource(R.string.item_quantity)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
// Asset ID
OutlinedTextField(
value = item.assetId ?: "",
onValueChange = { viewModel.updateAssetId(it) },
label = { Text(stringResource(R.string.item_asset_id)) },
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
IconButton(onClick = {
Timber.d("[DEBUG][ACTION][scan_qr_code_click] Scan QR code button clicked.")
navigationActions.navigateToScan()
}) {
Icon(Icons.Filled.QrCodeScanner, contentDescription = stringResource(R.string.scan_qr_code))
}
}
)
Spacer(modifier = Modifier.height(8.dp))
// Notes
OutlinedTextField(
value = item.notes ?: "",
onValueChange = { viewModel.updateNotes(it) },
label = { Text(stringResource(R.string.item_notes)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Serial Number
OutlinedTextField(
value = item.serialNumber ?: "",
onValueChange = { viewModel.updateSerialNumber(it) },
label = { Text(stringResource(R.string.item_serial_number)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Purchase Price
OutlinedTextField(
value = item.purchasePrice?.toString() ?: "",
onValueChange = { viewModel.updatePurchasePrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_purchase_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Purchase Date
var showPurchaseDatePicker by remember { mutableStateOf(false) }
val purchaseDatePickerState = rememberDatePickerState()
val coroutineScope = rememberCoroutineScope()
OutlinedTextField(
value = item.purchaseDate ?: "",
onValueChange = { }, // Read-only
label = { Text(stringResource(R.string.item_purchase_date)) },
modifier = Modifier.fillMaxWidth(),
readOnly = true,
interactionSource = remember { MutableInteractionSource() }
.also { interactionSource ->
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
coroutineScope.launch {
showPurchaseDatePicker = true
}
}
}
}
)
if (showPurchaseDatePicker) {
DatePickerDialog(
onDismissRequest = { showPurchaseDatePicker = false },
confirmButton = {
Button(onClick = {
purchaseDatePickerState.selectedDateMillis?.let { millis ->
val selectedDate = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate()
viewModel.updatePurchaseDate(selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
}
showPurchaseDatePicker = false
}) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
Button(onClick = { showPurchaseDatePicker = false }) {
Text(stringResource(R.string.cancel))
}
}
) {
DatePicker(state = purchaseDatePickerState)
}
}
Spacer(modifier = Modifier.height(8.dp))
// Warranty Until
var showWarrantyDatePicker by remember { mutableStateOf(false) }
val warrantyDatePickerState = rememberDatePickerState()
OutlinedTextField(
value = item.warrantyUntil ?: "",
onValueChange = { }, // Read-only
label = { Text(stringResource(R.string.item_warranty_until)) },
modifier = Modifier.fillMaxWidth(),
readOnly = true,
interactionSource = remember { MutableInteractionSource() }
.also { interactionSource ->
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect {
coroutineScope.launch {
showWarrantyDatePicker = true
}
}
}
}
)
if (showWarrantyDatePicker) {
DatePickerDialog(
onDismissRequest = { showWarrantyDatePicker = false },
confirmButton = {
Button(onClick = {
warrantyDatePickerState.selectedDateMillis?.let { millis ->
val selectedDate = Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDate()
viewModel.updateWarrantyUntil(selectedDate.format(DateTimeFormatter.ISO_LOCAL_DATE))
}
showWarrantyDatePicker = false
}) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
Button(onClick = { showWarrantyDatePicker = false }) {
Text(stringResource(R.string.cancel))
}
}
) {
DatePicker(state = warrantyDatePickerState)
}
}
Spacer(modifier = Modifier.height(8.dp))
// Parent ID (simplified for now, ideally a picker)
OutlinedTextField(
value = item.parentId ?: "",
onValueChange = { viewModel.updateParentId(it) },
label = { Text(stringResource(R.string.item_parent_id)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Checkboxes
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.item_is_archived))
Checkbox(
checked = item.isArchived ?: false,
onCheckedChange = { viewModel.updateIsArchived(it) }
)
}
HorizontalDivider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.item_insured))
Checkbox(
checked = item.insured ?: false,
onCheckedChange = { viewModel.updateInsured(it) }
)
}
HorizontalDivider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.item_lifetime_warranty))
Checkbox(
checked = item.lifetimeWarranty ?: false,
onCheckedChange = { viewModel.updateLifetimeWarranty(it) }
)
}
HorizontalDivider()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.item_sync_child_items_locations))
Checkbox(
checked = item.syncChildItemsLocations ?: false,
onCheckedChange = { viewModel.updateSyncChildItemsLocations(it) }
)
}
HorizontalDivider()
Spacer(modifier = Modifier.height(8.dp))
// Manufacturer
OutlinedTextField(
value = item.manufacturer ?: "",
onValueChange = { viewModel.updateManufacturer(it) },
label = { Text(stringResource(R.string.item_manufacturer)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Model Number
OutlinedTextField(
value = item.modelNumber ?: "",
onValueChange = { viewModel.updateModelNumber(it) },
label = { Text(stringResource(R.string.item_model_number)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Purchase From
OutlinedTextField(
value = item.purchaseFrom ?: "",
onValueChange = { viewModel.updatePurchaseFrom(it) },
label = { Text(stringResource(R.string.item_purchase_from)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Warranty Details
OutlinedTextField(
value = item.warrantyDetails ?: "",
onValueChange = { viewModel.updateWarrantyDetails(it) },
label = { Text(stringResource(R.string.item_warranty_details)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
// Sold Details (simplified for now)
OutlinedTextField(
value = item.soldNotes ?: "",
onValueChange = { viewModel.updateSoldNotes(it) },
label = { Text(stringResource(R.string.item_sold_notes)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldPrice?.toString() ?: "",
onValueChange = { viewModel.updateSoldPrice(it.toDoubleOrNull()) },
label = { Text(stringResource(R.string.item_sold_price)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldTime ?: "",
onValueChange = { viewModel.updateSoldTime(it) },
label = { Text(stringResource(R.string.item_sold_time)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = item.soldTo ?: "",
onValueChange = { viewModel.updateSoldTo(it) },
label = { Text(stringResource(R.string.item_sold_to)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
}
// [END_ENTITY: Function('ItemEditScreen')]
// [END_FILE_ItemEditScreen.kt]

View File

@@ -1,583 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModel.kt
// [SEMANTICS] ui, viewmodel, item_edit
package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.model.ItemUpdate
import com.homebox.lens.domain.model.Label
import com.homebox.lens.domain.model.Location
import com.homebox.lens.domain.model.LocationOut
import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateItemUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: DataClass('ItemEditUiState')]
/**
* @summary UI state for the item edit screen.
* @param item The item being edited, or null if creating a new item.
* @param isLoading Whether data is currently being loaded or saved.
* @param error An error message if an operation failed.
*/
data class ItemEditUiState(
val item: Item? = null,
val locations: List<LocationOut> = emptyList(),
val selectedLocationId: String? = null,
val isLoading: Boolean = false,
val error: String? = null
)
// [END_ENTITY: DataClass('ItemEditUiState')]
// [ENTITY: ViewModel('ItemEditViewModel')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateItemUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetItemDetailsUseCase')]
// [RELATION: ViewModel('ItemEditViewModel')] -> [EMITS_STATE] -> [DataClass('ItemEditUiState')]
/**
* @summary ViewModel for the item edit screen.
*/
@HiltViewModel
class ItemEditViewModel @Inject constructor(
private val createItemUseCase: CreateItemUseCase,
private val updateItemUseCase: UpdateItemUseCase,
private val getItemDetailsUseCase: GetItemDetailsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(ItemEditUiState())
val uiState: StateFlow<ItemEditUiState> = _uiState.asStateFlow()
private val _saveCompleted = MutableSharedFlow<Unit>()
val saveCompleted: SharedFlow<Unit> = _saveCompleted.asSharedFlow()
// [ENTITY: Function('loadItem')]
/**
* @summary Loads item details for editing or prepares for new item creation.
* @param itemId The ID of the item to load. If null, a new item is being created.
* @sideeffect Updates `_uiState` with loading, success, or error states.
*/
fun loadItem(itemId: String?) {
Timber.i("[INFO][ENTRYPOINT][loading_item] Attempting to load item with ID: %s", itemId)
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
loadLocations()
if (itemId == null) {
Timber.i("[INFO][ACTION][new_item_preparation] Preparing for new item creation.")
_uiState.value = _uiState.value.copy(
isLoading = false, item = Item(
id = "",
name = "",
description = null,
quantity = 0,
image = null,
location = null,
labels = emptyList(),
value = null,
createdAt = null,
assetId = null,
notes = null,
serialNumber = null,
purchasePrice = null,
purchaseDate = null,
warrantyUntil = null,
parentId = null,
isArchived = null,
insured = null,
lifetimeWarranty = null,
manufacturer = null,
modelNumber = null,
purchaseFrom = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = null,
warrantyDetails = null
)
)
} else {
try {
Timber.i("[INFO][ACTION][fetching_item_details] Fetching details for item ID: %s", itemId)
val itemOut = getItemDetailsUseCase(itemId)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
val item = Item(
id = itemOut.id,
name = itemOut.name,
description = itemOut.description,
quantity = itemOut.quantity,
image = itemOut.images.firstOrNull()?.path,
location = itemOut.location?.let { Location(it.id, it.name) },
labels = itemOut.labels.map { Label(it.id, it.name) },
value = itemOut.value,
createdAt = itemOut.createdAt,
assetId = itemOut.assetId,
notes = itemOut.notes,
serialNumber = itemOut.serialNumber,
purchasePrice = itemOut.purchasePrice,
purchaseDate = itemOut.purchaseDate,
warrantyUntil = itemOut.warrantyUntil,
parentId = itemOut.parent?.id,
isArchived = itemOut.isArchived,
insured = itemOut.insured,
lifetimeWarranty = itemOut.lifetimeWarranty,
manufacturer = itemOut.manufacturer,
modelNumber = itemOut.modelNumber,
purchaseFrom = itemOut.purchaseFrom,
soldNotes = itemOut.soldNotes,
soldPrice = itemOut.soldPrice,
soldTime = itemOut.soldTime,
soldTo = itemOut.soldTo,
syncChildItemsLocations = itemOut.syncChildItemsLocations,
warrantyDetails = itemOut.warrantyDetails
)
_uiState.value = _uiState.value.copy(
isLoading = false,
item = item,
selectedLocationId = item.location?.id
)
Timber.i("[INFO][ACTION][item_details_fetched] Successfully fetched item details for ID: %s", itemId)
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][item_load_failed] Failed to load item details for ID: %s", itemId)
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
}
}
}
}
// [END_ENTITY: Function('loadItem')]
// [ENTITY: Function('saveItem')]
/**
* @summary Saves the current item, either creating a new one or updating an existing one.
* @sideeffect Updates `_uiState` with loading, success, or error states. Calls `createItemUseCase` or `updateItemUseCase`.
* @throws IllegalStateException if `uiState.value.item` is null when attempting to save.
*/
private fun loadLocations() {
viewModelScope.launch {
try {
val locations = getAllLocationsUseCase()
_uiState.value = _uiState.value.copy(locations = locations.map { LocationOut(it.id, it.name, it.color, it.isArchived, it.createdAt, it.updatedAt) })
Timber.i("[INFO][ACTION][locations_loaded] Loaded %d locations", locations.size)
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][locations_load_failed] Failed to load locations")
_uiState.value = _uiState.value.copy(error = e.localizedMessage)
}
}
}
fun updateSelectedLocationId(locationId: String?) {
Timber.d("[DEBUG][ACTION][updating_selected_location] Selected location ID: %s", locationId)
val location = _uiState.value.locations.find { it.id == locationId }
_uiState.value = _uiState.value.copy(
selectedLocationId = locationId,
item = _uiState.value.item?.copy(location = location?.let { Location(it.id, it.name) })
)
}
fun saveItem() {
Timber.i("[INFO][ENTRYPOINT][saving_item] Attempting to save item.")
viewModelScope.launch {
val currentItem = _uiState.value.item
val selectedLocationId = _uiState.value.selectedLocationId
require(currentItem != null) { "[CONTRACT_VIOLATION][PRECONDITION][item_not_present] Cannot save a null item." }
if (currentItem.id.isBlank() && selectedLocationId == null) {
throw IllegalStateException("Location is required for creating a new item.")
}
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
try {
if (currentItem.id.isBlank()) {
Timber.i("[INFO][ACTION][creating_new_item] Creating new item: %s", currentItem.name)
val createdItemSummary = createItemUseCase(
ItemCreate(
name = currentItem.name,
description = currentItem.description,
quantity = currentItem.quantity,
assetId = currentItem.assetId,
notes = currentItem.notes,
serialNumber = currentItem.serialNumber,
value = currentItem.value,
purchasePrice = currentItem.purchasePrice,
purchaseDate = currentItem.purchaseDate,
warrantyUntil = currentItem.warrantyUntil,
locationId = selectedLocationId,
parentId = currentItem.parentId,
labelIds = currentItem.labels.map { it.id }
)
)
Timber.d("[DEBUG][ACTION][mapping_item_summary_to_item] Mapping ItemSummary to Item for UI state.")
val createdItem = currentItem.copy(id = createdItemSummary.id, name = createdItemSummary.name)
_uiState.value = _uiState.value.copy(isLoading = false, item = createdItem)
Timber.i("[INFO][ACTION][new_item_created] Successfully created new item with ID: %s", createdItem.id)
_saveCompleted.emit(Unit)
} else {
Timber.i("[INFO][ACTION][updating_existing_item] Updating existing item with ID: %s", currentItem.id)
val updatedItemOut = updateItemUseCase(currentItem)
Timber.d("[DEBUG][ACTION][mapping_item_out_to_item] Mapping ItemOut to Item for UI state.")
val updatedItem = currentItem.copy(
id = updatedItemOut.id,
name = updatedItemOut.name,
description = updatedItemOut.description,
quantity = updatedItemOut.quantity,
image = updatedItemOut.images.firstOrNull()?.path,
location = updatedItemOut.location?.let { Location(it.id, it.name) },
labels = updatedItemOut.labels.map { Label(it.id, it.name) },
value = updatedItemOut.value,
createdAt = updatedItemOut.createdAt,
assetId = updatedItemOut.assetId,
notes = updatedItemOut.notes,
serialNumber = updatedItemOut.serialNumber,
purchasePrice = updatedItemOut.purchasePrice,
purchaseDate = updatedItemOut.purchaseDate,
warrantyUntil = updatedItemOut.warrantyUntil,
parentId = updatedItemOut.parent?.id,
isArchived = updatedItemOut.isArchived,
insured = updatedItemOut.insured,
lifetimeWarranty = updatedItemOut.lifetimeWarranty,
manufacturer = updatedItemOut.manufacturer,
modelNumber = updatedItemOut.modelNumber,
purchaseFrom = updatedItemOut.purchaseFrom,
soldNotes = updatedItemOut.soldNotes,
soldPrice = updatedItemOut.soldPrice,
soldTime = updatedItemOut.soldTime,
soldTo = updatedItemOut.soldTo,
syncChildItemsLocations = updatedItemOut.syncChildItemsLocations,
warrantyDetails = updatedItemOut.warrantyDetails
)
_uiState.value = _uiState.value.copy(
isLoading = false,
item = updatedItem,
selectedLocationId = updatedItem.location?.id
)
Timber.i("[INFO][ACTION][item_updated] Successfully updated item with ID: %s", updatedItem.id)
_saveCompleted.emit(Unit)
}
} catch (e: Exception) {
Timber.e(e, "[ERROR][FALLBACK][item_save_failed] Failed to save item.")
_uiState.value = _uiState.value.copy(isLoading = false, error = e.localizedMessage)
}
}
}
// [END_ENTITY: Function('saveItem')]
// [ENTITY: Function('updateName')]
/**
* @summary Updates the name of the item in the UI state.
* @param newName The new name for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateName(newName: String) {
Timber.d("[DEBUG][ACTION][updating_item_name] Updating item name to: %s", newName)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(name = newName))
}
// [END_ENTITY: Function('updateName')]
// [ENTITY: Function('updateDescription')]
/**
* @summary Updates the description of the item in the UI state.
* @param newDescription The new description for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateDescription(newDescription: String) {
Timber.d("[DEBUG][ACTION][updating_item_description] Updating item description to: %s", newDescription)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(description = newDescription))
}
// [END_ENTITY: Function('updateDescription')]
// [ENTITY: Function('updateQuantity')]
/**
* @summary Updates the quantity of the item in the UI state.
* @param newQuantity The new quantity for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateQuantity(newQuantity: Int) {
Timber.d("[DEBUG][ACTION][updating_item_quantity] Updating item quantity to: %d", newQuantity)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(quantity = newQuantity))
}
// [END_ENTITY: Function('updateQuantity')]
// [ENTITY: Function('updateAssetId')]
/**
* @summary Updates the asset ID of the item in the UI state.
* @param newAssetId The new asset ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateAssetId(newAssetId: String?) {
Timber.d("[DEBUG][ACTION][updating_item_assetId] Updating item assetId to: %s", newAssetId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(assetId = newAssetId))
}
// [END_ENTITY: Function('updateAssetId')]
// [ENTITY: Function('updateNotes')]
/**
* @summary Updates the notes of the item in the UI state.
* @param newNotes The new notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateNotes(newNotes: String?) {
Timber.d("[DEBUG][ACTION][updating_item_notes] Updating item notes to: %s", newNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(notes = newNotes))
}
// [END_ENTITY: Function('updateNotes')]
// [ENTITY: Function('updateSerialNumber')]
/**
* @summary Updates the serial number of the item in the UI state.
* @param newSerialNumber The new serial number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSerialNumber(newSerialNumber: String?) {
Timber.d("[DEBUG][ACTION][updating_item_serialNumber] Updating item serialNumber to: %s", newSerialNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(serialNumber = newSerialNumber))
}
// [END_ENTITY: Function('updateSerialNumber')]
// [ENTITY: Function('updatePurchasePrice')]
/**
* @summary Updates the purchase price of the item in the UI state.
* @param newPurchasePrice The new purchase price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchasePrice(newPurchasePrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_purchasePrice] Updating item purchasePrice to: %f", newPurchasePrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchasePrice = newPurchasePrice))
}
// [END_ENTITY: Function('updatePurchasePrice')]
// [ENTITY: Function('updatePurchaseDate')]
/**
* @summary Updates the purchase date of the item in the UI state.
* @param newPurchaseDate The new purchase date for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseDate(newPurchaseDate: String?) {
Timber.d("[DEBUG][ACTION][updating_item_purchaseDate] Updating item purchaseDate to: %s", newPurchaseDate)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseDate = newPurchaseDate))
}
// [END_ENTITY: Function('updatePurchaseDate')]
// [ENTITY: Function('updateWarrantyUntil')]
/**
* @summary Updates the warranty until date of the item in the UI state.
* @param newWarrantyUntil The new warranty until date for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyUntil(newWarrantyUntil: String?) {
Timber.d("[DEBUG][ACTION][updating_item_warrantyUntil] Updating item warrantyUntil to: %s", newWarrantyUntil)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyUntil = newWarrantyUntil))
}
// [END_ENTITY: Function('updateWarrantyUntil')]
// [ENTITY: Function('updateParentId')]
/**
* @summary Updates the parent ID of the item in the UI state.
* @param newParentId The new parent ID for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateParentId(newParentId: String?) {
Timber.d("[DEBUG][ACTION][updating_item_parentId] Updating item parentId to: %s", newParentId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(parentId = newParentId))
}
// [END_ENTITY: Function('updateParentId')]
// [ENTITY: Function('updateIsArchived')]
/**
* @summary Updates the archived status of the item in the UI state.
* @param newIsArchived The new archived status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateIsArchived(newIsArchived: Boolean?) {
Timber.d("[DEBUG][ACTION][updating_item_isArchived] Updating item isArchived to: %b", newIsArchived)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(isArchived = newIsArchived))
}
// [END_ENTITY: Function('updateIsArchived')]
// [ENTITY: Function('updateInsured')]
/**
* @summary Updates the insured status of the item in the UI state.
* @param newInsured The new insured status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateInsured(newInsured: Boolean?) {
Timber.d("[DEBUG][ACTION][updating_item_insured] Updating item insured to: %b", newInsured)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(insured = newInsured))
}
// [END_ENTITY: Function('updateInsured')]
// [ENTITY: Function('updateLifetimeWarranty')]
/**
* @summary Updates the lifetime warranty status of the item in the UI state.
* @param newLifetimeWarranty The new lifetime warranty status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLifetimeWarranty(newLifetimeWarranty: Boolean?) {
Timber.d("[DEBUG][ACTION][updating_item_lifetimeWarranty] Updating item lifetimeWarranty to: %b", newLifetimeWarranty)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(lifetimeWarranty = newLifetimeWarranty))
}
// [END_ENTITY: Function('updateLifetimeWarranty')]
// [ENTITY: Function('updateManufacturer')]
/**
* @summary Updates the manufacturer of the item in the UI state.
* @param newManufacturer The new manufacturer for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateManufacturer(newManufacturer: String?) {
Timber.d("[DEBUG][ACTION][updating_item_manufacturer] Updating item manufacturer to: %s", newManufacturer)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(manufacturer = newManufacturer))
}
// [END_ENTITY: Function('updateManufacturer')]
// [ENTITY: Function('updateModelNumber')]
/**
* @summary Updates the model number of the item in the UI state.
* @param newModelNumber The new model number for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateModelNumber(newModelNumber: String?) {
Timber.d("[DEBUG][ACTION][updating_item_modelNumber] Updating item modelNumber to: %s", newModelNumber)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(modelNumber = newModelNumber))
}
// [END_ENTITY: Function('updateModelNumber')]
// [ENTITY: Function('updatePurchaseFrom')]
/**
* @summary Updates the purchase source of the item in the UI state.
* @param newPurchaseFrom The new purchase source for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updatePurchaseFrom(newPurchaseFrom: String?) {
Timber.d("[DEBUG][ACTION][updating_item_purchaseFrom] Updating item purchaseFrom to: %s", newPurchaseFrom)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(purchaseFrom = newPurchaseFrom))
}
// [END_ENTITY: Function('updatePurchaseFrom')]
// [ENTITY: Function('updateSoldNotes')]
/**
* @summary Updates the sold notes of the item in the UI state.
* @param newSoldNotes The new sold notes for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldNotes(newSoldNotes: String?) {
Timber.d("[DEBUG][ACTION][updating_item_soldNotes] Updating item soldNotes to: %s", newSoldNotes)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldNotes = newSoldNotes))
}
// [END_ENTITY: Function('updateSoldNotes')]
// [ENTITY: Function('updateSoldPrice')]
/**
* @summary Updates the sold price of the item in the UI state.
* @param newSoldPrice The new sold price for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldPrice(newSoldPrice: Double?) {
Timber.d("[DEBUG][ACTION][updating_item_soldPrice] Updating item soldPrice to: %f", newSoldPrice)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldPrice = newSoldPrice))
}
// [END_ENTITY: Function('updateSoldPrice')]
// [ENTITY: Function('updateSoldTime')]
/**
* @summary Updates the sold time of the item in the UI state.
* @param newSoldTime The new sold time for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTime(newSoldTime: String?) {
Timber.d("[DEBUG][ACTION][updating_item_soldTime] Updating item soldTime to: %s", newSoldTime)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTime = newSoldTime))
}
// [END_ENTITY: Function('updateSoldTime')]
// [ENTITY: Function('updateSoldTo')]
/**
* @summary Updates the sold to field of the item in the UI state.
* @param newSoldTo The new sold to field for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSoldTo(newSoldTo: String?) {
Timber.d("[DEBUG][ACTION][updating_item_soldTo] Updating item soldTo to: %s", newSoldTo)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(soldTo = newSoldTo))
}
// [END_ENTITY: Function('updateSoldTo')]
// [ENTITY: Function('updateSyncChildItemsLocations')]
/**
* @summary Updates the sync child items locations status of the item in the UI state.
* @param newSyncChildItemsLocations The new sync child items locations status for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateSyncChildItemsLocations(newSyncChildItemsLocations: Boolean?) {
Timber.d("[DEBUG][ACTION][updating_item_syncChildItemsLocations] Updating item syncChildItemsLocations to: %b", newSyncChildItemsLocations)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(syncChildItemsLocations = newSyncChildItemsLocations))
}
// [END_ENTITY: Function('updateSyncChildItemsLocations')]
// [ENTITY: Function('updateWarrantyDetails')]
/**
* @summary Updates the warranty details of the item in the UI state.
* @param newWarrantyDetails The new warranty details for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateWarrantyDetails(newWarrantyDetails: String?) {
Timber.d("[DEBUG][ACTION][updating_item_warrantyDetails] Updating item warrantyDetails to: %s", newWarrantyDetails)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(warrantyDetails = newWarrantyDetails))
}
// [END_ENTITY: Function('updateWarrantyDetails')]
// [ENTITY: Function('updateLocation')]
/**
* @summary Updates the location of the item in the UI state.
* @param newLocation The new location for the item.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun updateLocation(newLocation: Location?) {
Timber.d("[DEBUG][ACTION][updating_item_location] Updating item location to: %s", newLocation?.name)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(location = newLocation))
}
// [END_ENTITY: Function('updateLocation')]
// [ENTITY: Function('addLabel')]
/**
* @summary Adds a label to the item in the UI state.
* @param label The label to add.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun addLabel(label: Label) {
Timber.d("[DEBUG][ACTION][adding_label_to_item] Adding label: %s", label.name)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = _uiState.value.item?.labels.orEmpty() + label))
}
// [END_ENTITY: Function('addLabel')]
// [ENTITY: Function('removeLabel')]
/**
* @summary Removes a label from the item in the UI state.
* @param labelId The ID of the label to remove.
* @sideeffect Updates the `item` in `_uiState`.
*/
fun removeLabel(labelId: String) {
Timber.d("[DEBUG][ACTION][removing_label_from_item] Removing label with ID: %s", labelId)
_uiState.value = _uiState.value.copy(item = _uiState.value.item?.copy(labels = _uiState.value.item?.labels.orEmpty().filter { it.id != labelId }))
}
// [END_ENTITY: Function('removeLabel')]
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_FILE_ItemEditViewModel.kt]

View File

@@ -1,113 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
// [FILE] LabelEditScreen.kt
// [SEMANTICS] ui, screen, label, edit
package com.homebox.lens.ui.screen.labeledit
// [IMPORTS]
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.ui.components.ColorPicker
import com.homebox.lens.ui.components.LoadingOverlay
// [END_IMPORTS]
// [ENTITY: Function('LabelEditScreen')]
// [RELATION: Function('LabelEditScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelEditViewModel')]
/**
* @summary Composable-функция для экрана "Редактирование метки".
* @param labelId ID метки для редактирования или null для создания новой.
* @param onBack Навигация назад.
* @param onLabelSaved Действие после сохранения метки.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabelEditScreen(
labelId: String?,
onBack: () -> Unit,
onLabelSaved: () -> Unit,
viewModel: LabelEditViewModel = hiltViewModel()
) {
val uiState = viewModel.uiState
val snackbarHostState = SnackbarHostState()
LaunchedEffect(uiState.isSaved) {
if (uiState.isSaved) {
onLabelSaved()
}
}
LaunchedEffect(uiState.error) {
uiState.error?.let {
snackbarHostState.showSnackbar(
message = it,
actionLabel = "Dismiss",
duration = SnackbarDuration.Short
)
}
}
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) },
topBar = {
TopAppBar(
title = {
Text(
text = if (labelId == null) {
stringResource(id = R.string.label_edit_title_create)
} else {
stringResource(id = R.string.label_edit_title_edit)
}
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
}
},
actions = {
IconButton(onClick = viewModel::saveLabel) {
Icon(Icons.Default.Check, contentDescription = stringResource(R.string.save))
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
) {
OutlinedTextField(
value = uiState.name,
onValueChange = viewModel::onNameChange,
label = { Text(stringResource(R.string.label_name)) },
isError = uiState.nameError != null,
supportingText = { uiState.nameError?.let { Text(it) } },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
ColorPicker(
selectedColor = uiState.color,
onColorSelected = viewModel::onColorChange,
modifier = Modifier.fillMaxWidth()
)
}
if (uiState.isLoading) {
LoadingOverlay()
}
}
}
// [END_ENTITY: Function('LabelEditScreen')]
// [END_FILE_LabelEditScreen.kt]

View File

@@ -1,115 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labeledit
// [FILE] LabelEditViewModel.kt
// [SEMANTICS] ui, viewmodel, label_management
package com.homebox.lens.ui.screen.labeledit
// [IMPORTS]
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.LabelCreate
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LabelUpdate
import com.homebox.lens.domain.usecase.CreateLabelUseCase
import com.homebox.lens.domain.usecase.GetLabelDetailsUseCase
import com.homebox.lens.domain.usecase.UpdateLabelUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('LabelEditViewModel')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('GetLabelDetailsUseCase')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('CreateLabelUseCase')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [DEPENDS_ON] -> [UseCase('UpdateLabelUseCase')]
// [RELATION: ViewModel('LabelEditViewModel')] -> [EMITS_STATE] -> [DataClass('LabelEditUiState')]
@HiltViewModel
class LabelEditViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getLabelDetailsUseCase: GetLabelDetailsUseCase,
private val createLabelUseCase: CreateLabelUseCase,
private val updateLabelUseCase: UpdateLabelUseCase
) : ViewModel() {
var uiState by mutableStateOf(LabelEditUiState())
private set
private val labelId: String? = savedStateHandle["labelId"]
init {
if (labelId != null) {
loadLabelDetails(labelId)
}
}
fun onNameChange(newName: String) {
uiState = uiState.copy(name = newName, nameError = null)
}
fun onColorChange(newColor: String) {
uiState = uiState.copy(color = newColor)
}
fun saveLabel() {
viewModelScope.launch {
if (uiState.name.isBlank()) {
uiState = uiState.copy(nameError = "Label name cannot be empty.")
return@launch
}
uiState = uiState.copy(isLoading = true, error = null)
try {
if (labelId == null) {
// Create new label
val newLabel = LabelCreate(name = uiState.name, color = uiState.color)
createLabelUseCase(newLabel)
} else {
// Update existing label
val updatedLabel = LabelUpdate(name = uiState.name, color = uiState.color)
updateLabelUseCase(labelId, updatedLabel)
}
uiState = uiState.copy(isSaved = true)
} catch (e: Exception) {
uiState = uiState.copy(error = e.message, isLoading = false)
} finally {
uiState = uiState.copy(isLoading = false)
}
}
}
private fun loadLabelDetails(id: String) {
viewModelScope.launch {
uiState = uiState.copy(isLoading = true, error = null)
try {
val label = getLabelDetailsUseCase(id)
uiState = uiState.copy(
name = label.name,
color = label.color,
isLoading = false
)
} catch (e: Exception) {
uiState = uiState.copy(error = e.message, isLoading = false)
}
}
}
}
// [ENTITY: DataClass('LabelEditUiState')]
/**
* @summary Состояние UI для экрана редактирования метки.
*/
data class LabelEditUiState(
val name: String = "",
val color: String = "#FFFFFF", // Default color
val nameError: String? = null,
val isLoading: Boolean = false,
val error: String? = null,
val isSaved: Boolean = false,
val originalLabel: LabelOut? = null // To hold original label details if editing
)
// [END_ENTITY: DataClass('LabelEditUiState')]
// [END_FILE_LabelEditViewModel.kt]

View File

@@ -1,225 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt
// [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.material.icons.filled.Delete
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.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.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 com.homebox.lens.R
import com.homebox.lens.domain.model.Label
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.navigation.Screen
import com.homebox.lens.ui.common.MainScaffold
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Function('LabelsListScreen')]
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')]
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
/**
* @summary Отображает экран со списком всех меток.
* @param navController Контроллер навигации для перемещения между экранами.
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
*/
@Composable
fun LabelsListScreen(
currentRoute: String?,
navigationActions: NavigationActions,
viewModel: LabelsListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
MainScaffold(
topBarTitle = stringResource(id = R.string.screen_title_labels),
currentRoute = currentRoute,
navigationActions = navigationActions
) { paddingValues ->
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = {
Timber.i("[INFO][ACTION][navigate_to_label_edit] FAB clicked: Navigate to create new label screen.")
navigationActions.navigateToLabelEdit(null)
}) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(id = R.string.content_desc_create_label)
)
}
}
) { innerPaddingValues ->
val currentState = uiState
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPaddingValues), // Use innerPaddingValues here
contentAlignment = Alignment.Center
) {
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.no_labels_found))
} else {
LabelsList(
labels = currentState.labels,
onLabelClick = { label ->
Timber.i("[INFO][ACTION][navigate_to_label_edit] Label clicked: ${label.id}. Navigating to label edit screen.")
navigationActions.navigateToLabelEdit(label.id)
},
onDeleteClick = { label ->
viewModel.onShowDeleteDialog(label)
},
isShowingDeleteDialog = currentState.isShowingDeleteDialog,
labelToDelete = currentState.labelToDelete,
onConfirmDelete = {
currentState.labelToDelete?.let { label ->
viewModel.deleteLabel(label.id)
}
},
onDismissDeleteDialog = {
viewModel.onDismissDeleteDialog()
}
)
}
// Delete confirmation dialog
if (currentState is LabelsListUiState.Success && currentState.isShowingDeleteDialog && currentState.labelToDelete != null) {
AlertDialog(
onDismissRequest = { viewModel.onDismissDeleteDialog() },
title = { Text("Delete Label") },
text = { Text("Are you sure you want to delete the label '${currentState.labelToDelete!!.name}'? This action cannot be undone.") },
confirmButton = {
TextButton(
onClick = {
viewModel.deleteLabel(currentState.labelToDelete!!.id)
viewModel.onDismissDeleteDialog()
}
) {
Text("Delete")
}
},
dismissButton = {
TextButton(
onClick = { viewModel.onDismissDeleteDialog() }
) {
Text("Cancel")
}
}
)
}
}
}
}
}
}
}
// [END_ENTITY: Function('LabelsListScreen')]
// [ENTITY: Function('LabelsList')]
// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')]
/**
* @summary Composable-функция для отображения списка меток.
* @param labels Список объектов `Label` для отображения.
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
* @param modifier Модификатор для настройки внешнего вида.
*/
@Composable
private fun LabelsList(
labels: List<Label>,
onLabelClick: (Label) -> Unit,
onDeleteClick: (Label) -> Unit,
isShowingDeleteDialog: Boolean,
labelToDelete: Label?,
onConfirmDelete: () -> Unit,
onDismissDeleteDialog: () -> Unit,
modifier: Modifier = Modifier
) {
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) },
onDeleteClick = { onDeleteClick(label) }
)
}
}
}
// [END_ENTITY: Function('LabelsList')]
// [ENTITY: Function('LabelListItem')]
// [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')]
/**
* @summary Composable-функция для отображения одного элемента в списке меток.
* @param label Объект `Label`, который нужно отобразить.
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
*/
@Composable
private fun LabelListItem(
label: Label,
onClick: () -> Unit,
onDeleteClick: () -> Unit
) {
ListItem(
headlineContent = { Text(text = label.name) },
leadingContent = {
Icon(
imageVector = Icons.AutoMirrored.Filled.Label,
contentDescription = stringResource(id = R.string.content_desc_label_icon)
)
},
trailingContent = {
IconButton(onClick = onDeleteClick) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(id = R.string.content_desc_delete_label)
)
}
},
modifier = Modifier.clickable(onClick = onClick)
)
}
// [END_ENTITY: Function('LabelListItem')]
// [END_FILE_LabelsListScreen.kt]

View File

@@ -1,50 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListUiState.kt
// [SEMANTICS] ui_state, sealed_interface, contract
package com.homebox.lens.ui.screen.labelslist
// [IMPORTS]
import com.homebox.lens.domain.model.Label
// [END_IMPORTS]
// [ENTITY: SealedInterface('LabelsListUiState')]
/**
* @summary Определяет все возможные состояния для UI экрана со списком меток.
* @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
*/
sealed interface LabelsListUiState {
// [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')]
/**
* @summary Состояние успеха, содержит список меток и состояние диалога.
* @param labels Список меток для отображения.
* @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
* @invariant labels не может быть null.
*/
data class Success(
val labels: List<Label>,
val isShowingCreateDialog: Boolean = false,
val isShowingDeleteDialog: Boolean = false,
val labelToDelete: Label? = null
) : LabelsListUiState
// [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')]
/**
* @summary Состояние ошибки.
* @param message Текст ошибки для отображения пользователю.
* @invariant message не может быть пустой.
*/
data class Error(val message: String) : LabelsListUiState
// [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')]
/**
* @summary Состояние загрузки данных.
* @description Указывает, что идет процесс загрузки меток.
*/
data object Loading : LabelsListUiState
// [END_ENTITY: Object('Loading')]
}
// [END_ENTITY: SealedInterface('LabelsListUiState')]
// [END_FILE_LabelsListUiState.kt]

View File

@@ -1,149 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt
// [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.DeleteLabelUseCase
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
// [END_IMPORTS]
// [ENTITY: ViewModel('LabelsListViewModel')]
// [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
// [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')]
/**
* @summary ViewModel для экрана со списком меток.
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
*/
@HiltViewModel
class LabelsListViewModel @Inject constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val deleteLabelUseCase: DeleteLabelUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
val uiState = _uiState.asStateFlow()
init {
loadLabels()
}
// [ENTITY: Function('loadLabels')]
/**
* @summary Загружает список меток.
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error`.
* @sideeffect Асинхронно обновляет `_uiState`.
*/
fun loadLabels() {
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[INFO][ENTRYPOINT][loading_labels] Starting labels list load. State -> Loading.")
val result = runCatching {
getAllLabelsUseCase()
}
result.fold(
onSuccess = { labelOuts ->
Timber.i("[INFO][SUCCESS][labels_loaded] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
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][EXCEPTION][loading_failed] Failed to load labels. State -> Error.")
_uiState.value = LabelsListUiState.Error(
message = exception.message ?: "Could not load labels."
)
}
)
}
}
// [END_ENTITY: Function('loadLabels')]
// [ENTITY: Function('onShowDeleteDialog')]
/**
* @summary Показывает диалог подтверждения удаления метки.
* @param label Метка для удаления.
* @sideeffect Обновляет состояние для показа диалога удаления.
*/
fun onShowDeleteDialog(label: Label) {
Timber.i("[INFO][ACTION][show_delete_dialog] Show delete label dialog for: ${label.id}")
if (_uiState.value is LabelsListUiState.Success) {
_uiState.update { currentState ->
(currentState as LabelsListUiState.Success).copy(
isShowingDeleteDialog = true,
labelToDelete = label
)
}
}
}
// [END_ENTITY: Function('onShowDeleteDialog')]
// [ENTITY: Function('onDismissDeleteDialog')]
/**
* @summary Скрывает диалог подтверждения удаления метки.
* @sideeffect Обновляет состояние для скрытия диалога удаления.
*/
fun onDismissDeleteDialog() {
Timber.i("[INFO][ACTION][dismiss_delete_dialog] Dismiss delete label dialog")
if (_uiState.value is LabelsListUiState.Success) {
_uiState.update { currentState ->
(currentState as LabelsListUiState.Success).copy(
isShowingDeleteDialog = false,
labelToDelete = null
)
}
}
}
// [END_ENTITY: Function('onDismissDeleteDialog')]
// [ENTITY: Function('deleteLabel')]
/**
* @summary Удаляет выбранную метку.
* @param labelId ID метки для удаления.
* @sideeffect Выполняет удаление через UseCase, обновляет состояние UI.
*/
fun deleteLabel(labelId: String) {
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[INFO][ENTRYPOINT][deleting_label] Starting label deletion for ID: $labelId. State -> Loading.")
val result = runCatching {
deleteLabelUseCase(labelId)
}
result.fold(
onSuccess = {
Timber.i("[INFO][SUCCESS][label_deleted] Label deleted successfully. Reloading labels.")
loadLabels() // Refresh the list
},
onFailure = { exception ->
Timber.e(exception, "[ERROR][EXCEPTION][deletion_failed] Failed to delete label. State -> Error.")
_uiState.value = LabelsListUiState.Error(
message = exception.message ?: "Could not delete label."
)
}
)
}
}
// [END_ENTITY: Function('deleteLabel')]
}
// [END_ENTITY: ViewModel('LabelsListViewModel')]
// [END_FILE_LabelsListViewModel.kt]

View File

@@ -1,48 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationedit
// [FILE] LocationEditScreen.kt
// [SEMANTICS] ui, screen, location, edit
package com.homebox.lens.ui.screen.locationedit
// [IMPORTS]
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
// [END_IMPORTS]
// [ENTITY: Function('LocationEditScreen')]
/**
* @summary Composable-функция для экрана "Редактирование местоположения".
* @param locationId ID местоположения для редактирования или "new" для создания.
*/
@Composable
fun LocationEditScreen(
locationId: String?
) {
val title = if (locationId == "new") {
stringResource(id = R.string.location_edit_title_create)
} else {
stringResource(id = R.string.location_edit_title_edit)
}
Scaffold { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
// [AI_NOTE]: Implement Location Edit Screen UI
Text(text = "Location Edit Screen for ID: $locationId")
}
}
}
// [END_ENTITY: Function('LocationEditScreen')]
// [END_FILE_LocationEditScreen.kt]

View File

@@ -1,296 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt
// [SEMANTICS] ui, screen, locations, list
package com.homebox.lens.ui.screen.locationslist
// [IMPORTS]
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
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.MoreVert
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme
// [END_IMPORTS]
// [ENTITY: Function('LocationsListScreen')]
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LocationsListViewModel')]
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Список местоположений".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onAddNewLocationClick Лямбда-обработчик нажатия на кнопку добавления нового местоположения.
* @param viewModel ViewModel для этого экрана.
*/
@Composable
fun LocationsListScreen(
currentRoute: String?,
navigationActions: NavigationActions,
onLocationClick: (String) -> Unit,
onAddNewLocationClick: () -> Unit,
viewModel: LocationsListViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
MainScaffold(
topBarTitle = stringResource(id = R.string.locations_list_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) { paddingValues ->
Scaffold(
modifier = Modifier.padding(paddingValues),
floatingActionButton = {
FloatingActionButton(onClick = onAddNewLocationClick) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(id = R.string.cd_add_new_location)
)
}
}
) { innerPadding ->
LocationsListContent(
modifier = Modifier.padding(innerPadding),
uiState = uiState,
onLocationClick = onLocationClick,
onEditLocation = { /* [AI_NOTE]: Implement onEditLocation */ },
onDeleteLocation = { /* [AI_NOTE]: Implement onDeleteLocation */ }
)
}
}
}
// [END_ENTITY: Function('LocationsListScreen')]
// [ENTITY: Function('LocationsListContent')]
// [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')]
/**
* @summary Отображает основной контент экрана в зависимости от `uiState`.
* @param modifier Модификатор для стилизации.
* @param uiState Текущее состояние UI.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onEditLocation Лямбда-обработчик для редактирования местоположения.
* @param onDeleteLocation Лямбда-обработчик для удаления местоположения.
*/
@Composable
private fun LocationsListContent(
modifier: Modifier = Modifier,
uiState: LocationsListUiState,
onLocationClick: (String) -> Unit,
onEditLocation: (String) -> Unit,
onDeleteLocation: (String) -> Unit
) {
Box(modifier = modifier.fillMaxSize()) {
when (uiState) {
is LocationsListUiState.Loading -> {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
is LocationsListUiState.Error -> {
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
}
is LocationsListUiState.Success -> {
if (uiState.locations.isEmpty()) {
Text(
text = stringResource(id = R.string.locations_not_found),
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
} else {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(uiState.locations, key = { it.id }) { location ->
LocationCard(
location = location,
onClick = { onLocationClick(location.id) },
onEditClick = { onEditLocation(location.id) },
onDeleteClick = { onDeleteLocation(location.id) }
)
}
}
}
}
}
}
}
// [END_ENTITY: Function('LocationsListContent')]
// [ENTITY: Function('LocationCard')]
// [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/**
* @summary Карточка для отображения одного местоположения.
* @param location Данные о местоположении.
* @param onClick Лямбда-обработчик нажатия на карточку.
* @param onEditClick Лямбда-обработчик нажатия на "Редактировать".
* @param onDeleteClick Лямбда-обработчик нажатия на "Удалить".
*/
@Composable
private fun LocationCard(
location: LocationOutCount,
onClick: () -> Unit,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit
) {
var menuExpanded by remember { mutableStateOf(false) }
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
) {
Row(
modifier = Modifier.padding(start = 16.dp, end = 8.dp, top = 16.dp, bottom = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = location.name, style = MaterialTheme.typography.titleMedium)
Text(
text = stringResource(id = R.string.item_count, location.itemCount),
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(Modifier.width(16.dp))
Box {
IconButton(onClick = { menuExpanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.cd_more_options))
}
DropdownMenu(
expanded = menuExpanded,
onDismissRequest = { menuExpanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.edit)) },
onClick = {
menuExpanded = false
onEditClick()
}
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.delete)) },
onClick = {
menuExpanded = false
onDeleteClick()
}
)
}
}
}
}
}
// [END_ENTITY: Function('LocationCard')]
// [ENTITY: Function('LocationsListSuccessPreview')]
@Preview(showBackground = true, name = "Locations List Success")
@Composable
fun LocationsListSuccessPreview() {
val previewLocations = listOf(
LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""),
LocationOutCount("2", "Kitchen", "#00FF00", false, 5, "", ""),
LocationOutCount("3", "Office", "#0000FF", false, 23, "", "")
)
HomeboxLensTheme {
LocationsListContent(
uiState = LocationsListUiState.Success(previewLocations),
onLocationClick = {},
onEditLocation = {},
onDeleteLocation = {}
)
}
}
// [END_ENTITY: Function('LocationsListSuccessPreview')]
// [ENTITY: Function('LocationsListEmptyPreview')]
@Preview(showBackground = true, name = "Locations List Empty")
@Composable
fun LocationsListEmptyPreview() {
HomeboxLensTheme {
LocationsListContent(
uiState = LocationsListUiState.Success(emptyList()),
onLocationClick = {},
onEditLocation = {},
onDeleteLocation = {}
)
}
}
// [END_ENTITY: Function('LocationsListEmptyPreview')]
// [ENTITY: Function('LocationsListLoadingPreview')]
@Preview(showBackground = true, name = "Locations List Loading")
@Composable
fun LocationsListLoadingPreview() {
HomeboxLensTheme {
LocationsListContent(
uiState = LocationsListUiState.Loading,
onLocationClick = {},
onEditLocation = {},
onDeleteLocation = {}
)
}
}
// [END_ENTITY: Function('LocationsListLoadingPreview')]
// [ENTITY: Function('LocationsListErrorPreview')]
@Preview(showBackground = true, name = "Locations List Error")
@Composable
fun LocationsListErrorPreview() {
HomeboxLensTheme {
LocationsListContent(
uiState = LocationsListUiState.Error("Failed to load locations. Please try again."),
onLocationClick = {},
onEditLocation = {},
onDeleteLocation = {}
)
}
}
// [END_ENTITY: Function('LocationsListErrorPreview')]
// [END_FILE_LocationsListScreen.kt]

View File

@@ -1,42 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListUiState.kt
// [SEMANTICS] ui, state, locations
package com.homebox.lens.ui.screen.locationslist
// [IMPORTS]
import com.homebox.lens.domain.model.LocationOutCount
// [END_IMPORTS]
// [ENTITY: SealedInterface('LocationsListUiState')]
/**
* @summary Определяет возможные состояния UI для экрана списка местоположений.
* @see LocationsListViewModel
*/
sealed interface LocationsListUiState {
// [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/**
* @summary Состояние успешной загрузки данных.
* @param locations Список местоположений для отображения.
*/
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
// [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')]
/**
* @summary Состояние ошибки.
* @param message Сообщение об ошибке.
*/
data class Error(val message: String) : LocationsListUiState
// [END_ENTITY: DataClass('Error')]
// [ENTITY: Object('Loading')]
/**
* @summary Состояние загрузки данных.
*/
object Loading : LocationsListUiState
// [END_ENTITY: Object('Loading')]
}
// [END_ENTITY: SealedInterface('LocationsListUiState')]
// [END_FILE_LocationsListUiState.kt]

View File

@@ -1,64 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt
// [SEMANTICS] ui, viewmodel, locations, hilt
package com.homebox.lens.ui.screen.locationslist
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('LocationsListViewModel')]
// [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
// [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')]
/**
* @summary ViewModel для экрана списка местоположений.
* @param getAllLocationsUseCase Use case для получения всех местоположений.
* @property uiState Поток, содержащий текущее состояние UI.
* @invariant `uiState` всегда отражает результат последней операции загрузки.
*/
@HiltViewModel
class LocationsListViewModel @Inject constructor(
private val getAllLocationsUseCase: GetAllLocationsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
init {
loadLocations()
}
// [ENTITY: Function('loadLocations')]
/**
* @summary Загружает список местоположений из репозитория.
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
*/
fun loadLocations() {
Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.")
viewModelScope.launch {
_uiState.value = LocationsListUiState.Loading
try {
Timber.d("[DEBUG][ACTION][fetching_locations] Fetching locations from use case.")
val locations = getAllLocationsUseCase()
_uiState.value = LocationsListUiState.Success(locations)
Timber.d("[DEBUG][SUCCESS][locations_loaded] Successfully loaded locations.")
} catch (e: Exception) {
Timber.e(e, "[ERROR][EXCEPTION][loading_failed] Failed to load locations.")
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
}
}
}
// [END_ENTITY: Function('loadLocations')]
}
// [END_ENTITY: ViewModel('LocationsListViewModel')]
// [END_FILE_LocationsListViewModel.kt]

View File

@@ -1,39 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt
// [SEMANTICS] ui, screen, search
package com.homebox.lens.ui.screen.search
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('SearchScreen')]
// [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
// [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Composable-функция для экрана "Поиск".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
*/
@Composable
fun SearchScreen(
currentRoute: String?,
navigationActions: NavigationActions
) {
MainScaffold(
topBarTitle = stringResource(id = R.string.search_title),
currentRoute = currentRoute,
navigationActions = navigationActions
) {
// [AI_NOTE]: Implement Search Screen UI
Text(text = "Search Screen")
}
}
// [END_ENTITY: Function('SearchScreen')]
// [END_FILE_SearchScreen.kt]

View File

@@ -1,21 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchViewModel.kt
// [SEMANTICS] ui, viewmodel, search
package com.homebox.lens.ui.screen.search
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('SearchViewModel')]
/**
* @summary ViewModel for the search screen.
*/
@HiltViewModel
class SearchViewModel @Inject constructor() : ViewModel() {
// [AI_NOTE]: Implement UI state
}
// [END_ENTITY: ViewModel('SearchViewModel')]
// [END_FILE_SearchViewModel.kt]

View File

@@ -1,104 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.settings
// [FILE] SettingsScreen.kt
// [SEMANTICS] ui, screen, settings, compose
package com.homebox.lens.ui.screen.settings
// [IMPORTS]
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.navigation.Screen
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.screen.settings.SettingsUiState
import com.homebox.lens.ui.screen.settings.SettingsViewModel
import com.homebox.lens.ui.common.MainScaffold
// [END_IMPORTS]
// [ENTITY: Function('SettingsScreen')]
/**
* @summary Composable function for the Settings screen.
* @param viewModel The ViewModel for the Settings screen.
* @param onNavigateUp Callback to navigate up in the navigation stack.
* @sideeffect Collects UI state from ViewModel.
*/
@Composable
fun SettingsScreen(
currentRoute: String?,
navigationActions: NavigationActions,
viewModel: SettingsViewModel = hiltViewModel(),
onNavigateUp: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
MainScaffold(
topBarTitle = "Настройки",
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateUp = onNavigateUp
) { paddingValues ->
SettingsContent(
modifier = Modifier.padding(paddingValues),
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onSaveClick = viewModel::saveSettings
)
}
}
// [END_ENTITY: Function('SettingsScreen')]
// [ENTITY: Function('SettingsContent')]
/**
* @summary Composable function for the content of the Settings screen.
* @param modifier Modifier for the layout.
* @param uiState The current UI state of the settings.
* @param onServerUrlChange Callback for server URL changes.
* @param onSaveClick Callback for save button clicks.
* @sideeffect Displays UI elements based on uiState.
*/
@Composable
fun SettingsContent(
modifier: Modifier = Modifier,
uiState: SettingsUiState,
onServerUrlChange: (String) -> Unit,
onSaveClick: () -> Unit
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text("URL Сервера") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSaveClick,
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text("Сохранить")
}
}
if (uiState.isSaved) {
Text("Настройки сохранены!", color = MaterialTheme.colorScheme.primary)
}
if (uiState.error != null) {
Text(uiState.error, color = MaterialTheme.colorScheme.error)
}
}
}
// [END_ENTITY: Function('SettingsContent')]
// [END_FILE_SettingsScreen.kt]

View File

@@ -1,8 +0,0 @@
package com.homebox.lens.ui.screen.settings
data class SettingsUiState(
val serverUrl: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val isSaved: Boolean = false
)

View File

@@ -1,54 +0,0 @@
package com.homebox.lens.ui.screen.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.model.Credentials
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val credentialsRepository: CredentialsRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState = _uiState.asStateFlow()
init {
loadCurrentSettings()
}
private fun loadCurrentSettings() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
val credentials = credentialsRepository.getCredentials().first()
_uiState.value = _uiState.value.copy(
serverUrl = credentials?.serverUrl ?: "",
isLoading = false
)
}
}
fun onServerUrlChange(newUrl: String) {
_uiState.value = _uiState.value.copy(serverUrl = newUrl, isSaved = false)
}
fun saveSettings() {
Timber.i("[INFO][ACTION][settings_save] Attempting to save settings.")
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
val currentCredentials = credentialsRepository.getCredentials().first()
val updatedCredentials = currentCredentials?.copy(serverUrl = _uiState.value.serverUrl)
?: Credentials(serverUrl = _uiState.value.serverUrl, username = "", password = "") // Create new if no existing credentials
credentialsRepository.saveCredentials(updatedCredentials)
_uiState.value = _uiState.value.copy(isLoading = false, isSaved = true)
}
}
}

View File

@@ -1,141 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
// [SEMANTICS] ui, screen, setup, compose
@file:OptIn(ExperimentalMaterial3Api::class)
package com.homebox.lens.ui.screen.setup
// [IMPORTS]
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
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.text.input.PasswordVisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.homebox.lens.R
// [END_IMPORTS]
// [ENTITY: Function('SetupScreen')]
// [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
// [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')]
/**
* @summary Главная Composable-функция для экрана настройки соединения с сервером.
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
* @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`.
*/
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
if (uiState.isSetupComplete) {
onSetupComplete()
}
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect
)
}
// [END_ENTITY: Function('SetupScreen')]
// [ENTITY: Function('SetupScreenContent')]
// [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')]
/**
* @summary Отображает контент экрана настройки: поля ввода и кнопку.
* @param uiState Текущее состояние UI.
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
* @param onUsernameChange Лямбда-обработчик изменения имени пользователя.
* @param onPasswordChange Лямбда-обработчик изменения пароля.
* @param onConnectClick Лямбда-обработчик нажатия на кнопку "Подключиться".
*/
@Composable
private fun SetupScreenContent(
uiState: SetupUiState,
onServerUrlChange: (String) -> Unit,
onUsernameChange: (String) -> Unit,
onPasswordChange: (String) -> Unit,
onConnectClick: () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(title = { Text(stringResource(id = R.string.setup_title)) })
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text(stringResource(id = R.string.setup_username_label)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.password,
onValueChange = onPasswordChange,
label = { Text(stringResource(id = R.string.setup_password_label)) },
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onConnectClick,
enabled = !uiState.isLoading,
modifier = Modifier.fillMaxWidth()
) {
if (uiState.isLoading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
} else {
Text(stringResource(id = R.string.setup_connect_button))
}
}
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
}
}
}
}
// [END_ENTITY: Function('SetupScreenContent')]
// [ENTITY: Function('SetupScreenPreview')]
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {}
)
}
// [END_ENTITY: Function('SetupScreenPreview')]
// [END_FILE_SetupScreen.kt]

View File

@@ -1,27 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupUiState.kt
// [SEMANTICS] ui_state, data_model, immutable
package com.homebox.lens.ui.screen.setup
// [ENTITY: DataClass('SetupUiState')]
/**
* @summary Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
* @description Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
* @param serverUrl URL-адрес сервера Homebox.
* @param username Имя пользователя для входа.
* @param password Пароль пользователя.
* @param isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
* @param error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
* @param isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
*/
data class SetupUiState(
val serverUrl: String = "",
val username: String = "",
val password: String = "",
val isLoading: Boolean = false,
val error: String? = null,
val isSetupComplete: Boolean = false
)
// [END_ENTITY: DataClass('SetupUiState')]
// [END_FILE_SetupUiState.kt]

View File

@@ -1,113 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupViewModel.kt
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
package com.homebox.lens.ui.screen.setup
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.model.Credentials
import com.homebox.lens.domain.repository.CredentialsRepository
import com.homebox.lens.domain.usecase.LoginUseCase
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
// [END_IMPORTS]
// [ENTITY: ViewModel('SetupViewModel')]
// [RELATION: ViewModel('SetupViewModel')] -> [DEPENDS_ON] -> [Repository('CredentialsRepository')]
// [RELATION: ViewModel('SetupViewModel')] -> [DEPENDS_ON] -> [UseCase('LoginUseCase')]
// [RELATION: ViewModel('SetupViewModel')] -> [EMITS_STATE] -> [DataClass('SetupUiState')]
/**
* @summary ViewModel для экрана первоначальной настройки (Setup).
* @param credentialsRepository Репозиторий для операций с учетными данными.
* @param loginUseCase Use case для выполнения логики входа.
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
*/
@HiltViewModel
class SetupViewModel @Inject constructor(
private val credentialsRepository: CredentialsRepository,
private val loginUseCase: LoginUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(SetupUiState())
val uiState = _uiState.asStateFlow()
init {
loadCredentials()
}
// [ENTITY: Function('loadCredentials')]
private fun loadCredentials() {
Timber.d("[DEBUG][ENTRYPOINT][loading_credentials] Loading credentials from repository.")
viewModelScope.launch {
credentialsRepository.getCredentials().collect { credentials ->
if (credentials != null) {
Timber.d("[DEBUG][ACTION][updating_state] Credentials found, updating UI state.")
_uiState.update {
it.copy(
serverUrl = credentials.serverUrl,
username = credentials.username,
password = credentials.password
)
}
}
}
}
}
// [END_ENTITY: Function('loadCredentials')]
// [ENTITY: Function('onServerUrlChange')]
fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
}
// [END_ENTITY: Function('onServerUrlChange')]
// [ENTITY: Function('onUsernameChange')]
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
// [END_ENTITY: Function('onUsernameChange')]
// [ENTITY: Function('onPasswordChange')]
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
// [END_ENTITY: Function('onPasswordChange')]
// [ENTITY: Function('connect')]
fun connect() {
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, error = null) }
val credentials = Credentials(
serverUrl = _uiState.value.serverUrl.trim(),
username = _uiState.value.username.trim(),
password = _uiState.value.password
)
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
credentialsRepository.saveCredentials(credentials)
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
loginUseCase(credentials).fold(
onSuccess = {
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
},
onFailure = { exception ->
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
}
)
}
}
// [END_ENTITY: Function('connect')]
}
// [END_ENTITY: ViewModel('SetupViewModel')]
// [END_FILE_SetupViewModel.kt]

View File

@@ -1,74 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt
// [SEMANTICS] ui, theme
package com.homebox.lens.ui.theme
// [IMPORTS]
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
// [END_IMPORTS]
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
)
// [ENTITY: Function('HomeboxLensTheme')]
// [RELATION: Function('HomeboxLensTheme')] -> [DEPENDS_ON] -> [DataStructure('Typography')]
/**
* @summary The main theme for the Homebox Lens application.
* @param darkTheme Whether the theme should be dark or light.
* @param dynamicColor Whether to use dynamic color (on Android 12+).
* @param content The content to be displayed within the theme.
*/
@Composable
fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
// [END_ENTITY: Function('HomeboxLensTheme')]
// [END_FILE_Theme.kt]

View File

@@ -1,29 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Typography.kt
// [SEMANTICS] ui, theme, typography
package com.homebox.lens.ui.theme
// [IMPORTS]
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// [END_IMPORTS]
// [ENTITY: DataStructure('Typography')]
/**
* @summary Defines the typography for the application.
*/
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
// [END_ENTITY: DataStructure('Typography')]
// [END_FILE_Typography.kt]

View File

@@ -1,239 +0,0 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditViewModelTest.kt
// [SEMANTICS] ui, viewmodel, testing
package com.homebox.lens.ui.screen.itemedit
import app.cash.turbine.test
import com.homebox.lens.domain.model.Item
import com.homebox.lens.domain.model.ItemCreate
import com.homebox.lens.domain.model.ItemOut
import com.homebox.lens.domain.model.ItemSummary
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.domain.usecase.CreateItemUseCase
import com.homebox.lens.domain.usecase.GetItemDetailsUseCase
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import com.homebox.lens.domain.usecase.UpdateItemUseCase
import io.mockk.coEvery
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.UUID
@ExperimentalCoroutinesApi
class ItemEditViewModelTest {
private val testDispatcher = StandardTestDispatcher()
private lateinit var createItemUseCase: CreateItemUseCase
private lateinit var updateItemUseCase: UpdateItemUseCase
private lateinit var getItemDetailsUseCase: GetItemDetailsUseCase
private lateinit var getAllLocationsUseCase: GetAllLocationsUseCase
private lateinit var viewModel: ItemEditViewModel
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
createItemUseCase = mockk()
updateItemUseCase = mockk()
getItemDetailsUseCase = mockk()
getAllLocationsUseCase = mockk<GetAllLocationsUseCase>()
coEvery { getAllLocationsUseCase() } returns listOf(
LocationOutCount("1", "Test Location", "#000000", false, 0, "2025-08-28T12:00:00Z", "2025-08-28T12:00:00Z")
)
viewModel = ItemEditViewModel(createItemUseCase, updateItemUseCase, getItemDetailsUseCase, getAllLocationsUseCase)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loadItem with valid id should update uiState with item`() = runTest {
val itemId = UUID.randomUUID().toString()
val itemOut = ItemOut(
id = itemId,
name = "Test Item",
assetId = null,
description = "Description",
notes = null,
serialNumber = null,
quantity = 1,
isArchived = false,
value = 10.0,
purchasePrice = null,
purchaseDate = null,
warrantyUntil = null,
location = null,
parent = null,
children = emptyList(),
labels = emptyList(),
attachments = emptyList(),
images = emptyList(),
fields = emptyList(),
maintenance = emptyList(),
createdAt = "2025-08-28T12:00:00Z",
updatedAt = "2025-08-28T12:00:00Z",
insured = null,
lifetimeWarranty = null,
manufacturer = null,
modelNumber = null,
purchaseFrom = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = null,
warrantyDetails = null
)
coEvery { getItemDetailsUseCase(itemId) } returns itemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Test Item", uiState.item?.name)
}
@Test
fun `loadItem with null id should prepare a new item`() = runTest {
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals("", uiState.item?.id)
assertEquals("", uiState.item?.name)
}
@Test
fun `saveItem should call createItemUseCase for new item`() = runTest {
val createdItemSummary = ItemSummary(id = UUID.randomUUID().toString(), name = "New Item", assetId = null, image = null, isArchived = false, labels = emptyList(), location = null, value = 0.0, createdAt = "2025-08-28T12:00:00Z", updatedAt = "2025-08-28T12:00:00Z")
coEvery { createItemUseCase(any()) } returns createdItemSummary
viewModel.loadItem(null)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("New Item")
viewModel.updateDescription("New Description")
viewModel.updateQuantity(2)
viewModel.updateSelectedLocationId("1")
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(createdItemSummary.id, uiState.item?.id)
}
@Test
fun `saveItem should call updateItemUseCase for existing item`() = runTest {
val itemId = UUID.randomUUID().toString()
val updatedItemOut = ItemOut(
id = itemId,
name = "Updated Item",
assetId = null,
description = "Updated Description",
notes = null,
serialNumber = null,
quantity = 4,
isArchived = false,
value = 12.0,
purchasePrice = null,
purchaseDate = null,
warrantyUntil = null,
location = null,
parent = null,
children = emptyList(),
labels = emptyList(),
attachments = emptyList(),
images = emptyList(),
fields = emptyList(),
maintenance = emptyList(),
createdAt = "2025-08-28T12:00:00Z",
updatedAt = "2025-08-28T12:00:00Z",
insured = null,
lifetimeWarranty = null,
manufacturer = null,
modelNumber = null,
purchaseFrom = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = null,
warrantyDetails = null
)
coEvery { getItemDetailsUseCase(itemId) } returns ItemOut(
id = itemId,
name = "Existing Item",
assetId = null,
description = "Existing Description",
notes = null,
serialNumber = null,
quantity = 3,
isArchived = false,
value = 10.0,
purchasePrice = null,
purchaseDate = null,
warrantyUntil = null,
location = null,
parent = null,
children = emptyList(),
labels = emptyList(),
attachments = emptyList(),
images = emptyList(),
fields = emptyList(),
maintenance = emptyList(),
createdAt = "2025-08-28T12:00:00Z",
updatedAt = "2025-08-28T12:00:00Z",
insured = null,
lifetimeWarranty = null,
manufacturer = null,
modelNumber = null,
purchaseFrom = null,
soldNotes = null,
soldPrice = null,
soldTime = null,
soldTo = null,
syncChildItemsLocations = null,
warrantyDetails = null
)
coEvery { updateItemUseCase(any()) } returns updatedItemOut
viewModel.loadItem(itemId)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.updateName("Updated Item")
viewModel.updateDescription("Updated Description")
viewModel.updateQuantity(4)
testDispatcher.scheduler.advanceUntilIdle()
viewModel.saveItem()
testDispatcher.scheduler.advanceUntilIdle()
val uiState = viewModel.uiState.value
assertFalse(uiState.isLoading)
assertNotNull(uiState.item)
assertEquals(itemId, uiState.item?.id)
assertEquals("Updated Item", uiState.item?.name)
assertEquals(4, uiState.item?.quantity)
}
}

View File

@@ -4,27 +4,27 @@
// [ENTITY: Object('Versions')] // [ENTITY: Object('Versions')]
object Versions { object Versions {
const val compileSdk = 36 const val compileSdk = 34
const val minSdk = 26 const val minSdk = 24
const val targetSdk = 36 const val targetSdk = 34
const val versionCode = 1 const val versionCode = 1
const val versionName = "1.0" const val versionName = "1.0"
const val kotlin = "2.0.0" const val kotlin = "1.9.10"
const val coroutines = "1.7.3" const val coroutines = "1.7.3"
const val composeCompiler = "1.5.8" // this is not used anymore const val composeCompiler = "1.5.4"
const val composeBom = "2023.10.01" // this is not used anymore const val composeBom = "2024.05.00"
const val activityCompose = "1.11.0" const val activityCompose = "1.8.2"
const val navigationCompose = "2.9.4" const val navigationCompose = "2.7.7"
const val hiltNavigationCompose = "1.3.0" const val hiltNavigationCompose = "1.1.0"
const val coreKtx = "1.12.0" const val coreKtx = "1.12.0"
const val lifecycle = "2.6.2" const val lifecycle = "2.7.0"
const val appcompat = "1.6.1" const val appcompat = "1.6.1"
const val retrofit = "2.9.0" const val retrofit = "2.9.0"
const val okhttp = "4.12.0" const val okhttp = "4.12.0"
const val moshi = "1.15.0" const val moshi = "1.15.1"
const val room = "2.6.1" const val room = "2.6.1"
const val hilt = "2.51.1" const val hilt = "2.51.1"
const val hiltCompiler = "1.1.0" const val hiltCompiler = "1.2.0"
const val timber = "5.0.1" const val timber = "5.0.1"
const val junit = "4.13.2" const val junit = "4.13.2"
const val extJunit = "1.1.5" const val extJunit = "1.1.5"
@@ -41,10 +41,13 @@ object Libs {
const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}" const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}" const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}" const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
const val composeUi = "androidx.compose.ui:ui:1.9.1" const val composeUi = "androidx.compose.ui:ui:1.5.4"
const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.9.1" const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.5.4"
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.9.1" const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.5.4"
const val composeMaterial3 = "androidx.compose.material3:material3:1.3.2" const val composeMaterial3 = "androidx.compose.material3:material3:1.1.2"
const val composeFoundation = "androidx.compose.foundation:foundation:1.5.4"
const val composeFoundationLayout = "androidx.compose.foundation:foundation-layout:1.5.4"
const val composeMaterialIconsExtended = "androidx.compose.material:material-icons-extended:1.5.4"
const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}" const val activityCompose = "androidx.activity:activity-compose:${Versions.activityCompose}"
const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}" const val navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:${Versions.hiltNavigationCompose}" const val hiltNavigationCompose = "androidx.hilt:hilt-navigation-compose:${Versions.hiltNavigationCompose}"
@@ -64,9 +67,9 @@ object Libs {
const val junit = "junit:junit:${Versions.junit}" const val junit = "junit:junit:${Versions.junit}"
const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}" const val extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}" const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.9.1" const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.5.4"
const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.9.1" const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.5.4"
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.9.1" const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.5.4"
const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}" const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}"
const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}" const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}"
const val mockk = "io.mockk:mockk:${Versions.mockk}" const val mockk = "io.mockk:mockk:${Versions.mockk}"

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,18 +0,0 @@
// Файл: /data/semantic-ktlint-rules/build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
// Зависимость для RuleSetProviderV3
implementation("com.pinterest.ktlint:ktlint-cli-ruleset-core:1.2.1")
// Зависимость для Rule, RuleId и psi-утилит
api("com.pinterest.ktlint:ktlint-rule-engine:1.2.1")
// Зависимости для тестирования остаются без изменений
testImplementation(kotlin("test"))
testImplementation("com.pinterest.ktlint:ktlint-test:1.2.1")
testImplementation("org.assertj:assertj-core:3.24.2")
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,28 +0,0 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] ExampleInstrumentedTest.kt
// [SEMANTICS] testing, android, ktlint, rules
package com.busya.ktlint.rules
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.busya.ktlint.rules", appContext.packageName)
}
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.HomeboxLens" />
</manifest>

View File

@@ -1,18 +0,0 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] CustomRuleSetProvider.kt
// [SEMANTICS] ktlint, rules, provider
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.RuleProvider
import com.pinterest.ktlint.rule.engine.core.api.RuleSetId
import com.pinterest.ktlint.cli.ruleset.core.api.RuleSetProviderV3
class CustomRuleSetProvider : RuleSetProviderV3(RuleSetId("custom")) {
override fun getRuleProviders(): Set<RuleProvider> {
return setOf(
RuleProvider { FileHeaderRule() },
RuleProvider { MandatoryEntityDeclarationRule() },
RuleProvider { NoStrayCommentsRule() }
)
}
}

View File

@@ -1,35 +0,0 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] FileHeaderRule.kt
// [SEMANTICS] ktlint, rules, file_header
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class FileHeaderRule : Rule(ruleId = RuleId("custom:file-header-rule"), about = About()) {
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.FILE) {
val lines = node.text.lines()
if (lines.size < 3) {
emit(node.startOffset, "File must start with a 3-line semantic header.", false)
return
}
if (!lines[0].startsWith("// [PACKAGE]")) {
emit(node.startOffset, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.", false)
}
if (!lines[1].startsWith("// [FILE]")) {
emit(node.startOffset + lines[0].length + 1, "File header missing or incorrect. Line 2 must be '// [FILE] ...'.", false)
}
if (!lines[2].startsWith("// [SEMANTICS]")) {
emit(node.startOffset + lines[0].length + lines[1].length + 2, "File header missing or incorrect. Line 3 must be '// [SEMANTICS] ...'.", false)
}
}
}
}

View File

@@ -1,42 +0,0 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] MandatoryEntityDeclarationRule.kt
// [SEMANTICS] ktlint, rules, entity_declaration
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import com.pinterest.ktlint.rule.engine.core.api.prevLeaf
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
class MandatoryEntityDeclarationRule : Rule(ruleId = RuleId("custom:entity-declaration-rule"), about = About()) {
private val entityTypes = setOf(
ElementType.CLASS,
ElementType.OBJECT_DECLARATION,
ElementType.FUN
)
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType in entityTypes) {
val ktDeclaration = node.psi as? KtDeclaration ?: return
if (node.elementType == ElementType.FUN &&
(ktDeclaration.hasModifier(KtTokens.PRIVATE_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.PROTECTED_KEYWORD) ||
ktDeclaration.hasModifier(KtTokens.INTERNAL_KEYWORD))
) {
return
}
val prevComment = node.prevLeaf { it.elementType == ElementType.EOL_COMMENT }
if (prevComment == null || !prevComment.text.startsWith("// [ENTITY:")) {
emit(node.startOffset, "Missing or misplaced '// [ENTITY: ...]' declaration before '${node.elementType}'.", false)
}
}
}
}

View File

@@ -1,26 +0,0 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] NoStrayCommentsRule.kt
// [SEMANTICS] ktlint, rules, comments
package com.busya.ktlint.rules
import com.pinterest.ktlint.rule.engine.core.api.ElementType
import com.pinterest.ktlint.rule.engine.core.api.Rule
import com.pinterest.ktlint.rule.engine.core.api.Rule.About
import com.pinterest.ktlint.rule.engine.core.api.RuleId
import org.jetbrains.kotlin.com.intellij.lang.ASTNode
class NoStrayCommentsRule : Rule(ruleId = RuleId("custom:no-stray-comments-rule"), about = About()) {
private val allowedCommentPattern = Regex("""^//\s?\[([A-Z_]+|ENTITY:|RELATION:|AI_NOTE:)]""")
override fun beforeVisitChildNodes(
node: ASTNode,
autoCorrect: Boolean,
emit: (offset: Int, errorMessage: String, canBeAutoCorrected: Boolean) -> Unit
) {
if (node.elementType == ElementType.EOL_COMMENT) {
val commentText = node.text
if (!allowedCommentPattern.matches(commentText)) {
emit(node.startOffset, "Stray comment found. Use semantic anchors like '// [TAG]' or '// [AI_NOTE]:' instead.", false)
}
}
}
}

View File

@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.HomeboxLens" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<string name="app_name">semantic-ktlint-rules</string>
</resources>

View File

@@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.HomeboxLens" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View File

@@ -1,45 +0,0 @@
// [PACKAGE] com.busya.ktlint.rules
// [FILE] ExampleUnitTest.kt
// [SEMANTICS] testing, ktlint, rules
package com.busya.ktlint.rules
import com.pinterest.ktlint.test.KtLintAssertThat.Companion.assertThatRule
import org.junit.jupiter.api.Test
class FileHeaderRuleTest {
private val ruleAssertThat = assertThatRule { FileHeaderRule() }
@Test
fun `should pass on correct header`() {
val code = """
// [PACKAGE] com.example
// [FILE] Test.kt
// [SEMANTICS] test, example
package com.example
""".trimIndent()
ruleAssertThat(code).hasNoLintViolations()
}
@Test
fun `should fail on missing header`() {
val code = """
package com.example
""".trimIndent()
ruleAssertThat(code)
.hasLintViolation(1, 1, "File must start with a 3-line semantic header.")
}
@Test
fun `should fail on incorrect line 1`() {
val code = """
// [WRONG_TAG] com.example
// [FILE] Test.kt
// [SEMANTICS] test, example
package com.example
""".trimIndent()
ruleAssertThat(code)
.hasLintViolation(1, 1, "File header missing or incorrect. Line 1 must be '// [PACKAGE] ...'.")
}
}

View File

@@ -1,502 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# [PACKAGE] tools.semantic_parser
# [FILE] extract_semantics.py
# [SEMANTICS] cli, parser, xml, json, file_io, design_by_contract, structured_logging, protocol_resolver, graphrag, validation, manifest_synchronization
# [AI_NOTE]: Этот скрипт является эталонной реализацией всех четырех ключевых принципов
# семантического обогащения. Он не только проверяет код на соответствие этим правилам,
# но и сам написан с их неукоснительным соблюдением.
# Версия 2.0 добавляет функциональность синхронизации манифеста.
# [IMPORTS]
import sys
import re
import json
import argparse
import os
import logging
import xml.etree.ElementTree as ET
from typing import List, Dict, Any, Optional, Set
# [END_IMPORTS]
# [ENTITY: Class('StructuredFormatter')]
# [RELATION: Class('StructuredFormatter')] -> [INHERITS_FROM] -> [Class('logging.Formatter')]
class StructuredFormatter(logging.Formatter):
"""
@summary Форматтер для логов, реализующий стандарт AIFriendlyLogging.
@invariant Каждый лог, отформатированный этим классом, будет иметь структуру "[LEVEL][ANCHOR][STATE] message".
@sideeffect Отсутствуют.
"""
def format(self, record: logging.LogRecord) -> str:
assert record.msg is not None, "Сообщение лога не может быть None."
record.msg = f"[{record.levelname.upper()}]{record.msg}"
result = super().format(record)
assert result.startswith(f"[{record.levelname.upper()}]"), "Постусловие нарушено: лог не начинается с уровня."
return result
# [END_ENTITY: Class('StructuredFormatter')]
# [ENTITY: Class('SemanticProtocol')]
# [RELATION: Class('SemanticProtocol')] -> [DEPENDS_ON] -> [Module('xml.etree.ElementTree')]
class SemanticProtocol:
"""
@summary Загружает, разрешает <INCLUDE> и предоставляет доступ к правилам из протокола.
@description Этот класс действует как 'резолвер протоколов', рекурсивно обрабатывая
теги <INCLUDE> и объединяя правила из нескольких файлов в единый набор.
@invariant Экземпляр класса всегда содержит полный, объединенный набор правил.
@sideeffect Читает несколько файлов с диска при инициализации.
"""
def __init__(self, main_protocol_path: str):
logger.debug("[DEBUG][ENTRYPOINT][initializing_protocol] Инициализация протокола из главного файла: '%s'", main_protocol_path)
if not os.path.exists(main_protocol_path):
raise FileNotFoundError(f"Главный файл протокола не найден: {main_protocol_path}")
self.processed_paths: Set[str] = set()
self.all_rule_nodes: List[ET.Element] = []
self._resolve_and_load(main_protocol_path)
self.rules = self._parse_all_rules()
logger.info("[INFO][ACTION][resolution_complete] Разрешение протокола завершено. Всего загружено правил: %d", len(self.rules))
def _resolve_and_load(self, file_path: str):
abs_path = os.path.abspath(file_path)
if abs_path in self.processed_paths:
return
logger.info("[INFO][ACTION][resolving_includes] Обработка файла протокола: %s", abs_path)
self.processed_paths.add(abs_path)
try:
tree = ET.parse(abs_path)
root = tree.getroot()
except ET.ParseError as e:
logger.error("[ERROR][ACTION][parsing_failed] Ошибка парсинга XML в файле %s: %s", abs_path, e)
return
self.all_rule_nodes.extend(root.findall(".//Rule"))
base_dir = os.path.dirname(abs_path)
for include_node in root.findall(".//INCLUDE"):
relative_path = include_node.get("from")
if relative_path and relative_path.lower().endswith('.xml'):
included_path = os.path.join(base_dir, relative_path)
self._resolve_and_load(included_path)
def _parse_all_rules(self) -> Dict[str, Dict[str, Any]]:
rules_dict = {}
for rule_node in self.all_rule_nodes:
rule_id = rule_node.get('id')
if not rule_id: continue
definition_node = rule_node.find("Definition")
rules_dict[rule_id] = self._parse_definition(definition_node)
return rules_dict
def _parse_definition(self, node: Optional[ET.Element]) -> Optional[Dict[str, Any]]:
if node is None: return None
def_type = node.get("type")
if def_type in ("regex", "dynamic_regex", "negative_regex"):
return {"type": def_type, "pattern": node.findtext("Pattern", "")}
if def_type == "paired_regex":
return {"type": def_type, "start": node.findtext("Pattern[@name='start']", ""), "end": node.findtext("Pattern[@name='end']", "")}
if def_type == "multi_check":
checks = []
for check_node in node.findall(".//Check"):
check_data = check_node.attrib
check_data['failure_message'] = check_node.findtext("FailureMessage", "")
if check_data.get('type') == 'block_order':
check_data['preceding_pattern'] = check_node.findtext("PrecedingBlockPattern", "")
check_data['following_pattern'] = check_node.findtext("FollowingBlockPattern", "")
elif check_data.get('type') == 'kdoc_validation':
check_data['for_function'] = {t.get('name'): t.get('condition') for t in check_node.findall(".//RequiredTagsForFunction/Tag")}
check_data['for_class'] = {t.get('name'): t.get('condition') for t in check_node.findall(".//RequiredTagsForClass/Tag")}
elif check_data.get('type') == 'contract_enforcement':
condition_node = check_node.find("Condition")
check_data['kdoc_tag'] = condition_node.get('kdoc_tag')
check_data['code_must_contain'] = condition_node.get('code_must_contain')
elif check_data.get('type') == 'entity_type_validation':
check_data['valid_types'] = {t.text for t in check_node.findall(".//ValidEntityTypes/Type")}
elif check_data.get('type') == 'relation_validation':
check_data['triplet_pattern'] = check_node.findtext("TripletPattern", "")
check_data['valid_relations'] = {t.text for t in check_node.findall(".//ValidRelationTypes/Type")}
else:
check_data['pattern'] = check_node.findtext("Pattern", "")
checks.append(check_data)
return {"type": def_type, "checks": checks}
return None
def get_rule(self, rule_id: str) -> Optional[Dict[str, Any]]:
return self.rules.get(rule_id)
# [END_ENTITY: Class('SemanticProtocol')]
# [ENTITY: Class('CodeValidator')]
# [RELATION: Class('CodeValidator')] -> [DEPENDS_ON] -> [Class('SemanticProtocol')]
class CodeValidator:
"""
@summary Применяет правила из протокола к содержимому файла для поиска ошибок.
@invariant Всегда работает с валидным и загруженным экземпляром SemanticProtocol.
"""
def __init__(self, protocol: SemanticProtocol):
self.protocol = protocol
def validate(self, file_path: str, content: str, entity_blocks: List[str]) -> List[str]:
errors = []
rules = self.protocol.rules
if "AIFriendlyLogging" in rules:
errors.extend(self._validate_logging(file_path, content, rules["AIFriendlyLogging"]))
if "DesignByContract" in rules or "GraphRAG" in rules:
for entity_content in entity_blocks:
if "DesignByContract" in rules:
errors.extend(self._validate_entity_dbc(entity_content, rules["DesignByContract"]))
if "GraphRAG" in rules:
errors.extend(self._validate_entity_graphrag(entity_content, rules["GraphRAG"]))
return list(set(errors))
def _validate_logging(self, file_path: str, content: str, rule: Dict) -> List[str]:
errors = []
if rule.get('type') != 'multi_check': return []
for check in rule['checks']:
if check.get('type') == 'negative_regex_in_path' and check.get('path_contains') in file_path and re.search(check['pattern'], content):
errors.append(check['failure_message'])
elif check.get('type') == 'negative_regex' and re.search(check['pattern'], content):
errors.append(check['failure_message'])
elif check.get('type') == 'positive_regex_on_match':
for line in content.splitlines():
if re.search(check['trigger'], line) and not re.search(check['pattern'], line):
errors.append(f"{check['failure_message']} [Строка: '{line.strip()}']")
return errors
def _validate_entity_dbc(self, entity_content: str, rule: Dict) -> List[str]:
errors = []
if rule.get('type') != 'multi_check': return []
kdoc_match = re.search(r"(\/\*\*[\s\S]*?\*\/)", entity_content)
kdoc = kdoc_match.group(1) if kdoc_match else ""
signature_match = re.search(r"\s*(public\s+|private\s+|internal\s+)?(class|interface|fun|object)\s+\w+", entity_content)
is_public = not (signature_match and signature_match.group(1) and 'private' in signature_match.group(1)) if signature_match else False
for check in rule['checks']:
if not is_public and check.get('type') != 'block_order': continue # Проверки контрактов только для public
if check.get('type') == 'kdoc_validation':
is_class = bool(re.search(r"\s*(class|interface|object)", entity_content))
if is_class:
for tag, _ in check['for_class'].items():
if tag not in kdoc: errors.append(f"{check['failure_message']} ({tag})")
else: # is_function
has_params = bool(re.search(r"fun\s+\w+\s*\((.|\s)*\S(.|\s)*\)", entity_content))
returns_value = not bool(re.search(r"fun\s+\w+\(.*\)\s*:\s*Unit", entity_content) or not re.search(r"fun\s+\w+\(.*\)\s*:", entity_content))
for tag, cond in check['for_function'].items():
if tag not in kdoc and (not cond or (cond == 'has_parameters' and has_params) or (cond == 'returns_value' and returns_value)):
errors.append(f"{check['failure_message']} ({tag})")
elif check.get('type') == 'contract_enforcement' and check['kdoc_tag'] in kdoc and not re.search(check['code_must_contain'], entity_content):
errors.append(check['failure_message'])
return errors
def _validate_entity_graphrag(self, entity_content: str, rule: Dict) -> List[str]:
errors = []
if rule.get('type') != 'multi_check': return []
markup_block_match = re.search(r"^([\s\S]*?)(\/\*\*|class|interface|fun|object)", entity_content)
markup_block = markup_block_match.group(1) if markup_block_match else ""
for check in rule['checks']:
if check.get('type') == 'block_order' and "/**" in markup_block:
errors.append(check['failure_message'])
elif check.get('type') == 'entity_type_validation':
entity_match = re.search(r"//\s*\[ENTITY:\s*(?P<type>\w+)\((?P<name>.*?)\)\]", markup_block)
if entity_match and entity_match.group('type') not in check['valid_types']:
errors.append(f"{check['failure_message']} Найдено: {entity_match.group('type')}.")
elif check.get('type') == 'relation_validation':
for line in re.findall(r"//\s*\[RELATION:.*\]", markup_block):
match = re.match(check['triplet_pattern'], line)
if not match:
errors.append(f"{check['failure_message']} (неверный формат). Строка: {line.strip()}")
elif match.group('relation_type') not in check['valid_relations']:
errors.append(f"{check['failure_message']} Найдено: [{match.group('relation_type')}].")
elif check.get('type') == 'markup_cohesion':
for line in markup_block.strip().split('\n'):
if line.strip() and not line.strip().startswith('//'):
errors.append(check['failure_message']); break
return errors
# [END_ENTITY: Class('CodeValidator')]
# [ENTITY: Class('SemanticParser')]
# [RELATION: Class('SemanticParser')] -> [DEPENDS_ON] -> [Class('SemanticProtocol')]
# [RELATION: Class('SemanticParser')] -> [CREATES_INSTANCE_OF] -> [Class('CodeValidator')]
class SemanticParser:
"""
@summary Оркестрирует процесс валидации и парсинга исходных файлов.
@invariant Всегда работает с валидным и загруженным экземпляром SemanticProtocol.
@sideeffect Читает содержимое файлов, переданных для парсинга.
"""
def __init__(self, protocol: SemanticProtocol):
assert isinstance(protocol, SemanticProtocol), "Объект protocol должен быть экземпляром SemanticProtocol."
self.protocol = protocol
self.validator = CodeValidator(protocol)
def parse_file(self, file_path: str) -> Dict[str, Any]:
logger.info("[INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: '%s'", file_path)
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return {"file_path": file_path, "status": "error", "error_message": f"Не удалось прочитать файл: {e}"}
entity_rule = self.protocol.get_rule("EntityContainerization")
entity_blocks = re.findall(entity_rule['start'] + r'[\s\S]*?' + entity_rule['end'], content, re.DOTALL) if entity_rule else []
validation_errors = self.validator.validate(file_path, content, entity_blocks)
header_rule = self.protocol.get_rule("FileHeaderIntegrity")
if not re.search(header_rule['pattern'], content) if header_rule else None:
msg = "Нарушение целостности заголовка (правило FileHeaderIntegrity)."
if msg not in validation_errors: validation_errors.append(msg)
if validation_errors:
logger.warn("[WARN][ACTION][validation_failed] Файл %s не прошел валидацию: %s", file_path, " | ".join(validation_errors))
return {"file_path": file_path, "status": "error", "error_message": " | ".join(validation_errors)}
header_match = re.search(header_rule['pattern'], content)
header_data = header_match.groupdict()
file_info = {
"file_path": file_path, "status": "success",
"header": {"package": header_data.get('package','').strip(), "file_name": header_data.get('file','').strip(), "semantics_tags": [t.strip() for t in header_data.get('semantics','').split(',')]},
"entities": self._extract_entities(content)
}
logger.info("[INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: %d", len(file_info["entities"]))
return file_info
def _extract_entities(self, content: str) -> List[Dict[str, Any]]:
entity_rule = self.protocol.get_rule("EntityContainerization")
if not entity_rule: return []
entities = []
for match in re.finditer(entity_rule['start'] + r'(?P<body>.*?)' + entity_rule['end'], content, re.DOTALL):
data = match.groupdict()
kdoc = self._parse_kdoc(data.get('body', ''))
e_type, e_name = data.get('type', 'N/A'), data.get('name', 'N/A')
type_snake = re.sub(r'(?<!^)(?=[A-Z])', '_', e_type).lower()
name_snake = re.sub(r'[^a-zA-Z0-9_]', '', e_name.replace(' ', '_')).lower()
entities.append({
"node_id": f"{type_snake}_{name_snake}", "entity_type": e_type, "entity_name": e_name,
"summary": kdoc['summary'], "description": kdoc['description'], "relations": kdoc['relations']
})
return entities
def _parse_kdoc(self, body: str) -> Dict[str, Any]:
summary_match = re.search(r"@summary\s*(.*)", body)
summary = summary_match.group(1).strip() if summary_match else ""
desc_match = re.search(r"@description\s*(.*)", body, re.DOTALL)
desc = ""
if desc_match:
lines = [re.sub(r"^\s*\*\s?", "", l).strip() for l in desc_match.group(1).strip().split('\n')]
desc = " ".join(lines)
relations = [m.groupdict() for m in re.finditer(r"->\s*\[(?P<type>\w+)\]\s*->\s*\[\w+\('(?P<target>.*?)'\)\]", body)]
return {"summary": summary, "description": desc, "relations": relations}
# [END_ENTITY: Class('SemanticParser')]
# [ENTITY: Class('ManifestSynchronizer')]
# [RELATION: Class('ManifestSynchronizer')] -> [DEPENDS_ON] -> [Module('xml.etree.ElementTree')]
# [RELATION: Class('ManifestSynchronizer')] -> [MODIFIES_STATE_OF] -> [DataStructure('PROJECT_MANIFEST.xml')]
class ManifestSynchronizer:
"""
@summary Управляет чтением, сравнением и обновлением PROJECT_MANIFEST.xml.
@invariant Экземпляр класса всегда работает с корректно загруженным XML-деревом.
@sideeffect Читает и может перезаписывать файл манифеста на диске.
"""
def __init__(self, manifest_path: str):
"""
@param manifest_path: Путь к файлу PROJECT_MANIFEST.xml.
@sideeffect Читает и парсит XML-файл. Вызывает исключение, если файл не найден или поврежден.
"""
require(os.path.exists(manifest_path), f"Файл манифеста не найден: {manifest_path}")
logger.info("[INFO][ENTRYPOINT][manifest_loading] Загрузка манифеста: %s", manifest_path)
self.manifest_path = manifest_path
try:
self.tree = ET.parse(manifest_path)
self.root = self.tree.getroot()
self.graph_node = self.root.find("PROJECT_GRAPH")
if self.graph_node is None:
raise ValueError("В манифесте отсутствует тег <PROJECT_GRAPH>")
except (ET.ParseError, ValueError) as e:
logger.error("[ERROR][ACTION][manifest_parsing_failed] Ошибка парсинга манифеста: %s", e)
raise ValueError(f"Ошибка парсинга манифеста: {e}")
def synchronize(self, parsed_code_data: List[Dict[str, Any]]) -> Dict[str, int]:
"""
@summary Синхронизирует состояние манифеста с состоянием кодовой базы.
@param parsed_code_data: Список словарей, представляющих состояние файлов, от SemanticParser.
@return Словарь со статистикой изменений.
@sideeffect Модифицирует внутреннее XML-дерево.
"""
stats = {"nodes_added": 0, "nodes_updated": 0, "nodes_removed": 0}
all_code_node_ids = {
entity["node_id"]
for file_data in parsed_code_data if file_data["status"] == "success"
for entity in file_data["entities"]
}
manifest_nodes_map = {node.get("id"): node for node in self.graph_node.findall("NODE")}
manifest_node_ids = set(manifest_nodes_map.keys())
# Удаление узлов, которых больше нет в коде
nodes_to_remove = manifest_node_ids - all_code_node_ids
for node_id in nodes_to_remove:
logger.debug("[DEBUG][ACTION][removing_node] Удаление устаревшего узла: %s", node_id)
self.graph_node.remove(manifest_nodes_map[node_id])
stats["nodes_removed"] += 1
# Добавление и обновление узлов
for file_data in parsed_code_data:
if file_data["status"] != "success":
continue
for entity in file_data["entities"]:
node_id = entity["node_id"]
existing_node = manifest_nodes_map.get(node_id)
if existing_node is None:
logger.debug("[DEBUG][ACTION][adding_node] Добавление нового узла: %s", node_id)
new_node = ET.SubElement(self.graph_node, "NODE", id=node_id)
self._update_node_attributes(new_node, entity, file_data)
stats["nodes_added"] += 1
else:
if self._is_update_needed(existing_node, entity, file_data):
logger.debug("[DEBUG][ACTION][updating_node] Обновление узла: %s", node_id)
self._update_node_attributes(existing_node, entity, file_data)
stats["nodes_updated"] += 1
logger.info("[INFO][POSTCONDITION][synchronization_complete] Синхронизация завершена. Статистика: %s", stats)
return stats
def _update_node_attributes(self, node: ET.Element, entity: Dict, file_data: Dict):
node.set("type", entity["entity_type"])
node.set("name", entity["entity_name"])
node.set("file_path", file_data["file_path"])
node.set("package", file_data["header"]["package"])
# Очистка и добавление дочерних тегов
for child in list(node):
node.remove(child)
ET.SubElement(node, "SUMMARY").text = entity["summary"]
ET.SubElement(node, "DESCRIPTION").text = entity["description"]
tags_node = ET.SubElement(node, "SEMANTICS_TAGS")
tags_node.text = ", ".join(file_data["header"]["semantics_tags"])
relations_node = ET.SubElement(node, "RELATIONS")
for rel in entity["relations"]:
ET.SubElement(relations_node, "RELATION", type=rel["type"], target_id=rel["target"])
def _is_update_needed(self, node: ET.Element, entity: Dict, file_data: Dict) -> bool:
# Простая проверка по нескольким ключевым полям
if node.get("type") != entity["entity_type"] or node.get("name") != entity["entity_name"]:
return True
summary_node = node.find("SUMMARY")
if summary_node is None or summary_node.text != entity["summary"]:
return True
return False
def write_xml(self):
"""
@summary Записывает измененное XML-дерево обратно в файл.
@sideeffect Перезаписывает файл манифеста на диске.
"""
require(self.tree is not None, "XML-дерево не было инициализировано.")
logger.info("[INFO][ACTION][writing_manifest] Запись изменений в файл манифеста: %s", self.manifest_path)
ET.indent(self.tree, space=" ")
self.tree.write(self.manifest_path, encoding="utf-8", xml_declaration=True)
# [END_ENTITY: Class('ManifestSynchronizer')]
# [ENTITY: Function('require')]
def require(condition: bool, message: str):
"""
@summary Проверяет предусловие и вызывает ValueError, если оно ложно.
@param condition: Условие для проверки.
@param message: Сообщение об ошибке.
@sideeffect Вызывает исключение при ложном условии.
"""
if not condition:
raise ValueError(message)
# [END_ENTITY: Function('require')]
# [ENTITY: Function('main')]
# [RELATION: Function('main')] -> [CREATES_INSTANCE_OF] -> [Class('SemanticProtocol')]
# [RELATION: Function('main')] -> [CREATES_INSTANCE_OF] -> [Class('SemanticParser')]
# [RELATION: Function('main')] -> [CREATES_INSTANCE_OF] -> [Class('ManifestSynchronizer')]
def main():
"""
@summary Главная точка входа в приложение.
@description Управляет жизненным циклом: парсинг аргументов, настройка логирования,
запуск парсинга файлов и синхронизации манифеста.
@sideeffect Читает аргументы командной строки, выводит результат в stdout/stderr.
"""
parser = argparse.ArgumentParser(description="Парсит .kt файлы и синхронизирует манифест проекта.")
parser.add_argument('files', nargs='+', help="Список .kt файлов для обработки.")
parser.add_argument('--protocol', required=True, help="Путь к главному файлу протокола.")
parser.add_argument('--manifest-path', required=True, help="Путь к файлу PROJECT_MANIFEST.xml.")
parser.add_argument('--update-in-place', action='store_true', help="Если указано, перезаписывает файл манифеста.")
parser.add_argument('--log-level', default='INFO', choices=['DEBUG', 'INFO', 'WARN', 'ERROR'], help="Уровень логирования.")
args = parser.parse_args()
logger.setLevel(args.log_level)
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(StructuredFormatter())
logger.addHandler(handler)
logger.info("[INFO][INITIALIZATION][configuring_logger] Логгер настроен. Уровень: %s", args.log_level)
output_report = {
"status": "success",
"manifest_path": args.manifest_path,
"files_scanned": len(args.files),
"files_with_errors": 0,
"changes": {}
}
try:
protocol = SemanticProtocol(args.protocol)
parser_instance = SemanticParser(protocol)
parsed_results = [parser_instance.parse_file(f) for f in args.files]
output_report["files_with_errors"] = sum(1 for r in parsed_results if r["status"] == "error")
synchronizer = ManifestSynchronizer(args.manifest_path)
change_stats = synchronizer.synchronize(parsed_results)
output_report["changes"] = change_stats
if args.update_in_place:
if sum(change_stats.values()) > 0:
synchronizer.write_xml()
logger.info("[INFO][ACTION][manifest_updated] Манифест был успешно обновлен.")
else:
logger.info("[INFO][ACTION][manifest_not_updated] Изменений не было, манифест не перезаписан.")
output_report["status"] = "success"
except (FileNotFoundError, ValueError, ET.ParseError) as e:
logger.critical("[FATAL][EXECUTION][critical_error] Критическая ошибка: %s", e, exc_info=True)
output_report["error_message"] = str(e)
output_report["status"] = "failure"
finally:
print(json.dumps(output_report, indent=2, ensure_ascii=False))
if output_report["status"] == "failure":
sys.exit(1)
# [END_ENTITY: Function('main')]
# [CONTRACT]
if __name__ == "__main__":
logger = logging.getLogger(__name__)
main()
# [END_CONTRACT]
# [END_FILE_extract_semantics.py]

View File

@@ -1,289 +0,0 @@
[INFO][INFO][INITIALIZATION][configuring_logger] Логгер настроен. Уровень: INFO
[INFO][INFO][ACTION][resolving_includes] Обработка файла протокола: /home/busya/dev/homebox_lens/agent_promts/protocols/semantic_enrichment_protocol.xml
[INFO][INFO][ACTION][resolving_includes] Обработка файла протокола: /home/busya/dev/homebox_lens/agent_promts/knowledge_base/semantic_linting.xml
[INFO][INFO][ACTION][resolving_includes] Обработка файла протокола: /home/busya/dev/homebox_lens/agent_promts/knowledge_base/graphrag_optimization.xml
[INFO][INFO][ACTION][resolving_includes] Обработка файла протокола: /home/busya/dev/homebox_lens/agent_promts/knowledge_base/design_by_contract.xml
[INFO][INFO][ACTION][resolving_includes] Обработка файла протокола: /home/busya/dev/homebox_lens/agent_promts/knowledge_base/ai_friendly_logging.xml
[INFO][INFO][ACTION][resolution_complete] Разрешение протокола завершено. Всего загружено правил: 7
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './buildSrc/src/main/java/Dependencies.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/test/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModelTest.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/navigation/NavGraph.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/navigation/Screen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/navigation/NavigationActions.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/MainApplication.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/common/AppDrawer.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/common/MainScaffold.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/theme/Color.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/theme/Theme.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/theme/Typography.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 11
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/labeledit/LabelEditViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 3
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 3
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListUiState.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/locationedit/LocationEditScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 7
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListUiState.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/components/ColorPicker.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/ui/components/LoadingOverlay.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './app/src/main/java/com/homebox/lens/MainActivity.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 3
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/test/java/com/homebox/lens/domain/usecase/UpdateItemUseCaseTest.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/UpdateLabelUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/UpdateLocationUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/GetRecentlyAddedItemsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/CreateLocationUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/DeleteLocationUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/DeleteLabelUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/GetLabelDetailsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/CreateLabelUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LabelCreate.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/GroupStatistics.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/ItemCreate.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Label.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LocationUpdate.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/TokenResponse.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Result.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/CustomField.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LabelSummary.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LocationOut.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LabelUpdate.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Image.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LocationCreate.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Credentials.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/ItemOut.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/ItemUpdate.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LabelOut.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/ItemAttachment.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/ItemSummary.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Statistics.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/LocationOutCount.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Location.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/PaginationResult.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/MaintenanceEntry.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/model/Item.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/repository/AuthRepository.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './domain/src/main/java/com/homebox/lens/domain/repository/CredentialsRepository.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/semantic-ktlint-rules/src/test/java/com/busya/ktlint/rules/ExampleUnitTest.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/CustomRuleSetProvider.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/MandatoryEntityDeclarationRule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/NoStrayCommentsRule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/semantic-ktlint-rules/src/main/java/com/busya/ktlint/rules/FileHeaderRule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/semantic-ktlint-rules/src/androidTest/java/com/busya/ktlint/rules/ExampleInstrumentedTest.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/di/ApiModule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/di/StorageModule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/di/DatabaseModule.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/HomeboxApiService.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/model/LoginRequest.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 0
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ItemOutDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LocationOutCountDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ItemCreateDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/TokenResponseDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ItemDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 4
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LocationOutDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/PaginationResultDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/PaginationDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ItemUpdateDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LocationUpdateDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ImageDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/CustomFieldDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/GroupStatisticsDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LabelSummaryDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/MaintenanceEntryDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LocationCreateDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LabelOutDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/StatisticsDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LocationDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ItemAttachmentDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LoginFormDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LabelCreateDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/ItemSummaryDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/dto/LabelUpdateDto.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/api/mapper/TokenMapper.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/HomeboxDatabase.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/entity/LabelEntity.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/entity/ItemLabelCrossRef.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/entity/ItemEntity.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/entity/LocationEntity.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/entity/Mapper.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 2
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/entity/ItemWithLabels.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/Converters.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/dao/LabelDao.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/dao/ItemDao.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/db/dao/LocationDao.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/security/CryptoManager.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/repository/CredentialsRepositoryImpl.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 4
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/repository/AuthRepositoryImpl.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][parsing_file] Начало парсинга файла: './data/src/main/java/com/homebox/lens/data/repository/EncryptedPreferencesWrapper.kt'
[INFO][INFO][POSTCONDITION][parsing_complete] Парсинг файла завершен. Найдено сущностей: 1
[INFO][INFO][ENTRYPOINT][manifest_loading] Загрузка манифеста: tech_spec/PROJECT_MANIFEST.xml
{
"status": "failure",
"manifest_path": "tech_spec/PROJECT_MANIFEST.xml",
"files_scanned": 137,
"files_with_errors": 0,
"changes": {}
}

View File

@@ -1,5 +1,6 @@
// [FILE] build.gradle.kts // [FILE] feature/dashboard/build.gradle.kts
// [SEMANTICS] build, configuration, module, feature, dashboard // [SEMANTICS] build, dashboard, feature_module
// [PURPOSE] Build script for the feature:dashboard module.
plugins { plugins {
id("com.android.library") id("com.android.library")
@@ -16,9 +17,6 @@ android {
defaultConfig { defaultConfig {
minSdk = Versions.minSdk minSdk = Versions.minSdk
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
} }
buildTypes { buildTypes {
@@ -26,7 +24,7 @@ android {
isMinifyEnabled = false isMinifyEnabled = false
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro",
) )
} }
} }
@@ -40,7 +38,6 @@ android {
buildFeatures { buildFeatures {
compose = true compose = true
} }
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -49,9 +46,23 @@ android {
} }
dependencies { dependencies {
// [MODULE_DEPENDENCY] Core modules // [MODULE_DEPENDENCY] Data module
implementation(project(":domain"))
implementation(project(":data")) implementation(project(":data"))
// [MODULE_DEPENDENCY] Domain module
implementation(project(":domain"))
// [MODULE_DEPENDENCY] Feature modules for navigation
implementation(project(":feature:inventorylist"))
implementation(project(":feature:itemdetails"))
implementation(project(":feature:itemedit"))
implementation(project(":feature:labeledit"))
implementation(project(":feature:labelslist"))
implementation(project(":feature:locationedit"))
implementation(project(":feature:locationslist"))
implementation(project(":feature:scan"))
implementation(project(":feature:search"))
implementation(project(":feature:settings"))
implementation(project(":feature:setup"))
implementation(project(":ui:common"))
// [DEPENDENCY] AndroidX // [DEPENDENCY] AndroidX
implementation(Libs.coreKtx) implementation(Libs.coreKtx)
@@ -61,9 +72,13 @@ dependencies {
// [DEPENDENCY] Compose // [DEPENDENCY] Compose
implementation(Libs.composeUi) implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics) implementation(Libs.composeUiGraphics)
implementation(Libs.composeFoundation)
implementation(Libs.composeUiToolingPreview) implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3) implementation(Libs.composeMaterial3)
implementation("androidx.compose.material:material-icons-extended-android:1.6.8") implementation(Libs.composeFoundationLayout)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.composeFoundationLayout)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose) implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose) implementation(Libs.hiltNavigationCompose)
@@ -92,4 +107,4 @@ kapt {
correctErrorTypes = true correctErrorTypes = true
} }
// [END_FILE_build.gradle.kts] // [END_FILE_feature/dashboard/build.gradle.kts]

View File

@@ -4,29 +4,26 @@
package com.homebox.lens.feature.dashboard package com.homebox.lens.feature.dashboard
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.ui.common.NavigationActions
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('addDashboardScreen')] // [ANCHOR:addDashboardScreen:Function]
// [RELATION: Function('addDashboardScreen')] -> [DEPENDS_ON] -> [Function('DashboardScreen')] // [RELATION:DEPENDS_ON:DashboardScreen]
/** // [CONTRACT:addDashboardScreen]
* @summary Extension function for NavGraphBuilder to add the Dashboard screen to the navigation graph. // [PURPOSE] Extension function for NavGraphBuilder to add the Dashboard screen to the navigation graph. Registers the Dashboard route and composes the DashboardScreen with appropriate navigation actions and common UI components.
* @description Registers the Dashboard route and composes the DashboardScreen with appropriate navigation actions and common UI components. // [PARAM:route:String] The route string for the Dashboard screen.
* @param route The route string for the Dashboard screen. // [PARAM:currentRoute:String] The current navigation route, used for highlighting.
* @param currentRoute The current navigation route, used for highlighting. // [PARAM:navigateToScan:Unit] Lambda for navigating to the scan screen.
* @param navigateToScan Lambda for navigating to the scan screen. // [PARAM:navigateToSearch:Unit] Lambda for navigating to the search screen.
* @param navigateToSearch Lambda for navigating to the search screen. // [PARAM:navigateToInventoryListWithLocation:Unit] Lambda for navigating to inventory filtered by location.
* @param navigateToInventoryListWithLocation Lambda for navigating to inventory filtered by location. // [PARAM:navigateToInventoryListWithLabel:Unit] Lambda for navigating to inventory filtered by label.
* @param navigateToInventoryListWithLabel Lambda for navigating to inventory filtered by label. // [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
* @param MainScaffoldContent Composable lambda for the main scaffold structure. // [PARAM:navController:NavHostController] Контроллер навигации.
* @param HomeboxLensTheme Composable lambda for applying the application theme. // [SIDE_EFFECT] Adds a composable route for the Dashboard screen.
* @sideeffect Adds a composable route for the Dashboard screen. // [END_CONTRACT:addDashboardScreen]
*/
fun NavGraphBuilder.addDashboardScreen( fun NavGraphBuilder.addDashboardScreen(
route: String, route: String,
currentRoute: String?, currentRoute: String?,
@@ -36,14 +33,6 @@ fun NavGraphBuilder.addDashboardScreen(
navigateToInventoryListWithLabel: (String) -> Unit, navigateToInventoryListWithLabel: (String) -> Unit,
navigationActions: NavigationActions, navigationActions: NavigationActions,
navController: NavHostController, navController: NavHostController,
MainScaffoldContent: @Composable (
topBarTitle: String,
currentRoute: String?,
navigationActions: NavigationActions,
topBarActions: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit
) -> Unit,
HomeboxLensTheme: @Composable (content: @Composable () -> Unit) -> Unit
) { ) {
composable(route = route) { composable(route = route) {
DashboardScreen( DashboardScreen(
@@ -52,12 +41,10 @@ fun NavGraphBuilder.addDashboardScreen(
navigateToSearch = navigateToSearch, navigateToSearch = navigateToSearch,
navigateToInventoryListWithLocation = navigateToInventoryListWithLocation, navigateToInventoryListWithLocation = navigateToInventoryListWithLocation,
navigateToInventoryListWithLabel = navigateToInventoryListWithLabel, navigateToInventoryListWithLabel = navigateToInventoryListWithLabel,
MainScaffoldContent = MainScaffoldContent,
HomeboxLensTheme = HomeboxLensTheme,
navigationActions = navigationActions, navigationActions = navigationActions,
navController = navController navController = navController,
) )
} }
} }
// [END_ENTITY: Function('addDashboardScreen')] // [END_ANCHOR:addDashboardScreen]
// [END_FILE_DashboardNavigation.kt] // [END_FILE_DashboardNavigation.kt]

View File

@@ -4,10 +4,18 @@
package com.homebox.lens.feature.dashboard package com.homebox.lens.feature.dashboard
// [IMPORTS] // [IMPORTS]
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.ExperimentalLayoutApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@@ -16,7 +24,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.* import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@@ -29,29 +43,30 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.homebox.lens.domain.model.* import com.homebox.lens.domain.model.*
import com.homebox.lens.feature.dashboard.R import com.homebox.lens.feature.dashboard.R
import com.homebox.lens.navigation.NavigationActions import com.homebox.lens.ui.common.mainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.ui.common.NavigationActions
import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
import timber.log.Timber import timber.log.Timber
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('DashboardScreen')] // [ANCHOR:DashboardScreen:Function]
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')] // [RELATION:CALLS:DashboardViewModel]
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')] // [RELATION:CALLS:mainScaffold]
/** // [CONTRACT:DashboardScreen]
* @summary Главная Composable-функция для экрана "Панель управления". // [PURPOSE] Главная Composable-функция для экрана "Панель управления".
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt. // [PARAM:viewModel:DashboardViewModel] ViewModel для этого экрана, предоставляется через Hilt.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer. // [PARAM:currentRoute:String] Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigateToScan Лямбда для навигации на экран сканирования. // [PARAM:navigateToScan:Unit] Лямбда для навигации на экран сканирования.
* @param navigateToSearch Лямбда для навигации на экран поиска. // [PARAM:navigateToSearch:Unit] Лямбда для навигации на экран поиска.
* @param navigateToInventoryListWithLocation Лямбда для навигации на список инвентаря с фильтром по локации. // [PARAM:navigateToInventoryListWithLocation:Unit] Лямбда для навигации на список инвентаря с фильтром по локации.
* @param navigateToInventoryListWithLabel Лямбда для навигации на список инвентаря с фильтром по метке. // [PARAM:navigateToInventoryListWithLabel:Unit] Лямбда для навигации на список инвентаря с фильтром по метке.
* @param MainScaffoldContent Composable-функция для отображения основного Scaffold. // [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
* @param HomeboxLensTheme Composable-функция для применения темы. // [PARAM:navController:NavHostController] Контроллер навигации.
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI. // [SIDE_EFFECT] Вызывает навигационные лямбды при взаимодействии с UI.
*/ // [END_CONTRACT:DashboardScreen]
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel(), viewModel: DashboardViewModel = hiltViewModel(),
@@ -62,17 +77,6 @@ fun DashboardScreen(
navigateToInventoryListWithLabel: (String) -> Unit, navigateToInventoryListWithLabel: (String) -> Unit,
navigationActions: NavigationActions, navigationActions: NavigationActions,
navController: NavHostController, navController: NavHostController,
MainScaffoldContent: @Composable (
topBarTitle: String,
currentRoute: String?,
navigationActions: NavigationActions,
onNavigateUp: (() -> Unit)?,
topBarActions: @Composable () -> Unit,
snackbarHost: @Composable () -> Unit,
floatingActionButton: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit
) -> Unit,
HomeboxLensTheme: @Composable (content: @Composable () -> Unit) -> Unit
) { ) {
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
@@ -81,60 +85,60 @@ fun DashboardScreen(
} }
HomeboxLensTheme { HomeboxLensTheme {
MainScaffoldContent( mainScaffold(
topBarTitle = stringResource(id = R.string.dashboard_title), topBarTitle = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_title),
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions, navigationActions = navigationActions,
onNavigateUp = null, // Dashboard doesn't have an "Up" button onNavigateUp = null, // Dashboard doesn't have an "Up" button
topBarActions = { topBarActions = {
IconButton(onClick = navigateToScan) { IconButton(onClick = navigateToScan) {
Icon( Icon(
Icons.Default.QrCodeScanner, Icons.Filled.QrCodeScanner,
contentDescription = stringResource(id = R.string.cd_scan_qr_code) contentDescription = stringResource(id = com.homebox.lens.feature.dashboard.R.string.cd_scan_qr_code),
) )
} }
IconButton(onClick = navigateToSearch) { IconButton(onClick = navigateToSearch) {
Icon( Icon(
Icons.Default.Search, Icons.Default.Search,
contentDescription = stringResource(id = R.string.cd_search) contentDescription = stringResource(id = com.homebox.lens.feature.dashboard.R.string.cd_search),
) )
} }
}, },
snackbarHost = {}, // Not used in Dashboard snackbarHost = { },
floatingActionButton = {} // Not used in Dashboard floatingActionButton = { },
) { paddingValues -> ) { paddingValues ->
DashboardContent( DashboardContent(
modifier = Modifier.padding(paddingValues), modifier = Modifier.padding(paddingValues),
uiState = uiState, uiState = uiState,
onLocationClick = { location -> onLocationClick = { location ->
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Location chip clicked: ${location.id}. Navigating...") Timber.i("[INFO][ACTION][navigate_to_inventory_with_location]", "Location chip clicked", "locationId", location.id)
navigateToInventoryListWithLocation(location.id) navigateToInventoryListWithLocation(location.id)
}, },
onLabelClick = { label -> onLabelClick = { label ->
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Label chip clicked: ${label.id}. Navigating...") Timber.i("[INFO][ACTION][navigate_to_inventory_with_label]", "Label chip clicked", "labelId", label.id)
navigateToInventoryListWithLabel(label.id) navigateToInventoryListWithLabel(label.id)
} },
) )
} }
} }
} }
// [END_ENTITY: Function('DashboardScreen')] // [END_ANCHOR:DashboardScreen]
// [ENTITY: Function('DashboardContent')] // [ANCHOR:DashboardContent:Function]
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')] // [RELATION:CONSUMES_STATE:DashboardUiState]
/** // [CONTRACT:DashboardContent]
* @summary Отображает основной контент экрана в зависимости от uiState. // [PURPOSE] Отображает основной контент экрана в зависимости от uiState.
* @param modifier Модификатор для стилизации. // [PARAM:modifier:Modifier] Модификатор для стилизации.
* @param uiState Текущее состояние UI экрана. // [PARAM:uiState:DashboardUiState] Текущее состояние UI экрана.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение. // [PARAM:onLocationClick:Unit] Лямбда-обработчик нажатия на местоположение.
* @param onLabelClick Лямбда-обработчик нажатия на метку. // [PARAM:onLabelClick:Unit] Лямбда-обработчик нажатия на метку.
*/ // [END_CONTRACT:DashboardContent]
@Composable @Composable
private fun DashboardContent( private fun DashboardContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
uiState: DashboardUiState, uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit, onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit onLabelClick: (LabelOut) -> Unit,
) { ) {
when (uiState) { when (uiState) {
is DashboardUiState.Loading -> { is DashboardUiState.Loading -> {
@@ -147,16 +151,17 @@ private fun DashboardContent(
Text( Text(
text = uiState.message, text = uiState.message,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center textAlign = TextAlign.Center,
) )
} }
} }
is DashboardUiState.Success -> { is DashboardUiState.Success -> {
LazyColumn( LazyColumn(
modifier = modifier modifier =
.fillMaxSize() modifier
.padding(horizontal = 16.dp), .fillMaxSize()
verticalArrangement = Arrangement.spacedBy(24.dp) .padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
item { Spacer(modifier = Modifier.height(8.dp)) } item { Spacer(modifier = Modifier.height(8.dp)) }
item { StatisticsSection(statistics = uiState.statistics) } item { StatisticsSection(statistics = uiState.statistics) }
@@ -168,77 +173,102 @@ private fun DashboardContent(
} }
} }
} }
// [END_ENTITY: Function('DashboardContent')] // [END_ANCHOR:DashboardContent]
// [ENTITY: Function('StatisticsSection')] // [ANCHOR:StatisticsSection:Function]
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')] // [RELATION:DEPENDS_ON:GroupStatistics]
/** // [CONTRACT:StatisticsSection]
* @summary Секция для отображения общей статистики. // [PURPOSE] Секция для отображения общей статистики.
* @param statistics Объект со статистическими данными. // [PARAM:statistics:GroupStatistics] Объект со статистическими данными.
*/ // [END_CONTRACT:StatisticsSection]
@Composable @Composable
private fun StatisticsSection(statistics: GroupStatistics) { private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_quick_stats), text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_quick_stats),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
Card { Card {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
modifier = Modifier modifier =
.height(120.dp) Modifier
.fillMaxWidth() .height(120.dp)
.padding(16.dp), .fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) } item {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) } StatisticCard(
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) } title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_items),
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) } value = statistics.items.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_value),
value = statistics.totalValue.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_labels),
value = statistics.labels.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_locations),
value = statistics.locations.toString(),
)
}
} }
} }
} }
} }
// [END_ENTITY: Function('StatisticsSection')] // [END_ANCHOR:StatisticsSection]
// [ENTITY: Function('StatisticCard')] // [ANCHOR:StatisticCard:Function]
/** // [CONTRACT:StatisticCard]
* @summary Карточка для отображения одного статистического показателя. // [PURPOSE] Карточка для отображения одного статистического показателя.
* @param title Название показателя. // [PARAM:title:String] Название показателя.
* @param value Значение показателя. // [PARAM:value:String] Значение показателя.
*/ // [END_CONTRACT:StatisticCard]
@Composable @Composable
private fun StatisticCard(title: String, value: String) { private fun StatisticCard(
title: String,
value: String,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center) Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center) Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
} }
} }
// [END_ENTITY: Function('StatisticCard')] // [END_ANCHOR:StatisticCard]
// [ENTITY: Function('RecentlyAddedSection')] // [ANCHOR:RecentlyAddedSection:Function]
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION:DEPENDS_ON:ItemSummary]
/** // [CONTRACT:RecentlyAddedSection]
* @summary Секция для отображения недавно добавленных элементов. // [PURPOSE] Секция для отображения недавно добавленных элементов.
* @param items Список элементов для отображения. // [PARAM:items:List<ItemSummary>] Список элементов для отображения.
*/ // [END_CONTRACT:RecentlyAddedSection]
@Composable @Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) { private fun RecentlyAddedSection(items: List<ItemSummary>) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_recently_added), text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_recently_added),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
if (items.isEmpty()) { if (items.isEmpty()) {
Text( Text(
text = stringResource(id = R.string.items_not_found), text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier modifier =
.fillMaxWidth() Modifier
.padding(vertical = 16.dp), .fillMaxWidth()
textAlign = TextAlign.Center .padding(vertical = 16.dp),
textAlign = TextAlign.Center,
) )
} else { } else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -249,126 +279,189 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
} }
} }
} }
// [END_ENTITY: Function('RecentlyAddedSection')] // [END_ANCHOR:RecentlyAddedSection]
// [ENTITY: Function('ItemCard')] // [ANCHOR:ItemCard:Function]
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION:DEPENDS_ON:ItemSummary]
/** // [CONTRACT:ItemCard]
* @summary Карточка для отображения краткой информации об элементе. // [PURPOSE] Карточка для отображения краткой информации об элементе.
* @param item Элемент для отображения. // [PARAM:item:ItemSummary] Элемент для отображения.
*/ // [END_CONTRACT:ItemCard]
@Composable @Composable
private fun ItemCard(item: ItemSummary) { private fun ItemCard(item: ItemSummary) {
Card(modifier = Modifier.width(150.dp)) { Card(modifier = Modifier.width(150.dp)) {
Column(modifier = Modifier.padding(8.dp)) { Column(modifier = Modifier.padding(8.dp)) {
// [AI_NOTE]: Add image here from item.image // [AI_NOTE]: Add image here from item.image
Spacer(modifier = Modifier Spacer(
.height(80.dp) modifier =
.fillMaxWidth() Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)) .height(80.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondaryContainer),
)
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1) Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1) Text(
text = item.location?.name ?: stringResource(id = com.homebox.lens.feature.dashboard.R.string.items_not_found),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
)
} }
} }
} }
// [END_ENTITY: Function('ItemCard')] // [END_ANCHOR:ItemCard]
// [ENTITY: Function('LocationsSection')] // [ANCHOR:LocationsSection:Function]
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION:DEPENDS_ON:LocationOutCount]
/** // [CONTRACT:LocationsSection]
* @summary Секция для отображения местоположений в виде чипсов. // [PURPOSE] Секция для отображения местоположений в виде чипсов.
* @param locations Список местоположений. // [PARAM:locations:List<LocationOutCount>] Список местоположений.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение. // [PARAM:onLocationClick:Unit] Лямбда-обработчик нажатия на местоположение.
*/ // [END_CONTRACT:LocationsSection]
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) { private fun LocationsSection(
locations: List<LocationOutCount>,
onLocationClick: (LocationOutCount) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_locations), text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_locations),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
locations.forEach { location -> locations.forEach { location ->
SuggestionChip( SuggestionChip(
onClick = { onLocationClick(location) }, onClick = {
label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) } Timber.i("[INFO][ACTION][location_chip_click]", "Location chip clicked", "locationId", location.id)
onLocationClick(location)
},
label = { Text(stringResource(id = com.homebox.lens.feature.dashboard.R.string.location_chip_label, location.name, location.itemCount)) },
) )
} }
} }
} }
} }
// [END_ENTITY: Function('LocationsSection')] // [END_ANCHOR:LocationsSection]
// [ENTITY: Function('LabelsSection')] // [ANCHOR:LabelsSection:Function]
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] // [RELATION:DEPENDS_ON:LabelOut]
/** // [CONTRACT:LabelsSection]
* @summary Секция для отображения меток в виде чипсов. // [PURPOSE] Секция для отображения меток в виде чипсов.
* @param labels Список меток. // [PARAM:labels:List<LabelOut>] Список меток.
* @param onLabelClick Лямбда-обработчик нажатия на метку. // [PARAM:onLabelClick:Unit] Лямбда-обработчик нажатия на метку.
*/ // [END_CONTRACT:LabelsSection]
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) { private fun LabelsSection(
labels: List<LabelOut>,
onLabelClick: (LabelOut) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text( Text(
text = stringResource(id = R.string.dashboard_section_labels), text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_labels),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium,
) )
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
labels.forEach { label -> labels.forEach { label ->
SuggestionChip( SuggestionChip(
onClick = { onLabelClick(label) }, onClick = {
label = { Text(label.name) } Timber.i("[INFO][ACTION][label_chip_click]", "Label chip clicked", "labelId", label.id)
onLabelClick(label)
},
label = { Text(label.name) },
) )
} }
} }
} }
} }
// [END_ENTITY: Function('LabelsSection')] // [END_ANCHOR:LabelsSection]
// [ENTITY: Function('DashboardContentSuccessPreview')] // [ANCHOR:DashboardContentSuccessPreview:Function]
@Preview(showBackground = true, name = "Dashboard Success State") @Preview(showBackground = true, name = "Dashboard Success State")
@Composable @Composable
fun DashboardContentSuccessPreview() { fun DashboardContentSuccessPreview() {
val previewState = DashboardUiState.Success( val previewState =
statistics = GroupStatistics( DashboardUiState.Success(
items = 123, statistics =
totalValue = 9999.99, GroupStatistics(
locations = 5, items = 123,
labels = 8 totalValue = 9999.99,
), locations = 5,
locations = listOf( labels = 8,
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""), ),
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""), locations =
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""), listOf(
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""), LocationOutCount(
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "") id = "1",
), name = "Office",
labels = listOf( color = "#FF0000",
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""), isArchived = false,
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""), itemCount = 10,
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""), createdAt = "",
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "") updatedAt = "",
), ),
recentlyAddedItems = emptyList() LocationOutCount(
) id = "2",
name = "Garage",
color = "#00FF00",
isArchived = false,
itemCount = 5,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "3",
name = "Living Room",
color = "#0000FF",
isArchived = false,
itemCount = 15,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "4",
name = "Kitchen",
color = "#FFFF00",
isArchived = false,
itemCount = 20,
createdAt = "",
updatedAt = "",
),
LocationOutCount(
id = "5",
name = "Basement",
color = "#00FFFF",
isArchived = false,
itemCount = 3,
createdAt = "",
updatedAt = "",
),
),
labels =
listOf(
LabelOut(id = "1", name = "electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "2", name = "important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "3", name = "seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id = "4", name = "hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = ""),
),
recentlyAddedItems = emptyList(),
)
HomeboxLensTheme { HomeboxLensTheme {
DashboardContent( DashboardContent(
uiState = previewState, uiState = previewState,
onLocationClick = {}, onLocationClick = {},
onLabelClick = {} onLabelClick = {},
) )
} }
} }
// [END_ENTITY: Function('DashboardContentSuccessPreview')] // [END_ANCHOR:DashboardContentSuccessPreview]
// [ENTITY: Function('DashboardContentLoadingPreview')] // [ANCHOR:DashboardContentLoadingPreview:Function]
@Preview(showBackground = true, name = "Dashboard Loading State") @Preview(showBackground = true, name = "Dashboard Loading State")
@Composable @Composable
fun DashboardContentLoadingPreview() { fun DashboardContentLoadingPreview() {
@@ -376,23 +469,23 @@ fun DashboardContentLoadingPreview() {
DashboardContent( DashboardContent(
uiState = DashboardUiState.Loading, uiState = DashboardUiState.Loading,
onLocationClick = {}, onLocationClick = {},
onLabelClick = {} onLabelClick = {},
) )
} }
} }
// [END_ENTITY: Function('DashboardContentLoadingPreview')] // [END_ANCHOR:DashboardContentLoadingPreview]
// [ENTITY: Function('DashboardContentErrorPreview')] // [ANCHOR:DashboardContentErrorPreview:Function]
@Preview(showBackground = true, name = "Dashboard Error State") @Preview(showBackground = true, name = "Dashboard Error State")
@Composable @Composable
fun DashboardContentErrorPreview() { fun DashboardContentErrorPreview() {
HomeboxLensTheme { HomeboxLensTheme {
DashboardContent( DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)), uiState = DashboardUiState.Error(stringResource(id = com.homebox.lens.feature.dashboard.R.string.error_loading_failed)),
onLocationClick = {}, onLocationClick = {},
onLabelClick = {} onLabelClick = {},
) )
} }
} }
// [END_ENTITY: Function('DashboardContentErrorPreview')] // [END_ANCHOR:DashboardContentErrorPreview]
// [END_FILE_DashboardScreen.kt] // [END_FILE_DashboardScreen.kt]

View File

@@ -10,46 +10,46 @@ import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount import com.homebox.lens.domain.model.LocationOutCount
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: SealedInterface('DashboardUiState')] // [ANCHOR:DashboardUiState:SealedInterface]
/** // [CONTRACT:DashboardUiState]
* @summary Определяет все возможные состояния для экрана "Дэшборд". // [PURPOSE] Определяет все возможные состояния для экрана "Дэшборд".
* @invariant В любой момент времени экран может находиться только в одном из этих состояний. // [INVARIANT] В любой момент времени экран может находиться только в одном из этих состояний.
*/ // [END_CONTRACT:DashboardUiState]
sealed interface DashboardUiState { sealed interface DashboardUiState {
// [ENTITY: DataClass('Success')] // [ANCHOR:Success:DataClass]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')] // [RELATION:DEPENDS_ON:GroupStatistics]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')] // [RELATION:DEPENDS_ON:LocationOutCount]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')] // [RELATION:DEPENDS_ON:LabelOut]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')] // [RELATION:DEPENDS_ON:ItemSummary]
/** // [CONTRACT:Success]
* @summary Состояние успешной загрузки данных. // [PURPOSE] Состояние успешной загрузки данных.
* @param statistics Статистика по инвентарю. // [PARAM:statistics:GroupStatistics] Статистика по инвентарю.
* @param locations Список локаций со счетчиками. // [PARAM:locations:List<LocationOutCount>] Список локаций со счетчиками.
* @param labels Список всех меток. // [PARAM:labels:List<LabelOut>] Список всех меток.
* @param recentlyAddedItems Список недавно добавленных товаров. // [PARAM:recentlyAddedItems:List<ItemSummary>] Список недавно добавленных товаров.
*/ // [END_CONTRACT:Success]
data class Success( data class Success(
val statistics: GroupStatistics, val statistics: GroupStatistics,
val locations: List<LocationOutCount>, val locations: List<LocationOutCount>,
val labels: List<LabelOut>, val labels: List<LabelOut>,
val recentlyAddedItems: List<ItemSummary> val recentlyAddedItems: List<ItemSummary>,
) : DashboardUiState ) : DashboardUiState
// [END_ENTITY: DataClass('Success')] // [END_ANCHOR:Success]
// [ENTITY: DataClass('Error')] // [ANCHOR:Error:DataClass]
/** // [CONTRACT:Error]
* @summary Состояние ошибки во время загрузки данных. // [PURPOSE] Состояние ошибки во время загрузки данных.
* @param message Человекочитаемое сообщение об ошибке. // [PARAM:message:String] Человекочитаемое сообщение об ошибке.
*/ // [END_CONTRACT:Error]
data class Error(val message: String) : DashboardUiState data class Error(val message: String) : DashboardUiState
// [END_ENTITY: DataClass('Error')] // [END_ANCHOR:Error]
// [ENTITY: Object('Loading')] // [ANCHOR:Loading:Object]
/** // [CONTRACT:Loading]
* @summary Состояние, когда данные для экрана загружаются. // [PURPOSE] Состояние, когда данные для экрана загружаются.
*/ // [END_CONTRACT:Loading]
data object Loading : DashboardUiState data object Loading : DashboardUiState
// [END_ENTITY: Object('Loading')] // [END_ANCHOR:Loading]
} }
// [END_ENTITY: SealedInterface('DashboardUiState')] // [END_ANCHOR:DashboardUiState]
// [END_FILE_DashboardUiState.kt] // [END_FILE_DashboardUiState.kt]

View File

@@ -17,71 +17,67 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// [END_IMPORTS] // [END_IMPORTS]
// [ANCHOR:DashboardViewModel:ViewModel]
// [ENTITY: ViewModel('DashboardViewModel')] // [RELATION:DEPENDS_ON:GetStatisticsUseCase]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')] // [RELATION:DEPENDS_ON:GetAllLocationsUseCase]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')] // [RELATION:DEPENDS_ON:GetAllLabelsUseCase]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')] // [RELATION:DEPENDS_ON:GetRecentlyAddedItemsUseCase]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')] // [RELATION:EMITS_STATE:DashboardUiState]
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')] // [CONTRACT:DashboardViewModel]
/** // [PURPOSE] ViewModel для главного экрана (Dashboard). Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
* @summary ViewModel для главного экрана (Dashboard). // [INVARIANT] `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний // [END_CONTRACT:DashboardViewModel]
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/
@HiltViewModel @HiltViewModel
class DashboardViewModel @Inject constructor( class DashboardViewModel
private val getStatisticsUseCase: GetStatisticsUseCase, @Inject
private val getAllLocationsUseCase: GetAllLocationsUseCase, constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase, private val getStatisticsUseCase: GetStatisticsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase private val getAllLocationsUseCase: GetAllLocationsUseCase,
) : ViewModel() { private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase,
) : ViewModel() {
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
val uiState = _uiState.asStateFlow()
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading) // [ANCHOR:loadDashboardData:Function]
val uiState = _uiState.asStateFlow() // [CONTRACT:loadDashboardData]
// [PURPOSE] Загружает все необходимые данные для экрана Dashboard. Выполняет UseCase'ы параллельно и обновляет UI, переключая его между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
// [SIDE_EFFECT] Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
// [END_CONTRACT:loadDashboardData]
fun loadDashboardData() {
if (uiState.value is DashboardUiState.Success || uiState.value is DashboardUiState.Loading) {
Timber.i("[INFO][SKIP][already_loaded] Dashboard data load skipped - already in progress or loaded.")
return
}
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.")
val statsFlow = flow { emit(getStatisticsUseCase()) }
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
val labelsFlow = flow { emit(getAllLabelsUseCase()) }
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
// [ENTITY: Function('loadDashboardData')] combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
/** DashboardUiState.Success(
* @summary Загружает все необходимые данные для экрана Dashboard. statistics = stats,
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его locations = locations,
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. labels = labels,
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. recentlyAddedItems = recentItems,
*/ )
fun loadDashboardData() { }.catch { exception ->
if (uiState.value is DashboardUiState.Success || uiState.value is DashboardUiState.Loading) { Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
Timber.i("[INFO][SKIP][already_loaded] Dashboard data load skipped - already in progress or loaded.") _uiState.value =
return DashboardUiState.Error(
} message = exception.message ?: "Could not load dashboard data.",
viewModelScope.launch { )
_uiState.value = DashboardUiState.Loading }.collect { successState ->
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.") Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
_uiState.value = successState
val statsFlow = flow { emit(getStatisticsUseCase()) } }
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
val labelsFlow = flow { emit(getAllLabelsUseCase()) }
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
DashboardUiState.Success(
statistics = stats,
locations = locations,
labels = labels,
recentlyAddedItems = recentItems
)
}.catch { exception ->
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
_uiState.value = DashboardUiState.Error(
message = exception.message ?: "Could not load dashboard data."
)
}.collect { successState ->
Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
_uiState.value = successState
} }
} }
// [END_ANCHOR:loadDashboardData]
} }
// [END_ENTITY: Function('loadDashboardData')] // [END_ANCHOR:DashboardViewModel]
}
// [END_ENTITY: ViewModel('DashboardViewModel')]
// [END_FILE_DashboardViewModel.kt] // [END_FILE_DashboardViewModel.kt]

View File

@@ -2,59 +2,56 @@
// [FILE] NavGraph.kt // [FILE] NavGraph.kt
// [SEMANTICS] navigation, compose, nav_host // [SEMANTICS] navigation, compose, nav_host
package com.homebox.lens.navigation package com.homebox.lens.feature.dashboard.navigation
// [IMPORTS] // [IMPORTS]
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.homebox.lens.feature.dashboard.addDashboardScreen import com.homebox.lens.feature.dashboard.addDashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen import com.homebox.lens.ui.common.NavigationActions
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen import com.homebox.lens.feature.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen import com.homebox.lens.feature.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen import com.homebox.lens.feature.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labeledit.LabelEditScreen import com.homebox.lens.feature.labeledit.LabelEditScreen
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen import com.homebox.lens.feature.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen import com.homebox.lens.feature.locationedit.LocationEditScreen
import com.homebox.lens.ui.screen.search.SearchScreen import com.homebox.lens.feature.locationslist.LocationsListScreen
import com.homebox.lens.ui.screen.setup.SetupScreen
import com.homebox.lens.feature.scan.ScanScreen import com.homebox.lens.feature.scan.ScanScreen
import com.homebox.lens.ui.common.MainScaffold import com.homebox.lens.feature.search.SearchScreen
import com.homebox.lens.ui.theme.HomeboxLensTheme import com.homebox.lens.feature.settings.SettingsScreen
// import com.homebox.lens.ui.screen.settings.SettingsScreen import com.homebox.lens.feature.setup.SetupScreen
// [END_IMPORTS] // [END_IMPORTS]
// [ENTITY: Function('NavGraph')] // [ANCHOR:NavGraph:Function]
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')] // [RELATION:DEPENDS_ON:NavHostController]
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')] // [RELATION:CREATES_INSTANCE_OF:NavigationActions]
/** // [CONTRACT:NavGraph]
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. // [PURPOSE] Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
* @param navController Контроллер навигации. // [PARAM:navController:NavHostController] Контроллер навигации.
* @see Screen // [SEE] Screen
* @sideeffect Регистрирует все экраны и управляет состоянием навигации. // [SIDE_EFFECT] Регистрирует все экраны и управляет состоянием навигации.
* @invariant Стартовый экран - `Screen.Setup`. // [INVARIANT] Стартовый экран - `Screen.Setup`.
*/ // [END_CONTRACT:NavGraph]
@Composable @Composable
fun NavGraph( fun navGraph(navController: NavHostController = rememberNavController()) {
navController: NavHostController = rememberNavController()
) {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
val navigationActions = remember(navController) { val navigationActions =
NavigationActions(navController) remember(navController) {
} NavigationActions(navController)
}
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = Screen.Setup.route startDestination = Screen.Setup.route,
) { ) {
composable(route = Screen.Setup.route) { composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = { SetupScreen(onSetupComplete = {
@@ -70,98 +67,89 @@ fun NavGraph(
navigateToSearch = navigationActions::navigateToSearch, navigateToSearch = navigationActions::navigateToSearch,
navigateToInventoryListWithLocation = navigationActions::navigateToInventoryListWithLocation, navigateToInventoryListWithLocation = navigationActions::navigateToInventoryListWithLocation,
navigateToInventoryListWithLabel = navigationActions::navigateToInventoryListWithLabel, navigateToInventoryListWithLabel = navigationActions::navigateToInventoryListWithLabel,
MainScaffoldContent = { topBarTitle, currentRoute, topBarActions, content -> navigationActions = navigationActions,
MainScaffold( navController = navController,
topBarTitle = topBarTitle,
currentRoute = currentRoute,
navigationActions = navigationActions,
topBarActions = topBarActions,
content = content
)
},
HomeboxLensTheme = { content -> HomeboxLensTheme(content = content) }
) )
composable(route = Screen.InventoryList.route) { composable(route = Screen.InventoryList.route) {
InventoryListScreen( InventoryListScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions,
) )
} }
composable(route = Screen.ItemDetails.route) { composable(
route = Screen.ItemDetails.route,
arguments = listOf(navArgument("itemId") { nullable = true }),
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")
ItemDetailsScreen( ItemDetailsScreen(
itemId = itemId,
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions,
) )
} }
composable( composable(
route = Screen.ItemEdit.route, route = Screen.ItemEdit.route,
arguments = listOf(navArgument("itemId") { nullable = true }) arguments = listOf(navArgument("itemId") { nullable = true }),
) { backStackEntry -> ) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId") val itemId = backStackEntry.arguments?.getString("itemId")
ItemEditScreen( ItemEditScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions, navigationActions = navigationActions,
itemId = itemId, itemId = itemId,
onSaveSuccess = { navController.popBackStack() } onSaveSuccess = { navController.popBackStack() },
) )
} }
composable(Screen.LabelsList.route) { composable(Screen.LabelsList.route) {
LabelsListScreen( LabelsListScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions,
) )
} }
composable(route = Screen.LocationsList.route) { composable(route = Screen.LocationsList.route) {
LocationsListScreen( LocationsListScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions, navigationActions = navigationActions,
onLocationClick = { locationId -> onLocationClick = { locationId: String ->
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen // [AI_NOTE]: Navigate to a pre-filtered inventory list screen
navController.navigate(Screen.InventoryList.route) navigationActions.navigateToInventoryListWithLocation(locationId)
}, },
onAddNewLocationClick = { onAddNewLocationClick = {
navController.navigate(Screen.LocationEdit.createRoute("new")) navController.navigate(Screen.LocationEdit.createRoute("new"))
} },
) )
} }
composable(route = Screen.LocationEdit.route) { backStackEntry -> composable(route = Screen.LocationEdit.route) { backStackEntry ->
val locationId = backStackEntry.arguments?.getString("locationId") val locationId = backStackEntry.arguments?.getString("locationId")
LocationEditScreen( LocationEditScreen(
locationId = locationId locationId = locationId,
)
}
composable(route = Screen.LocationEdit.route) { backStackEntry ->
val locationId = backStackEntry.arguments?.getString("locationId")
LocationEditScreen(
locationId = locationId
) )
} }
composable( composable(
route = Screen.LabelEdit.route, route = Screen.LabelEdit.route,
arguments = listOf(navArgument("labelId") { nullable = true }) arguments = listOf(navArgument("labelId") { nullable = true }),
) { backStackEntry -> ) { backStackEntry ->
val labelId = backStackEntry.arguments?.getString("labelId") val labelId = backStackEntry.arguments?.getString("labelId")
LabelEditScreen( LabelEditScreen(
labelId = labelId, labelId = labelId,
onBack = { navController.popBackStack() }, onBack = { navController.popBackStack() },
onLabelSaved = { navController.popBackStack() } onLabelSaved = { navController.popBackStack() },
) )
} }
composable(route = Screen.Search.route) { composable(route = Screen.Search.route) {
SearchScreen( SearchScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions navigationActions = navigationActions,
) )
} }
composable(Screen.Settings.route) { composable(Screen.Settings.route) {
com.homebox.lens.ui.screen.settings.SettingsScreen( SettingsScreen(
currentRoute = currentRoute, currentRoute = currentRoute,
navigationActions = navigationActions, navigationActions = navigationActions,
onNavigateUp = { navController.navigateUp() } onNavigateUp = { navController.navigateUp() },
) )
} }
composable(Screen.Scan.route) { backStackEntry -> composable(Screen.Scan.route) { backStackEntry ->
ScanScreen(onBarcodeResult = { barcode -> ScanScreen(onBarcodeResult = { barcode: String ->
val previousBackStackEntry = navController.previousBackStackEntry val previousBackStackEntry = navController.previousBackStackEntry
previousBackStackEntry?.savedStateHandle?.set("barcodeResult", barcode) previousBackStackEntry?.savedStateHandle?.set("barcodeResult", barcode)
navController.popBackStack() navController.popBackStack()
@@ -169,5 +157,5 @@ fun NavGraph(
} }
} }
} }
// [END_ENTITY: Function('NavGraph')] // [END_ANCHOR:NavGraph]
// [END_FILE_NavGraph.kt] // [END_FILE_NavGraph.kt]

View File

@@ -0,0 +1,24 @@
package com.homebox.lens.feature.dashboard.navigation
sealed class Screen(val route: String) {
object Dashboard : Screen("dashboard")
object InventoryList : Screen("inventoryList")
object ItemDetails : Screen("itemDetails/{itemId}") {
fun createRoute(itemId: String) = "itemDetails/$itemId"
}
object ItemEdit : Screen("itemEdit?itemId={itemId}") {
fun createRoute(itemId: String?) = "itemEdit" + (itemId?.let { "?itemId=$it" } ?: "")
}
object LabelEdit : Screen("labelEdit?labelId={labelId}") {
fun createRoute(labelId: String?) = "labelEdit" + (labelId?.let { "?labelId=$it" } ?: "")
}
object LabelsList : Screen("labelsList")
object LocationEdit : Screen("locationEdit?locationId={locationId}") {
fun createRoute(locationId: String?) = "locationEdit" + (locationId?.let { "?locationId=$it" } ?: "")
}
object LocationsList : Screen("locationsList")
object Scan : Screen("scan")
object Search : Screen("search")
object Settings : Screen("settings")
object Setup : Screen("setup")
}

View File

@@ -1,7 +1,7 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Color.kt // [FILE] Color.kt
// [SEMANTICS] ui, theme, color // [SEMANTICS] ui, theme, color
package com.homebox.lens.ui.theme
package com.homebox.lens.feature.dashboard.ui.theme
// [IMPORTS] // [IMPORTS]
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View File

@@ -0,0 +1,93 @@
// [FILE] Theme.kt
// [SEMANTICS] ui, theme
package com.homebox.lens.feature.dashboard.ui.theme
// [IMPORTS]
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import timber.log.Timber
// [END_IMPORTS]
private val DarkColorScheme =
darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
)
private val LightColorScheme =
lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
)
// [ANCHOR:HomeboxLensTheme:Function]
// [RELATION:DEPENDS_ON:Typography]
// [RELATION:DEPENDS_ON:Color]
// [CONTRACT:HomeboxLensTheme]
// [PURPOSE] The main theme for the Homebox Lens application.
// [PARAM:darkTheme:Boolean] Whether the theme should be dark or light.
// [PARAM:dynamicColor:Boolean] Whether to use dynamic color (on Android 12+).
// [PARAM:content:(@Composable () -> Unit)] The content to be displayed within the theme.
// [SIDE_EFFECT] Sets the status bar color based on the theme.
// [END_CONTRACT:HomeboxLensTheme]
@Composable
fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit,
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) {
Timber.i("[INFO][THEME][dynamic_dark_theme]", "Applying dynamic dark theme")
dynamicDarkColorScheme(context)
} else {
Timber.i("[INFO][THEME][dynamic_light_theme]", "Applying dynamic light theme")
dynamicLightColorScheme(context)
}
}
darkTheme -> {
Timber.i("[INFO][THEME][dark_theme]", "Applying static dark theme")
DarkColorScheme
}
else -> {
Timber.i("[INFO][THEME][light_theme]", "Applying static light theme")
LightColorScheme
}
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
Timber.i("[INFO][THEME][status_bar_color]", "Setting status bar color", "color", colorScheme.primary.toArgb())
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
// [END_ANCHOR:HomeboxLensTheme]
// [END_FILE_Theme.kt]

View File

@@ -0,0 +1,30 @@
// [FILE] Typography.kt
// [SEMANTICS] ui, theme, typography
package com.homebox.lens.feature.dashboard.ui.theme
// [IMPORTS]
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// [END_IMPORTS]
// [ANCHOR:Typography:DataStructure]
// [CONTRACT:Typography]
// [PURPOSE] Defines the typography for the application.
// [END_CONTRACT:Typography]
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
)
// [END_ANCHOR:Typography]
// [END_FILE_Typography.kt]

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Dashboard Screen --> <!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string> <string name="dashboard_title">Главная</string>
@@ -16,10 +15,7 @@
<!-- Common --> <!-- Common -->
<string name="items_not_found">Элементы не найдены</string> <string name="items_not_found">Элементы не найдены</string>
<string name="no_location">Нет локации</string>
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string> <string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
<!-- Content Descriptions -->
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string> <string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
<string name="cd_search">Поиск</string> <string name="cd_search">Поиск</string>
</resources> </resources>

View File

@@ -0,0 +1,50 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.inventorylist"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,20 @@
package com.homebox.lens.feature.inventorylist
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.homebox.lens.ui.common.mainScaffold
@Composable
fun InventoryListScreen(
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
) {
mainScaffold(
topBarTitle = "Inventory",
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
Text(text = "Inventory List Screen")
}
}

View File

@@ -0,0 +1,54 @@
// [FILE] feature/itemdetails/build.gradle.kts
// [SEMANTICS] build, itemdetails, feature_module
// [PURPOSE] Build script for the feature:itemdetails module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.itemdetails"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,27 @@
// [FILE] feature/itemdetails/src/main/java/com/homebox/lens/feature/itemdetails/ItemDetailsScreen.kt
// [SEMANTICS] ui, screen, item, details
// [PURPOSE] Composable for the Item Details screen.
package com.homebox.lens.feature.itemdetails
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.homebox.lens.ui.common.mainScaffold
// [ANCHOR:ItemDetailsScreen:Function]
@Composable
fun ItemDetailsScreen(
itemId: String?,
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
) {
mainScaffold(
topBarTitle = "Item Details",
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
Text(text = "Item Details Screen")
}
}
// [END_ANCHOR:ItemDetailsScreen]
// [END_FILE_feature/itemdetails/src/main/java/com/homebox/lens/feature/itemdetails/ItemDetailsScreen.kt]

View File

@@ -0,0 +1,55 @@
// [FILE] feature/itemedit/build.gradle.kts
// [SEMANTICS] build, itemedit, feature_module
// [PURPOSE] Build script for the feature:itemedit module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.itemedit"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,25 @@
// [FILE] feature/itemedit/src/main/java/com/homebox/lens/feature/itemedit/ItemEditScreen.kt
// [SEMANTICS] ui, screen, item, edit
// [PURPOSE] Composable for the Item Edit screen.
package com.homebox.lens.feature.itemedit
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.homebox.lens.ui.common.mainScaffold
@Composable
fun ItemEditScreen(
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
itemId: String?,
onSaveSuccess: () -> Unit,
) {
mainScaffold(
topBarTitle = "Edit Item",
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
Text(text = "Item Edit Screen")
}
}

View File

@@ -0,0 +1,54 @@
// [FILE] feature/labeledit/build.gradle.kts
// [SEMANTICS] build, labeledit, feature_module
// [PURPOSE] Build script for the feature:labeledit module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.labeledit"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,17 @@
// [FILE] feature/labeledit/src/main/java/com/homebox/lens/feature/labeledit/LabelEditScreen.kt
// [SEMANTICS] ui, screen, label, edit
// [PURPOSE] Composable for the Label Edit screen.
package com.homebox.lens.feature.labeledit
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun LabelEditScreen(
labelId: String?,
onBack: () -> Unit,
onLabelSaved: () -> Unit,
) {
Text(text = "Label Edit Screen")
}

View File

@@ -0,0 +1,54 @@
// [FILE] feature/labelslist/build.gradle.kts
// [SEMANTICS] build, labelslist, feature_module
// [PURPOSE] Build script for the feature:labelslist module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.labelslist"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,23 @@
// [FILE] feature/labelslist/src/main/java/com/homebox/lens/feature/labelslist/LabelsListScreen.kt
// [SEMANTICS] ui, screen, labels, list
// [PURPOSE] Composable for the Labels List screen.
package com.homebox.lens.feature.labelslist
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.homebox.lens.ui.common.mainScaffold
@Composable
fun LabelsListScreen(
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
) {
mainScaffold(
topBarTitle = "Labels",
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
Text(text = "Labels List Screen")
}
}

View File

@@ -0,0 +1,53 @@
// [FILE] feature/locationedit/build.gradle.kts
// [SEMANTICS] build, locationedit, feature_module
// [PURPOSE] Build script for the feature:locationedit module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.locationedit"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,15 @@
// [FILE] feature/locationedit/src/main/java/com/homebox/lens/feature/locationedit/LocationEditScreen.kt
// [SEMANTICS] ui, screen, location, edit
// [PURPOSE] Composable for the Location Edit screen.
package com.homebox.lens.feature.locationedit
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun LocationEditScreen(
locationId: String?,
) {
Text(text = "Location Edit Screen")
}

View File

@@ -0,0 +1,54 @@
// [FILE] feature/locationslist/build.gradle.kts
// [SEMANTICS] build, locationslist, feature_module
// [PURPOSE] Build script for the feature:locationslist module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.locationslist"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,25 @@
// [FILE] feature/locationslist/src/main/java/com/homebox/lens/feature/locationslist/LocationsListScreen.kt
// [SEMANTICS] ui, screen, locations, list
// [PURPOSE] Composable for the Locations List screen.
package com.homebox.lens.feature.locationslist
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.homebox.lens.ui.common.mainScaffold
@Composable
fun LocationsListScreen(
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
onLocationClick: (String) -> Unit,
onAddNewLocationClick: () -> Unit,
) {
mainScaffold(
topBarTitle = "Locations",
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
Text(text = "Locations List Screen")
}
}

View File

@@ -0,0 +1,54 @@
// [FILE] feature/search/build.gradle.kts
// [SEMANTICS] build, search, feature_module
// [PURPOSE] Build script for the feature:search module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.search"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,23 @@
// [FILE] feature/search/src/main/java/com/homebox/lens/feature/search/SearchScreen.kt
// [SEMANTICS] ui, screen, search
// [PURPOSE] Composable for the Search screen.
package com.homebox.lens.feature.search
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.homebox.lens.ui.common.mainScaffold
@Composable
fun SearchScreen(
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
) {
mainScaffold(
topBarTitle = "Search",
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
Text(text = "Search Screen")
}
}

View File

@@ -0,0 +1,55 @@
// [FILE] feature/settings/build.gradle.kts
// [SEMANTICS] build, settings, feature_module
// [PURPOSE] Build script for the feature:settings module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.settings"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

View File

@@ -0,0 +1,25 @@
// [FILE] feature/settings/src/main/java/com/homebox/lens/feature/settings/SettingsScreen.kt
// [SEMANTICS] ui, screen, settings
// [PURPOSE] Composable for the Settings screen.
package com.homebox.lens.feature.settings
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import com.homebox.lens.ui.common.mainScaffold
@Composable
fun SettingsScreen(
currentRoute: String?,
navigationActions: com.homebox.lens.ui.common.NavigationActions,
onNavigateUp: () -> Unit,
) {
mainScaffold(
topBarTitle = "Settings",
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateUp = onNavigateUp,
) {
Text(text = "Settings Screen")
}
}

View File

@@ -0,0 +1,54 @@
// [FILE] feature/setup/build.gradle.kts
// [SEMANTICS] build, setup, feature_module
// [PURPOSE] Build script for the feature:setup module.
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
id("com.google.dagger.hilt.android")
id("kotlin-kapt")
}
android {
namespace = "com.homebox.lens.feature.setup"
compileSdk = Versions.compileSdk
defaultConfig {
minSdk = Versions.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":ui:common"))
implementation(Libs.coreKtx)
implementation(Libs.lifecycleRuntime)
implementation(Libs.activityCompose)
implementation(Libs.composeUi)
implementation(Libs.composeUiGraphics)
implementation(Libs.composeUiToolingPreview)
implementation(Libs.composeMaterial3)
implementation(Libs.composeMaterialIconsExtended)
implementation(Libs.navigationCompose)
implementation(Libs.hiltNavigationCompose)
implementation(Libs.hiltAndroid)
kapt(Libs.hiltCompiler)
implementation(Libs.timber)
}

Some files were not shown because too many files have changed in this diff Show More