REFACTOR END
@@ -64,12 +64,11 @@
|
||||
### Шаг 3: Синтез плана и WorkOrder
|
||||
1. Сгенерировать детальный план в Markdown.
|
||||
2. Представить план пользователю для одобрения.
|
||||
3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.xml`.
|
||||
3. **Параллельно**, формализовать план как машиночитаемый `WorkOrder.md`.
|
||||
|
||||
### Шаг 4: Ожидание одобрения
|
||||
**ОСТАНОВИТЬ ВЫПОЛНЕНИЕ.** Ждать от человека явной, утверждающей команды.
|
||||
|
||||
### Шаг 5: Инициация разработки
|
||||
1. Обновить `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`.
|
||||
2. Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.xml`).
|
||||
Создать задачу для `Code` агента (например, путем создания файла `tasks/new_task.md`). Включить в задачу обновление `tech_spec/PROJECT_MANIFEST.xml` на основе `WorkOrder`.
|
||||
[/MASTER_WORKFLOW]
|
||||
@@ -26,32 +26,35 @@
|
||||
[RULES]
|
||||
- [RULE] CONSTRAINT: Весь генерируемый код ДОЛЖЕН на 100% соответствовать `semantic_enrichment_protocol.md`.
|
||||
- [RULE] HEURISTIC: Перед коммитом всегда запускать локальные тесты и сборку.
|
||||
- [RULE] CONSTRAINT: Если `validate_semantics.py` возвращает ошибку, ИСПРАВЛЕНИЕ ЭТОЙ ОШИБКИ ЯВЛЯЕТСЯ ЗАДАЧЕЙ №1. Агент ДОЛЖЕН прочитать отчет об ошибке, сравнить его с `semantic_enrichment_protocol.md` и исправить код. НИКАКИЕ ДРУГИЕ ДЕЙСТВИЯ НЕ ДОПУСКАЮТСЯ до тех пор, пока семантическая валидация не будет пройдена успешно.
|
||||
[/RULES]
|
||||
[/GRACE_FRAMEWORK]
|
||||
|
||||
[MASTER_WORKFLOW]
|
||||
### Шаг 1: Поиск и принятие задачи
|
||||
1. Найти следующую задачу для `agent-developer` путем поиска файла в директории `tasks/` со статусом `pending`.
|
||||
2. Прочитать файл задачи (`WorkOrder`) с помощью `read_file`.
|
||||
3. Изменить статус задачи на `in-progress` с помощью `apply_diff`.
|
||||
### Шаг 1: Поиск и Принятие Задачи
|
||||
1. Найти `WorkOrder` в `tasks/` со статусом `pending`.
|
||||
2. Прочитать `WorkOrder` и изменить его статус на `in-progress`.
|
||||
3. Создать новую ветку для разработки.
|
||||
|
||||
### Шаг 2: Реализация
|
||||
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`).
|
||||
### Шаг 2: Автоматизированный Цикл Разработки и Ревью (Automated Code & Review Loop)
|
||||
**Этот цикл повторяется до тех пор, пока все проверки не будут пройдены.**
|
||||
|
||||
### Шаг 3: Создание Pull Request и задачи для QA
|
||||
1. Закоммитить изменения (`execute_command git commit ...`).
|
||||
2. Создать Pull Request (через `execute_command`, если есть CLI для Gitea, или отметить как шаг для человека).
|
||||
3. Создать задачу для QA (написать файл `tasks/qa_task_...xml` с помощью `write_to_file`).
|
||||
4. Обновить статус основной задачи на `pending-qa` (`apply_diff`).
|
||||
1. **Реализация Кода:** Внести изменения в кодовую базу согласно `WorkOrder`.
|
||||
|
||||
2. **Семантическая Валидация:**
|
||||
a. Для каждого измененного файла запустить `python validate_semantics.py <file_path>`.
|
||||
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]
|
||||
|
||||
[SELF_REFLECTION_PROTOCOL]
|
||||
|
||||
@@ -39,25 +39,21 @@
|
||||
[/GRACE_FRAMEWORK]
|
||||
|
||||
[MASTER_WORKFLOW]
|
||||
### Шаг 1: Поиск задачи на тестирование
|
||||
1. Найти в директории `tasks/` файл задачи со статусом `pending-qa`.
|
||||
2. Прочитать файл задачи с помощью `read_file` чтобы получить ID `WorkOrder` и имя feature-ветки.
|
||||
### Шаг 1: Поиск и Принятие Задачи
|
||||
1. Найти `WorkOrder` в `tasks/` со статусом `pending-qa`.
|
||||
2. Прочитать `WorkOrder` и информацию о Pull Request.
|
||||
3. Изменить статус задачи на `final-review`.
|
||||
|
||||
### Шаг 2: Сбор контекста и подготовка
|
||||
1. Прочитать исходный `WorkOrder` (`tasks/workorder_{id}.xml`).
|
||||
2. Переключиться на feature-ветку (`execute_command git checkout ...`).
|
||||
3. Прочитать измененные файлы.
|
||||
|
||||
### Шаг 3: Статический и динамический анализ
|
||||
1. Проверить код на соответствие `semantic_enrichment_protocol.md`.
|
||||
2. Запустить тесты и сборку (`execute_command ./gradlew build`).
|
||||
|
||||
### Шаг 4: Вынесение вердикта
|
||||
**ЕСЛИ** анализ на шаге 3 успешен:
|
||||
1. Обновить статус задачи на `approved` с помощью `apply_diff`.
|
||||
2. Опционально: инициировать слияние ветки (`execute_command git merge ...`).
|
||||
### Шаг 2: Финальное Утверждение
|
||||
1. **Проверка Pull Request:** Провести высокоуровневый обзор изменений в PR. Детальная проверка кода и тесты уже выполнены `Code` агентом в рамках его автоматизированного цикла.
|
||||
2. **Основная задача QA** — подтвердить, что работа в целом соответствует бизнес-требованиям, изложенным в `WorkOrder`, и что автоматизированные проверки (`validate_semantics`, `build`) в CI/CD пайплайне успешно пройдены.
|
||||
|
||||
**ИНАЧЕ (если есть проблемы):**
|
||||
1. Создать детальный отчет `reports/defect_report_{id}.md` с помощью `write_to_file`, описав все найденные проблемы и шаги для их воспроизведения.
|
||||
2. Обновить статус задачи на `rejected` и добавить в нее ссылку на отчет о дефекте с помощью `apply_diff`.
|
||||
### Шаг 3: Завершение
|
||||
1. **Если все в порядке:**
|
||||
a. Влить (merge) Pull Request в основную ветку.
|
||||
b. Обновить статус `WorkOrder` на `completed`.
|
||||
c. Удалить ветку разработки.
|
||||
2. **Если обнаружены критические проблемы:**
|
||||
a. Отклонить Pull Request с четким объяснением.
|
||||
b. Вернуть `WorkOrder` в статус `pending` для `Code` агента.
|
||||
[/MASTER_WORKFLOW]
|
||||
@@ -7,7 +7,6 @@ plugins {
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.dagger.hilt.android")
|
||||
id("kotlin-kapt")
|
||||
// id("org.jlleitschuh.gradle.ktlint") version "12.1.1"
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -62,6 +61,16 @@ dependencies {
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":feature:scan"))
|
||||
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
|
||||
implementation(Libs.coreKtx)
|
||||
@@ -74,11 +83,10 @@ dependencies {
|
||||
implementation(Libs.composeUiGraphics)
|
||||
implementation(Libs.composeUiToolingPreview)
|
||||
implementation(Libs.composeMaterial3)
|
||||
implementation("androidx.compose.material:material-icons-extended-android:1.6.8")
|
||||
implementation(Libs.composeMaterialIconsExtended)
|
||||
implementation(Libs.navigationCompose)
|
||||
implementation(Libs.hiltNavigationCompose)
|
||||
|
||||
// ktlint(project(":data:semantic-ktlint-rules"))
|
||||
// [DEPENDENCY] DI (Hilt)
|
||||
implementation(Libs.hiltAndroid)
|
||||
kapt(Libs.hiltCompiler)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// [PACKAGE] com.homebox.lens
|
||||
// [FILE] MainActivity.kt
|
||||
// [FILE] app/src/main/java/com/homebox/lens/MainActivity.kt
|
||||
// [SEMANTICS] ui, activity, entrypoint
|
||||
package com.homebox.lens
|
||||
|
||||
@@ -14,21 +13,31 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.homebox.lens.navigation.NavGraph
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
|
||||
import com.homebox.lens.feature.dashboard.navigation.navGraph
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Activity('MainActivity')]
|
||||
|
||||
/**
|
||||
* @summary Главная и единственная Activity в приложении.
|
||||
*/
|
||||
// [ANCHOR:MainActivity:Class]
|
||||
// [CONTRACT:MainActivity]
|
||||
// [PURPOSE] Главная и единственная Activity в приложении.
|
||||
// [END_CONTRACT:MainActivity]
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
// [ENTITY: Function('onCreate')]
|
||||
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')]
|
||||
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')]
|
||||
// [ANCHOR:onCreate:Function]
|
||||
// [CONTRACT:onCreate]
|
||||
// [PURPOSE] Инициализация Activity.
|
||||
// [PARAM:savedInstanceState:Bundle?] Сохраненное состояние.
|
||||
// [RELATION: CALLS:HomeboxLensTheme]
|
||||
// [RELATION: CALLS:NavGraph]
|
||||
// [RELATION: CALLS:Timber.d]
|
||||
// [END_CONTRACT:onCreate]
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
|
||||
@@ -36,35 +45,48 @@ class MainActivity : ComponentActivity() {
|
||||
HomeboxLensTheme {
|
||||
Surface(
|
||||
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')]
|
||||
// [ANCHOR:greeting:Function]
|
||||
// [CONTRACT:greeting]
|
||||
// [PURPOSE] Отображает приветствие.
|
||||
// [PARAM:name:String] Имя для приветствия.
|
||||
// [PARAM:modifier:Modifier] Модификатор для элемента.
|
||||
// [END_CONTRACT:greeting]
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
fun greeting(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('Greeting')]
|
||||
// [END_ANCHOR:greeting]
|
||||
|
||||
// [ENTITY: Function('GreetingPreview')]
|
||||
// [ANCHOR:greetingPreview:Function]
|
||||
// [CONTRACT:greetingPreview]
|
||||
// [PURPOSE] Предварительный просмотр функции greeting.
|
||||
// [END_CONTRACT:greetingPreview]
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
fun greetingPreview() {
|
||||
HomeboxLensTheme {
|
||||
Greeting("Android")
|
||||
greeting("Android")
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('GreetingPreview')]
|
||||
|
||||
// [END_FILE_MainActivity.kt]
|
||||
// [END_ANCHOR:greetingPreview]
|
||||
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
|
||||
// [END_FILE_app/src/main/java/com/homebox/lens/MainActivity.kt]
|
||||
|
||||
@@ -10,12 +10,12 @@ import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Application('MainApplication')]
|
||||
|
||||
/**
|
||||
* @summary Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application() {
|
||||
|
||||
// [ENTITY: Function('onCreate')]
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -27,4 +27,4 @@ class MainApplication : Application() {
|
||||
// [END_ENTITY: Function('onCreate')]
|
||||
}
|
||||
// [END_ENTITY: Application('MainApplication')]
|
||||
// [END_FILE_MainApplication.kt]
|
||||
// [END_FILE_MainApplication.kt]
|
||||
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -4,27 +4,27 @@
|
||||
|
||||
// [ENTITY: Object('Versions')]
|
||||
object Versions {
|
||||
const val compileSdk = 36
|
||||
const val minSdk = 26
|
||||
const val targetSdk = 36
|
||||
const val compileSdk = 34
|
||||
const val minSdk = 24
|
||||
const val targetSdk = 34
|
||||
const val versionCode = 1
|
||||
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 composeCompiler = "1.5.8" // this is not used anymore
|
||||
const val composeBom = "2023.10.01" // this is not used anymore
|
||||
const val activityCompose = "1.11.0"
|
||||
const val navigationCompose = "2.9.4"
|
||||
const val hiltNavigationCompose = "1.3.0"
|
||||
const val composeCompiler = "1.5.4"
|
||||
const val composeBom = "2024.05.00"
|
||||
const val activityCompose = "1.8.2"
|
||||
const val navigationCompose = "2.7.7"
|
||||
const val hiltNavigationCompose = "1.1.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 retrofit = "2.9.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 hilt = "2.51.1"
|
||||
const val hiltCompiler = "1.1.0"
|
||||
const val hiltCompiler = "1.2.0"
|
||||
const val timber = "5.0.1"
|
||||
const val junit = "4.13.2"
|
||||
const val extJunit = "1.1.5"
|
||||
@@ -41,10 +41,13 @@ object Libs {
|
||||
const val coreKtx = "androidx.core:core-ktx:${Versions.coreKtx}"
|
||||
const val lifecycleRuntime = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.lifecycle}"
|
||||
const val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"
|
||||
const val composeUi = "androidx.compose.ui:ui:1.9.1"
|
||||
const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.9.1"
|
||||
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.9.1"
|
||||
const val composeMaterial3 = "androidx.compose.material3:material3:1.3.2"
|
||||
const val composeUi = "androidx.compose.ui:ui:1.5.4"
|
||||
const val composeUiGraphics = "androidx.compose.ui:ui-graphics:1.5.4"
|
||||
const val composeUiToolingPreview = "androidx.compose.ui:ui-tooling-preview:1.5.4"
|
||||
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 navigationCompose = "androidx.navigation:navigation-compose:${Versions.navigationCompose}"
|
||||
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 extJunit = "androidx.test.ext:junit:${Versions.extJunit}"
|
||||
const val espressoCore = "androidx.test.espresso:espresso-core:${Versions.espresso}"
|
||||
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.9.1"
|
||||
const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.9.1"
|
||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.9.1"
|
||||
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.5.4"
|
||||
const val composeUiTooling = "androidx.compose.ui:ui-tooling:1.5.4"
|
||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest:1.5.4"
|
||||
const val kotestRunnerJunit5 = "io.kotest:kotest-runner-junit5:${Versions.kotest}"
|
||||
const val kotestAssertionsCore = "io.kotest:kotest-assertions-core:${Versions.kotest}"
|
||||
const val mockk = "io.mockk:mockk:${Versions.mockk}"
|
||||
|
||||
1
data/semantic-ktlint-rules/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -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")
|
||||
}
|
||||
21
data/semantic-ktlint-rules/proguard-rules.pro
vendored
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 982 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,3 +0,0 @@
|
||||
<resources>
|
||||
<string name="app_name">semantic-ktlint-rules</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
com.busya.ktlint.rules.CustomRuleSetProvider
|
||||
@@ -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] ...'.")
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// [FILE] build.gradle.kts
|
||||
// [SEMANTICS] build, configuration, module, feature, dashboard
|
||||
// [FILE] feature/dashboard/build.gradle.kts
|
||||
// [SEMANTICS] build, dashboard, feature_module
|
||||
// [PURPOSE] Build script for the feature:dashboard module.
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
@@ -16,9 +17,6 @@ android {
|
||||
defaultConfig {
|
||||
minSdk = Versions.minSdk
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -26,7 +24,7 @@ android {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +38,6 @@ android {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@@ -49,9 +46,23 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// [MODULE_DEPENDENCY] Core modules
|
||||
implementation(project(":domain"))
|
||||
// [MODULE_DEPENDENCY] Data module
|
||||
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
|
||||
implementation(Libs.coreKtx)
|
||||
@@ -61,9 +72,13 @@ dependencies {
|
||||
// [DEPENDENCY] Compose
|
||||
implementation(Libs.composeUi)
|
||||
implementation(Libs.composeUiGraphics)
|
||||
implementation(Libs.composeFoundation)
|
||||
implementation(Libs.composeUiToolingPreview)
|
||||
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.hiltNavigationCompose)
|
||||
|
||||
@@ -92,4 +107,4 @@ kapt {
|
||||
correctErrorTypes = true
|
||||
}
|
||||
|
||||
// [END_FILE_build.gradle.kts]
|
||||
// [END_FILE_feature/dashboard/build.gradle.kts]
|
||||
|
||||
@@ -4,29 +4,26 @@
|
||||
package com.homebox.lens.feature.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.NavigationActions
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('addDashboardScreen')]
|
||||
// [RELATION: Function('addDashboardScreen')] -> [DEPENDS_ON] -> [Function('DashboardScreen')]
|
||||
/**
|
||||
* @summary Extension function for NavGraphBuilder to add the Dashboard screen to the navigation graph.
|
||||
* @description Registers the Dashboard route and composes the DashboardScreen with appropriate navigation actions and common UI components.
|
||||
* @param route The route string for the Dashboard screen.
|
||||
* @param currentRoute The current navigation route, used for highlighting.
|
||||
* @param navigateToScan Lambda for navigating to the scan screen.
|
||||
* @param navigateToSearch Lambda for navigating to the search screen.
|
||||
* @param navigateToInventoryListWithLocation Lambda for navigating to inventory filtered by location.
|
||||
* @param navigateToInventoryListWithLabel Lambda for navigating to inventory filtered by label.
|
||||
* @param MainScaffoldContent Composable lambda for the main scaffold structure.
|
||||
* @param HomeboxLensTheme Composable lambda for applying the application theme.
|
||||
* @sideeffect Adds a composable route for the Dashboard screen.
|
||||
*/
|
||||
// [ANCHOR:addDashboardScreen:Function]
|
||||
// [RELATION:DEPENDS_ON:DashboardScreen]
|
||||
// [CONTRACT:addDashboardScreen]
|
||||
// [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.
|
||||
// [PARAM:route:String] The route string for the Dashboard screen.
|
||||
// [PARAM:currentRoute:String] The current navigation route, used for highlighting.
|
||||
// [PARAM:navigateToScan:Unit] Lambda for navigating to the scan screen.
|
||||
// [PARAM:navigateToSearch:Unit] Lambda for navigating to the search screen.
|
||||
// [PARAM:navigateToInventoryListWithLocation:Unit] Lambda for navigating to inventory filtered by location.
|
||||
// [PARAM:navigateToInventoryListWithLabel:Unit] Lambda for navigating to inventory filtered by label.
|
||||
// [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
|
||||
// [PARAM:navController:NavHostController] Контроллер навигации.
|
||||
// [SIDE_EFFECT] Adds a composable route for the Dashboard screen.
|
||||
// [END_CONTRACT:addDashboardScreen]
|
||||
fun NavGraphBuilder.addDashboardScreen(
|
||||
route: String,
|
||||
currentRoute: String?,
|
||||
@@ -36,14 +33,6 @@ fun NavGraphBuilder.addDashboardScreen(
|
||||
navigateToInventoryListWithLabel: (String) -> Unit,
|
||||
navigationActions: NavigationActions,
|
||||
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) {
|
||||
DashboardScreen(
|
||||
@@ -52,12 +41,10 @@ fun NavGraphBuilder.addDashboardScreen(
|
||||
navigateToSearch = navigateToSearch,
|
||||
navigateToInventoryListWithLocation = navigateToInventoryListWithLocation,
|
||||
navigateToInventoryListWithLabel = navigateToInventoryListWithLabel,
|
||||
MainScaffoldContent = MainScaffoldContent,
|
||||
HomeboxLensTheme = HomeboxLensTheme,
|
||||
navigationActions = navigationActions,
|
||||
navController = navController
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('addDashboardScreen')]
|
||||
// [END_FILE_DashboardNavigation.kt]
|
||||
// [END_ANCHOR:addDashboardScreen]
|
||||
// [END_FILE_DashboardNavigation.kt]
|
||||
|
||||
@@ -4,10 +4,18 @@
|
||||
package com.homebox.lens.feature.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
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.LazyRow
|
||||
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.filled.QrCodeScanner
|
||||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -29,29 +43,30 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.homebox.lens.domain.model.*
|
||||
import com.homebox.lens.feature.dashboard.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
import com.homebox.lens.ui.common.mainScaffold
|
||||
import com.homebox.lens.ui.common.NavigationActions
|
||||
import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('DashboardScreen')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* @summary Главная Composable-функция для экрана "Панель управления".
|
||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigateToScan Лямбда для навигации на экран сканирования.
|
||||
* @param navigateToSearch Лямбда для навигации на экран поиска.
|
||||
* @param navigateToInventoryListWithLocation Лямбда для навигации на список инвентаря с фильтром по локации.
|
||||
* @param navigateToInventoryListWithLabel Лямбда для навигации на список инвентаря с фильтром по метке.
|
||||
* @param MainScaffoldContent Composable-функция для отображения основного Scaffold.
|
||||
* @param HomeboxLensTheme Composable-функция для применения темы.
|
||||
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
*/
|
||||
// [ANCHOR:DashboardScreen:Function]
|
||||
// [RELATION:CALLS:DashboardViewModel]
|
||||
// [RELATION:CALLS:mainScaffold]
|
||||
// [CONTRACT:DashboardScreen]
|
||||
// [PURPOSE] Главная Composable-функция для экрана "Панель управления".
|
||||
// [PARAM:viewModel:DashboardViewModel] ViewModel для этого экрана, предоставляется через Hilt.
|
||||
// [PARAM:currentRoute:String] Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
// [PARAM:navigateToScan:Unit] Лямбда для навигации на экран сканирования.
|
||||
// [PARAM:navigateToSearch:Unit] Лямбда для навигации на экран поиска.
|
||||
// [PARAM:navigateToInventoryListWithLocation:Unit] Лямбда для навигации на список инвентаря с фильтром по локации.
|
||||
// [PARAM:navigateToInventoryListWithLabel:Unit] Лямбда для навигации на список инвентаря с фильтром по метке.
|
||||
// [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
|
||||
// [PARAM:navController:NavHostController] Контроллер навигации.
|
||||
// [SIDE_EFFECT] Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
// [END_CONTRACT:DashboardScreen]
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
viewModel: DashboardViewModel = hiltViewModel(),
|
||||
@@ -62,17 +77,6 @@ fun DashboardScreen(
|
||||
navigateToInventoryListWithLabel: (String) -> Unit,
|
||||
navigationActions: NavigationActions,
|
||||
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()
|
||||
|
||||
@@ -81,60 +85,60 @@ fun DashboardScreen(
|
||||
}
|
||||
|
||||
HomeboxLensTheme {
|
||||
MainScaffoldContent(
|
||||
topBarTitle = stringResource(id = R.string.dashboard_title),
|
||||
mainScaffold(
|
||||
topBarTitle = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onNavigateUp = null, // Dashboard doesn't have an "Up" button
|
||||
topBarActions = {
|
||||
IconButton(onClick = navigateToScan) {
|
||||
Icon(
|
||||
Icons.Default.QrCodeScanner,
|
||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code)
|
||||
Icons.Filled.QrCodeScanner,
|
||||
contentDescription = stringResource(id = com.homebox.lens.feature.dashboard.R.string.cd_scan_qr_code),
|
||||
)
|
||||
}
|
||||
IconButton(onClick = navigateToSearch) {
|
||||
Icon(
|
||||
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
|
||||
floatingActionButton = {} // Not used in Dashboard
|
||||
snackbarHost = { },
|
||||
floatingActionButton = { },
|
||||
) { paddingValues ->
|
||||
DashboardContent(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
uiState = uiState,
|
||||
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)
|
||||
},
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardScreen')]
|
||||
// [END_ANCHOR:DashboardScreen]
|
||||
|
||||
// [ENTITY: Function('DashboardContent')]
|
||||
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* @summary Отображает основной контент экрана в зависимости от uiState.
|
||||
* @param modifier Модификатор для стилизации.
|
||||
* @param uiState Текущее состояние UI экрана.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
// [ANCHOR:DashboardContent:Function]
|
||||
// [RELATION:CONSUMES_STATE:DashboardUiState]
|
||||
// [CONTRACT:DashboardContent]
|
||||
// [PURPOSE] Отображает основной контент экрана в зависимости от uiState.
|
||||
// [PARAM:modifier:Modifier] Модификатор для стилизации.
|
||||
// [PARAM:uiState:DashboardUiState] Текущее состояние UI экрана.
|
||||
// [PARAM:onLocationClick:Unit] Лямбда-обработчик нажатия на местоположение.
|
||||
// [PARAM:onLabelClick:Unit] Лямбда-обработчик нажатия на метку.
|
||||
// [END_CONTRACT:DashboardContent]
|
||||
@Composable
|
||||
private fun DashboardContent(
|
||||
modifier: Modifier = Modifier,
|
||||
uiState: DashboardUiState,
|
||||
onLocationClick: (LocationOutCount) -> Unit,
|
||||
onLabelClick: (LabelOut) -> Unit
|
||||
onLabelClick: (LabelOut) -> Unit,
|
||||
) {
|
||||
when (uiState) {
|
||||
is DashboardUiState.Loading -> {
|
||||
@@ -147,16 +151,17 @@ private fun DashboardContent(
|
||||
Text(
|
||||
text = uiState.message,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
is DashboardUiState.Success -> {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
item { Spacer(modifier = Modifier.height(8.dp)) }
|
||||
item { StatisticsSection(statistics = uiState.statistics) }
|
||||
@@ -168,77 +173,102 @@ private fun DashboardContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContent')]
|
||||
// [END_ANCHOR:DashboardContent]
|
||||
|
||||
// [ENTITY: Function('StatisticsSection')]
|
||||
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||
/**
|
||||
* @summary Секция для отображения общей статистики.
|
||||
* @param statistics Объект со статистическими данными.
|
||||
*/
|
||||
// [ANCHOR:StatisticsSection:Function]
|
||||
// [RELATION:DEPENDS_ON:GroupStatistics]
|
||||
// [CONTRACT:StatisticsSection]
|
||||
// [PURPOSE] Секция для отображения общей статистики.
|
||||
// [PARAM:statistics:GroupStatistics] Объект со статистическими данными.
|
||||
// [END_CONTRACT:StatisticsSection]
|
||||
@Composable
|
||||
private fun StatisticsSection(statistics: GroupStatistics) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_quick_stats),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_quick_stats),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Card {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier
|
||||
.height(120.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.height(120.dp)
|
||||
.fillMaxWidth()
|
||||
.padding(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 { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
|
||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
|
||||
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
|
||||
item {
|
||||
StatisticCard(
|
||||
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_items),
|
||||
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')]
|
||||
/**
|
||||
* @summary Карточка для отображения одного статистического показателя.
|
||||
* @param title Название показателя.
|
||||
* @param value Значение показателя.
|
||||
*/
|
||||
// [ANCHOR:StatisticCard:Function]
|
||||
// [CONTRACT:StatisticCard]
|
||||
// [PURPOSE] Карточка для отображения одного статистического показателя.
|
||||
// [PARAM:title:String] Название показателя.
|
||||
// [PARAM:value:String] Значение показателя.
|
||||
// [END_CONTRACT:StatisticCard]
|
||||
@Composable
|
||||
private fun StatisticCard(title: String, value: String) {
|
||||
private fun StatisticCard(
|
||||
title: String,
|
||||
value: String,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
|
||||
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
|
||||
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('StatisticCard')]
|
||||
// [END_ANCHOR:StatisticCard]
|
||||
|
||||
// [ENTITY: Function('RecentlyAddedSection')]
|
||||
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Секция для отображения недавно добавленных элементов.
|
||||
* @param items Список элементов для отображения.
|
||||
*/
|
||||
// [ANCHOR:RecentlyAddedSection:Function]
|
||||
// [RELATION:DEPENDS_ON:ItemSummary]
|
||||
// [CONTRACT:RecentlyAddedSection]
|
||||
// [PURPOSE] Секция для отображения недавно добавленных элементов.
|
||||
// [PARAM:items:List<ItemSummary>] Список элементов для отображения.
|
||||
// [END_CONTRACT:RecentlyAddedSection]
|
||||
@Composable
|
||||
private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_recently_added),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_recently_added),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
if (items.isEmpty()) {
|
||||
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,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
} else {
|
||||
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')]
|
||||
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Карточка для отображения краткой информации об элементе.
|
||||
* @param item Элемент для отображения.
|
||||
*/
|
||||
// [ANCHOR:ItemCard:Function]
|
||||
// [RELATION:DEPENDS_ON:ItemSummary]
|
||||
// [CONTRACT:ItemCard]
|
||||
// [PURPOSE] Карточка для отображения краткой информации об элементе.
|
||||
// [PARAM:item:ItemSummary] Элемент для отображения.
|
||||
// [END_CONTRACT:ItemCard]
|
||||
@Composable
|
||||
private fun ItemCard(item: ItemSummary) {
|
||||
Card(modifier = Modifier.width(150.dp)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
// [AI_NOTE]: Add image here from item.image
|
||||
Spacer(modifier = Modifier
|
||||
.height(80.dp)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer))
|
||||
Spacer(
|
||||
modifier =
|
||||
Modifier
|
||||
.height(80.dp)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
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')]
|
||||
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
/**
|
||||
* @summary Секция для отображения местоположений в виде чипсов.
|
||||
* @param locations Список местоположений.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
*/
|
||||
// [ANCHOR:LocationsSection:Function]
|
||||
// [RELATION:DEPENDS_ON:LocationOutCount]
|
||||
// [CONTRACT:LocationsSection]
|
||||
// [PURPOSE] Секция для отображения местоположений в виде чипсов.
|
||||
// [PARAM:locations:List<LocationOutCount>] Список местоположений.
|
||||
// [PARAM:onLocationClick:Unit] Лямбда-обработчик нажатия на местоположение.
|
||||
// [END_CONTRACT:LocationsSection]
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@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)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_locations),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_locations),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
locations.forEach { location ->
|
||||
SuggestionChip(
|
||||
onClick = { onLocationClick(location) },
|
||||
label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }
|
||||
onClick = {
|
||||
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')]
|
||||
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
* @summary Секция для отображения меток в виде чипсов.
|
||||
* @param labels Список меток.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
// [ANCHOR:LabelsSection:Function]
|
||||
// [RELATION:DEPENDS_ON:LabelOut]
|
||||
// [CONTRACT:LabelsSection]
|
||||
// [PURPOSE] Секция для отображения меток в виде чипсов.
|
||||
// [PARAM:labels:List<LabelOut>] Список меток.
|
||||
// [PARAM:onLabelClick:Unit] Лямбда-обработчик нажатия на метку.
|
||||
// [END_CONTRACT:LabelsSection]
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@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)) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.dashboard_section_labels),
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_labels),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
labels.forEach { label ->
|
||||
SuggestionChip(
|
||||
onClick = { onLabelClick(label) },
|
||||
label = { Text(label.name) }
|
||||
onClick = {
|
||||
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")
|
||||
@Composable
|
||||
fun DashboardContentSuccessPreview() {
|
||||
val previewState = DashboardUiState.Success(
|
||||
statistics = GroupStatistics(
|
||||
items = 123,
|
||||
totalValue = 9999.99,
|
||||
locations = 5,
|
||||
labels = 8
|
||||
),
|
||||
locations = listOf(
|
||||
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 = ""),
|
||||
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()
|
||||
)
|
||||
val previewState =
|
||||
DashboardUiState.Success(
|
||||
statistics =
|
||||
GroupStatistics(
|
||||
items = 123,
|
||||
totalValue = 9999.99,
|
||||
locations = 5,
|
||||
labels = 8,
|
||||
),
|
||||
locations =
|
||||
listOf(
|
||||
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 = "",
|
||||
),
|
||||
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 {
|
||||
DashboardContent(
|
||||
uiState = previewState,
|
||||
onLocationClick = {},
|
||||
onLabelClick = {}
|
||||
onLabelClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
|
||||
// [END_ANCHOR:DashboardContentSuccessPreview]
|
||||
|
||||
// [ENTITY: Function('DashboardContentLoadingPreview')]
|
||||
// [ANCHOR:DashboardContentLoadingPreview:Function]
|
||||
@Preview(showBackground = true, name = "Dashboard Loading State")
|
||||
@Composable
|
||||
fun DashboardContentLoadingPreview() {
|
||||
@@ -376,23 +469,23 @@ fun DashboardContentLoadingPreview() {
|
||||
DashboardContent(
|
||||
uiState = DashboardUiState.Loading,
|
||||
onLocationClick = {},
|
||||
onLabelClick = {}
|
||||
onLabelClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
|
||||
// [END_ANCHOR:DashboardContentLoadingPreview]
|
||||
|
||||
// [ENTITY: Function('DashboardContentErrorPreview')]
|
||||
// [ANCHOR:DashboardContentErrorPreview:Function]
|
||||
@Preview(showBackground = true, name = "Dashboard Error State")
|
||||
@Composable
|
||||
fun DashboardContentErrorPreview() {
|
||||
HomeboxLensTheme {
|
||||
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 = {},
|
||||
onLabelClick = {}
|
||||
onLabelClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('DashboardContentErrorPreview')]
|
||||
// [END_FILE_DashboardScreen.kt]
|
||||
// [END_ANCHOR:DashboardContentErrorPreview]
|
||||
// [END_FILE_DashboardScreen.kt]
|
||||
|
||||
@@ -10,46 +10,46 @@ import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* @summary Определяет все возможные состояния для экрана "Дэшборд".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
*/
|
||||
// [ANCHOR:DashboardUiState:SealedInterface]
|
||||
// [CONTRACT:DashboardUiState]
|
||||
// [PURPOSE] Определяет все возможные состояния для экрана "Дэшборд".
|
||||
// [INVARIANT] В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
// [END_CONTRACT:DashboardUiState]
|
||||
sealed interface DashboardUiState {
|
||||
// [ENTITY: DataClass('Success')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* @summary Состояние успешной загрузки данных.
|
||||
* @param statistics Статистика по инвентарю.
|
||||
* @param locations Список локаций со счетчиками.
|
||||
* @param labels Список всех меток.
|
||||
* @param recentlyAddedItems Список недавно добавленных товаров.
|
||||
*/
|
||||
// [ANCHOR:Success:DataClass]
|
||||
// [RELATION:DEPENDS_ON:GroupStatistics]
|
||||
// [RELATION:DEPENDS_ON:LocationOutCount]
|
||||
// [RELATION:DEPENDS_ON:LabelOut]
|
||||
// [RELATION:DEPENDS_ON:ItemSummary]
|
||||
// [CONTRACT:Success]
|
||||
// [PURPOSE] Состояние успешной загрузки данных.
|
||||
// [PARAM:statistics:GroupStatistics] Статистика по инвентарю.
|
||||
// [PARAM:locations:List<LocationOutCount>] Список локаций со счетчиками.
|
||||
// [PARAM:labels:List<LabelOut>] Список всех меток.
|
||||
// [PARAM:recentlyAddedItems:List<ItemSummary>] Список недавно добавленных товаров.
|
||||
// [END_CONTRACT:Success]
|
||||
data class Success(
|
||||
val statistics: GroupStatistics,
|
||||
val locations: List<LocationOutCount>,
|
||||
val labels: List<LabelOut>,
|
||||
val recentlyAddedItems: List<ItemSummary>
|
||||
val recentlyAddedItems: List<ItemSummary>,
|
||||
) : DashboardUiState
|
||||
// [END_ENTITY: DataClass('Success')]
|
||||
// [END_ANCHOR:Success]
|
||||
|
||||
// [ENTITY: DataClass('Error')]
|
||||
/**
|
||||
* @summary Состояние ошибки во время загрузки данных.
|
||||
* @param message Человекочитаемое сообщение об ошибке.
|
||||
*/
|
||||
// [ANCHOR:Error:DataClass]
|
||||
// [CONTRACT:Error]
|
||||
// [PURPOSE] Состояние ошибки во время загрузки данных.
|
||||
// [PARAM:message:String] Человекочитаемое сообщение об ошибке.
|
||||
// [END_CONTRACT:Error]
|
||||
data class Error(val message: String) : DashboardUiState
|
||||
// [END_ENTITY: DataClass('Error')]
|
||||
// [END_ANCHOR:Error]
|
||||
|
||||
// [ENTITY: Object('Loading')]
|
||||
/**
|
||||
* @summary Состояние, когда данные для экрана загружаются.
|
||||
*/
|
||||
// [ANCHOR:Loading:Object]
|
||||
// [CONTRACT:Loading]
|
||||
// [PURPOSE] Состояние, когда данные для экрана загружаются.
|
||||
// [END_CONTRACT:Loading]
|
||||
data object Loading : DashboardUiState
|
||||
// [END_ENTITY: Object('Loading')]
|
||||
// [END_ANCHOR:Loading]
|
||||
}
|
||||
// [END_ENTITY: SealedInterface('DashboardUiState')]
|
||||
// [END_FILE_DashboardUiState.kt]
|
||||
// [END_ANCHOR:DashboardUiState]
|
||||
// [END_FILE_DashboardUiState.kt]
|
||||
|
||||
@@ -17,71 +17,67 @@ import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
|
||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* @summary ViewModel для главного экрана (Dashboard).
|
||||
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
||||
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
|
||||
*/
|
||||
// [ANCHOR:DashboardViewModel:ViewModel]
|
||||
// [RELATION:DEPENDS_ON:GetStatisticsUseCase]
|
||||
// [RELATION:DEPENDS_ON:GetAllLocationsUseCase]
|
||||
// [RELATION:DEPENDS_ON:GetAllLabelsUseCase]
|
||||
// [RELATION:DEPENDS_ON:GetRecentlyAddedItemsUseCase]
|
||||
// [RELATION:EMITS_STATE:DashboardUiState]
|
||||
// [CONTRACT:DashboardViewModel]
|
||||
// [PURPOSE] ViewModel для главного экрана (Dashboard). Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||
// [INVARIANT] `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
|
||||
// [END_CONTRACT:DashboardViewModel]
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
private val getStatisticsUseCase: GetStatisticsUseCase,
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase,
|
||||
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
|
||||
) : ViewModel() {
|
||||
class DashboardViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val getStatisticsUseCase: GetStatisticsUseCase,
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase,
|
||||
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)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
// [ANCHOR:loadDashboardData:Function]
|
||||
// [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')]
|
||||
/**
|
||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
||||
*/
|
||||
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)
|
||||
|
||||
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
|
||||
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_ENTITY: ViewModel('DashboardViewModel')]
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
// [END_ANCHOR:DashboardViewModel]
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
|
||||
@@ -2,59 +2,56 @@
|
||||
// [FILE] NavGraph.kt
|
||||
// [SEMANTICS] navigation, compose, nav_host
|
||||
|
||||
package com.homebox.lens.navigation
|
||||
package com.homebox.lens.feature.dashboard.navigation
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.homebox.lens.feature.dashboard.addDashboardScreen
|
||||
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||
import com.homebox.lens.ui.screen.labeledit.LabelEditScreen
|
||||
import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
import com.homebox.lens.ui.common.NavigationActions
|
||||
import com.homebox.lens.feature.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.feature.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.feature.itemedit.ItemEditScreen
|
||||
import com.homebox.lens.feature.labeledit.LabelEditScreen
|
||||
import com.homebox.lens.feature.labelslist.LabelsListScreen
|
||||
import com.homebox.lens.feature.locationedit.LocationEditScreen
|
||||
import com.homebox.lens.feature.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.feature.scan.ScanScreen
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||
// import com.homebox.lens.ui.screen.settings.SettingsScreen
|
||||
import com.homebox.lens.feature.search.SearchScreen
|
||||
import com.homebox.lens.feature.settings.SettingsScreen
|
||||
import com.homebox.lens.feature.setup.SetupScreen
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('NavGraph')]
|
||||
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
||||
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
||||
/**
|
||||
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||
* @param navController Контроллер навигации.
|
||||
* @see Screen
|
||||
* @sideeffect Регистрирует все экраны и управляет состоянием навигации.
|
||||
* @invariant Стартовый экран - `Screen.Setup`.
|
||||
*/
|
||||
// [ANCHOR:NavGraph:Function]
|
||||
// [RELATION:DEPENDS_ON:NavHostController]
|
||||
// [RELATION:CREATES_INSTANCE_OF:NavigationActions]
|
||||
// [CONTRACT:NavGraph]
|
||||
// [PURPOSE] Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||
// [PARAM:navController:NavHostController] Контроллер навигации.
|
||||
// [SEE] Screen
|
||||
// [SIDE_EFFECT] Регистрирует все экраны и управляет состоянием навигации.
|
||||
// [INVARIANT] Стартовый экран - `Screen.Setup`.
|
||||
// [END_CONTRACT:NavGraph]
|
||||
@Composable
|
||||
fun NavGraph(
|
||||
navController: NavHostController = rememberNavController()
|
||||
) {
|
||||
fun navGraph(navController: NavHostController = rememberNavController()) {
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
val navigationActions = remember(navController) {
|
||||
NavigationActions(navController)
|
||||
}
|
||||
val navigationActions =
|
||||
remember(navController) {
|
||||
NavigationActions(navController)
|
||||
}
|
||||
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Setup.route
|
||||
startDestination = Screen.Setup.route,
|
||||
) {
|
||||
composable(route = Screen.Setup.route) {
|
||||
SetupScreen(onSetupComplete = {
|
||||
@@ -70,98 +67,89 @@ fun NavGraph(
|
||||
navigateToSearch = navigationActions::navigateToSearch,
|
||||
navigateToInventoryListWithLocation = navigationActions::navigateToInventoryListWithLocation,
|
||||
navigateToInventoryListWithLabel = navigationActions::navigateToInventoryListWithLabel,
|
||||
MainScaffoldContent = { topBarTitle, currentRoute, topBarActions, content ->
|
||||
MainScaffold(
|
||||
topBarTitle = topBarTitle,
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
topBarActions = topBarActions,
|
||||
content = content
|
||||
)
|
||||
},
|
||||
HomeboxLensTheme = { content -> HomeboxLensTheme(content = content) }
|
||||
navigationActions = navigationActions,
|
||||
navController = navController,
|
||||
)
|
||||
composable(route = Screen.InventoryList.route) {
|
||||
InventoryListScreen(
|
||||
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(
|
||||
itemId = itemId,
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
navigationActions = navigationActions,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.ItemEdit.route,
|
||||
arguments = listOf(navArgument("itemId") { nullable = true })
|
||||
arguments = listOf(navArgument("itemId") { nullable = true }),
|
||||
) { backStackEntry ->
|
||||
val itemId = backStackEntry.arguments?.getString("itemId")
|
||||
ItemEditScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
itemId = itemId,
|
||||
onSaveSuccess = { navController.popBackStack() }
|
||||
onSaveSuccess = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable(Screen.LabelsList.route) {
|
||||
LabelsListScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
navigationActions = navigationActions,
|
||||
)
|
||||
}
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
LocationsListScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onLocationClick = { locationId ->
|
||||
onLocationClick = { locationId: String ->
|
||||
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen
|
||||
navController.navigate(Screen.InventoryList.route)
|
||||
navigationActions.navigateToInventoryListWithLocation(locationId)
|
||||
},
|
||||
onAddNewLocationClick = {
|
||||
navController.navigate(Screen.LocationEdit.createRoute("new"))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
||||
LocationEditScreen(
|
||||
locationId = locationId
|
||||
)
|
||||
}
|
||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
||||
LocationEditScreen(
|
||||
locationId = locationId
|
||||
locationId = locationId,
|
||||
)
|
||||
}
|
||||
composable(
|
||||
route = Screen.LabelEdit.route,
|
||||
arguments = listOf(navArgument("labelId") { nullable = true })
|
||||
arguments = listOf(navArgument("labelId") { nullable = true }),
|
||||
) { backStackEntry ->
|
||||
val labelId = backStackEntry.arguments?.getString("labelId")
|
||||
LabelEditScreen(
|
||||
labelId = labelId,
|
||||
onBack = { navController.popBackStack() },
|
||||
onLabelSaved = { navController.popBackStack() }
|
||||
onLabelSaved = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
navigationActions = navigationActions,
|
||||
)
|
||||
}
|
||||
composable(Screen.Settings.route) {
|
||||
com.homebox.lens.ui.screen.settings.SettingsScreen(
|
||||
SettingsScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onNavigateUp = { navController.navigateUp() }
|
||||
onNavigateUp = { navController.navigateUp() },
|
||||
)
|
||||
}
|
||||
composable(Screen.Scan.route) { backStackEntry ->
|
||||
ScanScreen(onBarcodeResult = { barcode ->
|
||||
ScanScreen(onBarcodeResult = { barcode: String ->
|
||||
val previousBackStackEntry = navController.previousBackStackEntry
|
||||
previousBackStackEntry?.savedStateHandle?.set("barcodeResult", barcode)
|
||||
navController.popBackStack()
|
||||
@@ -169,5 +157,5 @@ fun NavGraph(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('NavGraph')]
|
||||
// [END_FILE_NavGraph.kt]
|
||||
// [END_ANCHOR:NavGraph]
|
||||
// [END_FILE_NavGraph.kt]
|
||||
@@ -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")
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.theme
|
||||
// [FILE] Color.kt
|
||||
// [SEMANTICS] ui, theme, color
|
||||
package com.homebox.lens.ui.theme
|
||||
|
||||
package com.homebox.lens.feature.dashboard.ui.theme
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -15,4 +15,4 @@ val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
|
||||
// [END_FILE_Color.kt]
|
||||
// [END_FILE_Color.kt]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -1,4 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Dashboard Screen -->
|
||||
<string name="dashboard_title">Главная</string>
|
||||
@@ -16,10 +15,7 @@
|
||||
|
||||
<!-- Common -->
|
||||
<string name="items_not_found">Элементы не найдены</string>
|
||||
<string name="no_location">Нет локации</string>
|
||||
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
|
||||
|
||||
<!-- Content Descriptions -->
|
||||
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
|
||||
<string name="cd_search">Поиск</string>
|
||||
</resources>
|
||||
50
feature/inventorylist/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
54
feature/itemdetails/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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]
|
||||
55
feature/itemedit/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
54
feature/labeledit/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
54
feature/labelslist/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
53
feature/locationedit/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
54
feature/locationslist/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
54
feature/search/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
55
feature/settings/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
54
feature/setup/build.gradle.kts
Normal 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)
|
||||
}
|
||||