fix: Resolve build errors

- Add missing quantity field to Item model
- Add missing string resources and translations
- Fix unresolved references in UI screens
This commit is contained in:
2025-08-18 16:15:01 +03:00
parent 7e2e6009f7
commit cf4fc7a535
39 changed files with 2859 additions and 623 deletions

View File

@@ -1,8 +1,10 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainActivity.kt
// [SEMANTICS] android, activity, compose, hilt
package com.homebox.lens
// [IMPORTS]
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -16,15 +18,24 @@ import androidx.compose.ui.tooling.preview.Preview
import com.homebox.lens.navigation.NavGraph
import com.homebox.lens.ui.theme.HomeboxLensTheme
import dagger.hilt.android.AndroidEntryPoint
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Activity('MainActivity')]
// [RELATION: Activity('MainActivity') -> [INHERITS_FROM] -> Class('ComponentActivity')]
// [RELATION: Activity('MainActivity') -> [DEPENDS_ON] -> Annotation('AndroidEntryPoint')]
/**
* [ENTITY: Activity('MainActivity')]
* [PURPOSE] Главная и единственная Activity в приложении.
*/
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// [ENTITY: Function('onCreate')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('setContent')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('Surface')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('NavGraph')]
// [LIFECYCLE]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -40,9 +51,12 @@ class MainActivity : ComponentActivity() {
}
}
}
// [END_ENTITY: Function('onCreate')]
}
// [END_ENTITY: Activity('MainActivity')]
// [HELPER]
// [ENTITY: Function('Greeting')]
// [RELATION: Function('Greeting') -> [CALLS] -> Function('Text')]
@Composable
fun Greeting(
name: String,
@@ -53,7 +67,11 @@ fun Greeting(
modifier = modifier,
)
}
// [END_ENTITY: Function('Greeting')]
// [ENTITY: Function('GreetingPreview')]
// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('GreetingPreview') -> [CALLS] -> Function('Greeting')]
// [PREVIEW]
@Preview(showBackground = true)
@Composable
@@ -62,5 +80,7 @@ fun GreetingPreview() {
Greeting("Android")
}
}
// [END_ENTITY: Function('GreetingPreview')]
// [END_FILE_MainActivity.kt]
// [END_CONTRACT]
// [END_FILE_MainActivity.kt]

View File

@@ -1,20 +1,28 @@
// [PACKAGE] com.homebox.lens
// [FILE] MainApplication.kt
// [SEMANTICS] android, application, hilt, timber
package com.homebox.lens
// [IMPORTS]
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Application('MainApplication')]
// [RELATION: Application('MainApplication') -> [INHERITS_FROM] -> Class('Application')]
// [RELATION: Application('MainApplication') -> [DEPENDS_ON] -> Annotation('HiltAndroidApp')]
/**
* [ENTITY: Application('MainApplication')]
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
*/
@HiltAndroidApp
class MainApplication : Application() {
// [ENTITY: Function('onCreate')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('super.onCreate')]
// [RELATION: Function('onCreate') -> [CALLS] -> Function('Timber.plant')]
// [LIFECYCLE]
override fun onCreate() {
super.onCreate()
@@ -23,6 +31,9 @@ class MainApplication : Application() {
Timber.plant(Timber.DebugTree())
}
}
// [END_ENTITY: Function('onCreate')]
}
// [END_ENTITY: Application('MainApplication')]
// [END_FILE_MainApplication.kt]
// [END_CONTRACT]
// [END_FILE_MainApplication.kt]

View File

@@ -13,18 +13,43 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.compose.runtime.collectAsState
import com.homebox.lens.domain.model.Item
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
import com.homebox.lens.ui.screen.inventorylist.InventoryListViewModel
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsViewModel
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
import com.homebox.lens.ui.screen.itemedit.ItemEditViewModel
import com.homebox.lens.ui.screen.labelslist.labelsListScreen
import com.homebox.lens.ui.screen.labelslist.LabelsListViewModel
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.search.SearchViewModel
import com.homebox.lens.ui.screen.setup.SetupScreen
import timber.log.Timber
// [END_IMPORTS]
// [CORE-LOGIC]
// [CONTRACT]
// [ENTITY: Function('NavGraph')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('rememberNavController')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('currentBackStackEntryAsState')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('remember')]
// [RELATION: Function('NavGraph') -> [CREATES_INSTANCE_OF] -> Class('NavigationActions')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('NavHost')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('composable')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SetupScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('DashboardScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('InventoryListScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemDetailsScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('ItemEditScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LabelsListScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationsListScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('LocationEditScreen')]
// [RELATION: Function('NavGraph') -> [CALLS] -> Function('SearchScreen')]
/**
* [CONTRACT]
* Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
@@ -66,29 +91,59 @@ fun NavGraph(navController: NavHostController = rememberNavController()) {
)
}
// [COMPOSABLE_INVENTORY_LIST]
composable(route = Screen.InventoryList.route) {
composable(route = Screen.InventoryList.route) { backStackEntry ->
val viewModel: InventoryListViewModel = hiltViewModel(backStackEntry)
InventoryListScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
onItemClick = { item ->
// TODO: Navigate to item details
Timber.d("Item clicked: ${item.name}")
},
onNavigateBack = {
navController.popBackStack()
}
)
}
// [COMPOSABLE_ITEM_DETAILS]
composable(route = Screen.ItemDetails.route) {
composable(route = Screen.ItemDetails.route) { backStackEntry ->
val viewModel: ItemDetailsViewModel = hiltViewModel(backStackEntry)
ItemDetailsScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateBack = {
navController.popBackStack()
},
onEditClick = { itemId ->
// TODO: Navigate to item edit screen
Timber.d("Edit item clicked: $itemId")
}
)
}
// [COMPOSABLE_ITEM_EDIT]
composable(route = Screen.ItemEdit.route) {
composable(route = Screen.ItemEdit.route) { backStackEntry ->
val viewModel: ItemEditViewModel = hiltViewModel(backStackEntry)
ItemEditScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateBack = {
navController.popBackStack()
}
)
}
// [COMPOSABLE_LABELS_LIST]
composable(Screen.LabelsList.route) {
LabelsListScreen(navController = navController)
composable(Screen.LabelsList.route) { backStackEntry ->
val viewModel: LabelsListViewModel = hiltViewModel(backStackEntry)
val uiState by viewModel.uiState.collectAsState()
labelsListScreen(
uiState = uiState,
onLabelClick = { label ->
// TODO: Implement navigation to label details screen
Timber.d("Label clicked: ${label.name}")
},
onAddClick = {
// TODO: Implement navigation to add new label screen
Timber.d("Add new label clicked")
},
onNavigateBack = {
navController.popBackStack()
}
)
}
// [COMPOSABLE_LOCATIONS_LIST]
composable(route = Screen.LocationsList.route) {
@@ -112,13 +167,20 @@ fun NavGraph(navController: NavHostController = rememberNavController()) {
)
}
// [COMPOSABLE_SEARCH]
composable(route = Screen.Search.route) {
composable(route = Screen.Search.route) { backStackEntry ->
val viewModel: SearchViewModel = hiltViewModel(backStackEntry)
SearchScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateBack = {
navController.popBackStack()
},
onItemClick = { item ->
// TODO: Navigate to item details
Timber.d("Search result item clicked: ${item.name}")
}
)
}
}
// [END_FUNCTION_NavGraph]
}
// [END_FILE_NavGraph.kt]
// [END_ENTITY: Function('NavGraph')]
// [END_CONTRACT]
// [END_FILE_NavGraph.kt]

View File

@@ -2,79 +2,122 @@
// [FILE] NavigationActions.kt
// [SEMANTICS] navigation, controller, actions
package com.homebox.lens.navigation
import androidx.navigation.NavHostController
// [CORE-LOGIC]
// [IMPORTS]
import androidx.navigation.NavHostController
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Class('NavigationActions')]
// [RELATION: Class('NavigationActions') -> [DEPENDS_ON] -> Class('NavHostController')]
/**
[CONTRACT]
@summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
@param navController Контроллер Jetpack Navigation.
@invariant Все навигационные действия должны использовать предоставленный navController.
* [CONTRACT]
* @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
* @param navController Контроллер Jetpack Navigation.
* @invariant Все навигационные действия должны использовать предоставленный navController.
*/
class NavigationActions(private val navController: NavHostController) {
// [ACTION]
// [ENTITY: Function('navigateToDashboard')]
// [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('navController.navigate')]
// [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('Screen.Dashboard.route')]
// [RELATION: Function('navigateToDashboard') -> [CALLS] -> Function('popUpTo')]
// [ACTION]
/**
[CONTRACT]
@summary Навигация на главный экран.
@sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
* [CONTRACT]
* @summary Навигация на главный экран.
* @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
*/
fun navigateToDashboard() {
navController.navigate(Screen.Dashboard.route) {
// Используем popUpTo для удаления всех экранов до dashboard из back stack
// Это предотвращает создание большой стопки экранов при навигации через drawer
// Используем popUpTo для удаления всех экранов до dashboard из back stack
// Это предотвращает создание большой стопки экранов при навигации через drawer
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToDashboard')]
// [ENTITY: Function('navigateToLocations')]
// [RELATION: Function('navigateToLocations') -> [CALLS] -> Function('navController.navigate')]
// [RELATION: Function('navigateToLocations') -> [CALLS] -> Function('Screen.LocationsList.route')]
// [ACTION]
fun navigateToLocations() {
navController.navigate(Screen.LocationsList.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToLocations')]
// [ENTITY: Function('navigateToLabels')]
// [RELATION: Function('navigateToLabels') -> [CALLS] -> Function('navController.navigate')]
// [RELATION: Function('navigateToLabels') -> [CALLS] -> Function('Screen.LabelsList.route')]
// [ACTION]
fun navigateToLabels() {
navController.navigate(Screen.LabelsList.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToLabels')]
// [ENTITY: Function('navigateToSearch')]
// [RELATION: Function('navigateToSearch') -> [CALLS] -> Function('navController.navigate')]
// [RELATION: Function('navigateToSearch') -> [CALLS] -> Function('Screen.Search.route')]
// [ACTION]
fun navigateToSearch() {
navController.navigate(Screen.Search.route) {
launchSingleTop = true
}
}
// [END_ENTITY: Function('navigateToSearch')]
// [ENTITY: Function('navigateToInventoryListWithLabel')]
// [RELATION: Function('navigateToInventoryListWithLabel') -> [CALLS] -> Function('Screen.InventoryList.withFilter')]
// [RELATION: Function('navigateToInventoryListWithLabel') -> [CALLS] -> Function('navController.navigate')]
// [ACTION]
fun navigateToInventoryListWithLabel(labelId: String) {
val route = Screen.InventoryList.withFilter("label", labelId)
navController.navigate(route)
}
// [END_ENTITY: Function('navigateToInventoryListWithLabel')]
// [ENTITY: Function('navigateToInventoryListWithLocation')]
// [RELATION: Function('navigateToInventoryListWithLocation') -> [CALLS] -> Function('Screen.InventoryList.withFilter')]
// [RELATION: Function('navigateToInventoryListWithLocation') -> [CALLS] -> Function('navController.navigate')]
// [ACTION]
fun navigateToInventoryListWithLocation(locationId: String) {
val route = Screen.InventoryList.withFilter("location", locationId)
navController.navigate(route)
}
// [END_ENTITY: Function('navigateToInventoryListWithLocation')]
// [ENTITY: Function('navigateToCreateItem')]
// [RELATION: Function('navigateToCreateItem') -> [CALLS] -> Function('Screen.ItemEdit.createRoute')]
// [RELATION: Function('navigateToCreateItem') -> [CALLS] -> Function('navController.navigate')]
// [ACTION]
fun navigateToCreateItem() {
navController.navigate(Screen.ItemEdit.createRoute("new"))
}
// [END_ENTITY: Function('navigateToCreateItem')]
// [ENTITY: Function('navigateToLogout')]
// [RELATION: Function('navigateToLogout') -> [CALLS] -> Function('navController.navigate')]
// [RELATION: Function('navigateToLogout') -> [CALLS] -> Function('popUpTo')]
// [ACTION]
fun navigateToLogout() {
navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
}
}
// [END_ENTITY: Function('navigateToLogout')]
// [ENTITY: Function('navigateBack')]
// [RELATION: Function('navigateBack') -> [CALLS] -> Function('navController.popBackStack')]
// [ACTION]
fun navigateBack() {
navController.popBackStack()
}
// [END_ENTITY: Function('navigateBack')]
}
// [END_FILE_NavigationActions.kt]
// [END_ENTITY: Class('NavigationActions')]
// [END_CONTRACT]
// [END_FILE_NavigationActions.kt]

View File

@@ -3,8 +3,11 @@
// [SEMANTICS] navigation, routes, sealed_class
package com.homebox.lens.navigation
// [CORE-LOGIC]
// [IMPORTS]
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: SealedClass('Screen')]
/**
* [CONTRACT]
* Запечатанный класс для определения маршрутов навигации в приложении.
@@ -12,12 +15,17 @@ package com.homebox.lens.navigation
* @property route Строковый идентификатор маршрута.
*/
sealed class Screen(val route: String) {
// [STATE]
// [ENTITY: DataObject('Setup')]
data object Setup : Screen("setup_screen")
// [END_ENTITY: DataObject('Setup')]
// [ENTITY: DataObject('Dashboard')]
data object Dashboard : Screen("dashboard_screen")
// [END_ENTITY: DataObject('Dashboard')]
// [ENTITY: DataObject('InventoryList')]
data object InventoryList : Screen("inventory_list_screen") {
// [ENTITY: Function('withFilter')]
/**
* [CONTRACT]
* Создает маршрут для экрана списка инвентаря с параметром фильтра.
@@ -27,7 +35,6 @@ sealed class Screen(val route: String) {
* @throws IllegalArgumentException если ключ или значение пустые.
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
*/
// [HELPER]
fun withFilter(
key: String,
value: String,
@@ -41,9 +48,13 @@ sealed class Screen(val route: String) {
check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." }
return constructedRoute
}
// [END_ENTITY: Function('withFilter')]
}
// [END_ENTITY: DataObject('InventoryList')]
// [ENTITY: DataObject('ItemDetails')]
data object ItemDetails : Screen("item_details_screen/{itemId}") {
// [ENTITY: Function('createRoute')]
/**
* [CONTRACT]
* Создает маршрут для экрана деталей элемента с указанным ID.
@@ -51,7 +62,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой.
*/
// [HELPER]
fun createRoute(itemId: String): String {
// [PRECONDITION]
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
@@ -61,9 +71,13 @@ sealed class Screen(val route: String) {
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
return route
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: DataObject('ItemDetails')]
// [ENTITY: DataObject('ItemEdit')]
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
// [ENTITY: Function('createRoute')]
/**
* [CONTRACT]
* Создает маршрут для экрана редактирования элемента с указанным ID.
@@ -71,7 +85,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута.
* @throws IllegalArgumentException если itemId пустой.
*/
// [HELPER]
fun createRoute(itemId: String): String {
// [PRECONDITION]
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
@@ -81,13 +94,21 @@ sealed class Screen(val route: String) {
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
return route
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: DataObject('ItemEdit')]
// [ENTITY: DataObject('LabelsList')]
data object LabelsList : Screen("labels_list_screen")
// [END_ENTITY: DataObject('LabelsList')]
// [ENTITY: DataObject('LocationsList')]
data object LocationsList : Screen("locations_list_screen")
// [END_ENTITY: DataObject('LocationsList')]
// [ENTITY: DataObject('LocationEdit')]
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
// [ENTITY: Function('createRoute')]
/**
* [CONTRACT]
* Создает маршрут для экрана редактирования местоположения с указанным ID.
@@ -95,7 +116,6 @@ sealed class Screen(val route: String) {
* @return Строку полного маршрута.
* @throws IllegalArgumentException если locationId пустой.
*/
// [HELPER]
fun createRoute(locationId: String): String {
// [PRECONDITION]
require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." }
@@ -105,8 +125,14 @@ sealed class Screen(val route: String) {
check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." }
return route
}
// [END_ENTITY: Function('createRoute')]
}
// [END_ENTITY: DataObject('LocationEdit')]
// [ENTITY: DataObject('Search')]
data object Search : Screen("search_screen")
// [END_ENTITY: DataObject('Search')]
}
// [END_FILE_Screen.kt]
// [END_ENTITY: SealedClass('Screen')]
// [END_CONTRACT]
// [END_FILE_Screen.kt]

View File

@@ -1,6 +1,9 @@
// [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
@@ -22,13 +25,31 @@ 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]
// [CONTRACT]
// [ENTITY: Function('AppDrawerContent')]
// [RELATION: Function('AppDrawerContent') -> [DEPENDS_ON] -> Class('NavigationActions')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('ModalDrawerSheet')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Spacer')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Button')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Divider')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('NavigationDrawerItem')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Dashboard.route')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.LocationsList.route')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.LabelsList.route')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Search.route')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.ItemEdit.createRoute')]
// [RELATION: Function('AppDrawerContent') -> [CALLS] -> Function('Screen.Setup.route')]
/**
[CONTRACT]
@summary Контент для бокового навигационного меню (Drawer).
@param currentRoute Текущий маршрут для подсветки активного элемента.
@param navigationActions Объект с навигационными действиями.
@param onCloseDrawer Лямбда для закрытия бокового меню.
* [CONTRACT]
* @summary Контент для бокового навигационного меню (Drawer).
* @param currentRoute Текущий маршрут для подсветки активного элемента.
* @param navigationActions Объект с навигационными действиями.
* @param onCloseDrawer Лямбда для закрытия бокового меню.
*/
@Composable
internal fun AppDrawerContent(
@@ -98,3 +119,6 @@ internal fun AppDrawerContent(
)
}
}
// [END_ENTITY: Function('AppDrawerContent')]
// [END_CONTRACT]
// [END_FILE_AppDrawer.kt]

View File

@@ -15,9 +15,21 @@ import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
import com.homebox.lens.navigation.NavigationActions
import kotlinx.coroutines.launch
// [END_IMPORTS]
// [UI_COMPONENT]
// [CONTRACT]
// [ENTITY: Function('MainScaffold')]
// [RELATION: Function('MainScaffold') -> [DEPENDS_ON] -> Class('NavigationActions')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('rememberDrawerState')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('rememberCoroutineScope')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('ModalNavigationDrawer')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('AppDrawerContent')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Text')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('MainScaffold') -> [CALLS] -> Function('Icon')]
/**
* [CONTRACT]
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
@@ -73,6 +85,7 @@ fun MainScaffold(
content(paddingValues)
}
}
// [END_FUNCTION_MainScaffold]
}
// [END_FILE_MainScaffold.kt]
// [END_ENTITY: Function('MainScaffold')]
// [END_CONTRACT]
// [END_FILE_MainScaffold.kt]

View File

@@ -2,6 +2,7 @@
// [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose, navigation
package com.homebox.lens.ui.screen.dashboard
// [IMPORTS]
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -29,15 +30,26 @@ import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
import com.homebox.lens.ui.theme.HomeboxLensTheme
import timber.log.Timber
// [ENTRYPOINT]
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Function('DashboardScreen')]
// [RELATION: Function('DashboardScreen') -> [DEPENDS_ON] -> Class('DashboardViewModel')]
// [RELATION: Function('DashboardScreen') -> [DEPENDS_ON] -> Class('NavigationActions')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('MainScaffold')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('DashboardScreen') -> [CALLS] -> Function('DashboardContent')]
/**
[CONTRACT]
@summary Главная Composable-функция для экрана "Панель управления".
@param viewModel ViewModel для этого экрана, предоставляется через Hilt.
@param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
@param navigationActions Объект с навигационными действиями.
@sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
* [CONTRACT]
* @summary Главная Composable-функция для экрана "Панель управления".
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
*/
@Composable
fun DashboardScreen(
@@ -45,9 +57,9 @@ fun DashboardScreen(
currentRoute: String?,
navigationActions: NavigationActions,
) {
// [STATE]
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [UI_COMPONENT]
// [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.dashboard_title),
currentRoute = currentRoute,
@@ -74,17 +86,28 @@ fun DashboardScreen(
},
)
}
// [END_FUNCTION_DashboardScreen]
}
// [HELPER]
// [END_ENTITY: Function('DashboardScreen')]
// [ENTITY: Function('DashboardContent')]
// [RELATION: Function('DashboardContent') -> [DEPENDS_ON] -> SealedInterface('DashboardUiState')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Box')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('Spacer')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('StatisticsSection')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('RecentlyAddedSection')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LocationsSection')]
// [RELATION: Function('DashboardContent') -> [CALLS] -> Function('LabelsSection')]
/**
[CONTRACT]
@summary Отображает основной контент экрана в зависимости от uiState.
@param modifier Модификатор для стилизации.
@param uiState Текущее состояние UI экрана.
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
@param onLabelClick Лямбда-обработчик нажатия на метку.
* [CONTRACT]
* @summary Отображает основной контент экрана в зависимости от uiState.
* @param modifier Модификатор для стилизации.
* @param uiState Текущее состояние UI экрана.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onLabelClick Лямбда-обработчик нажатия на метку.
*/
@Composable
private fun DashboardContent(
@@ -93,7 +116,7 @@ private fun DashboardContent(
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit,
) {
// [CORE-LOGIC]
// [CORE-LOGIC]
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
@@ -126,14 +149,23 @@ private fun DashboardContent(
}
}
}
// [END_FUNCTION_DashboardContent]
}
// [UI_COMPONENT]
// [END_ENTITY: Function('DashboardContent')]
// [ENTITY: Function('StatisticsSection')]
// [RELATION: Function('StatisticsSection') -> [DEPENDS_ON] -> Class('GroupStatistics')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('Card')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('LazyVerticalGrid')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('GridCells.Fixed')]
// [RELATION: Function('StatisticsSection') -> [CALLS] -> Function('StatisticCard')]
/**
[CONTRACT]
@summary Секция для отображения общей статистики.
@param statistics Объект со статистическими данными.
* [CONTRACT]
* @summary Секция для отображения общей статистики.
* @param statistics Объект со статистическими данными.
*/
@Composable
private fun StatisticsSection(statistics: GroupStatistics) {
@@ -181,13 +213,18 @@ private fun StatisticsSection(statistics: GroupStatistics) {
}
}
}
// [UI_COMPONENT]
// [END_ENTITY: Function('StatisticsSection')]
// [ENTITY: Function('StatisticCard')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Column')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('Text')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.labelMedium')]
// [RELATION: Function('StatisticCard') -> [CALLS] -> Function('MaterialTheme.typography.headlineSmall')]
/**
[CONTRACT]
@summary Карточка для отображения одного статистического показателя.
@param title Название показателя.
@param value Значение показателя.
* [CONTRACT]
* @summary Карточка для отображения одного статистического показателя.
* @param title Название показателя.
* @param value Значение показателя.
*/
@Composable
private fun StatisticCard(
@@ -199,12 +236,20 @@ private fun StatisticCard(
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
}
}
// [UI_COMPONENT]
// [END_ENTITY: Function('StatisticCard')]
// [ENTITY: Function('RecentlyAddedSection')]
// [RELATION: Function('RecentlyAddedSection') -> [DEPENDS_ON] -> Class('ItemSummary')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('LazyRow')]
// [RELATION: Function('RecentlyAddedSection') -> [CALLS] -> Function('ItemCard')]
/**
[CONTRACT]
@summary Секция для отображения недавно добавленных элементов.
@param items Список элементов для отображения.
* [CONTRACT]
* @summary Секция для отображения недавно добавленных элементов.
* @param items Список элементов для отображения.
*/
@Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) {
@@ -232,12 +277,21 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
}
}
}
// [UI_COMPONENT]
// [END_ENTITY: Function('RecentlyAddedSection')]
// [ENTITY: Function('ItemCard')]
// [RELATION: Function('ItemCard') -> [DEPENDS_ON] -> Class('ItemSummary')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Card')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Column')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Spacer')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('MaterialTheme.typography.titleSmall')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('MaterialTheme.typography.bodySmall')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('stringResource')]
/**
[CONTRACT]
@summary Карточка для отображения краткой информации об элементе.
@param item Элемент для отображения.
* [CONTRACT]
* @summary Карточка для отображения краткой информации об элементе.
* @param item Элемент для отображения.
*/
@Composable
private fun ItemCard(item: ItemSummary) {
@@ -261,13 +315,21 @@ private fun ItemCard(item: ItemSummary) {
}
}
}
// [UI_COMPONENT]
// [END_ENTITY: Function('ItemCard')]
// [ENTITY: Function('LocationsSection')]
// [RELATION: Function('LocationsSection') -> [DEPENDS_ON] -> Class('LocationOutCount')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('FlowRow')]
// [RELATION: Function('LocationsSection') -> [CALLS] -> Function('SuggestionChip')]
/**
[CONTRACT]
@summary Секция для отображения местоположений в виде чипсов.
@param locations Список местоположений.
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
* [CONTRACT]
* @summary Секция для отображения местоположений в виде чипсов.
* @param locations Список местоположений.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -292,13 +354,21 @@ private fun LocationsSection(
}
}
}
// [UI_COMPONENT]
// [END_ENTITY: Function('LocationsSection')]
// [ENTITY: Function('LabelsSection')]
// [RELATION: Function('LabelsSection') -> [DEPENDS_ON] -> Class('LabelOut')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('FlowRow')]
// [RELATION: Function('LabelsSection') -> [CALLS] -> Function('SuggestionChip')]
/**
[CONTRACT]
@summary Секция для отображения меток в виде чипсов.
@param labels Список меток.
@param onLabelClick Лямбда-обработчик нажатия на метку.
* [CONTRACT]
* @summary Секция для отображения меток в виде чипсов.
* @param labels Список меток.
* @param onLabelClick Лямбда-обработчик нажатия на метку.
*/
@OptIn(ExperimentalLayoutApi::class)
@Composable
@@ -323,7 +393,15 @@ private fun LabelsSection(
}
}
}
// [END_ENTITY: Function('LabelsSection')]
// [ENTITY: Function('DashboardContentSuccessPreview')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardUiState.Success')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('GroupStatistics')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LocationOutCount')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('LabelOut')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('DashboardContentSuccessPreview') -> [CALLS] -> Function('DashboardContent')]
// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Success State")
@Composable
@@ -402,7 +480,12 @@ fun DashboardContentSuccessPreview() {
)
}
}
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
// [ENTITY: Function('DashboardContentLoadingPreview')]
// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardContent')]
// [RELATION: Function('DashboardContentLoadingPreview') -> [CALLS] -> Function('DashboardUiState.Loading')]
// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
@@ -415,7 +498,13 @@ fun DashboardContentLoadingPreview() {
)
}
}
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
// [ENTITY: Function('DashboardContentErrorPreview')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardContent')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('DashboardUiState.Error')]
// [RELATION: Function('DashboardContentErrorPreview') -> [CALLS] -> Function('stringResource')]
// [PREVIEW]
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
@@ -428,4 +517,6 @@ fun DashboardContentErrorPreview() {
)
}
}
// [END_FILE_DashboardScreen.kt]
// [END_ENTITY: Function('DashboardContentErrorPreview')]
// [END_CONTRACT]
// [END_FILE_DashboardScreen.kt]

View File

@@ -1,23 +1,29 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardUiState.kt
// [FILE] DashboardUiState.kt
// [SEMANTICS] ui, state, dashboard
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard
// [IMPORTS]
import com.homebox.lens.domain.model.GroupStatistics
import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount
import com.homebox.lens.domain.model.ItemSummary
// [END_IMPORTS]
// [CORE-LOGIC]
// [CONTRACT]
// [ENTITY: SealedInterface('DashboardUiState')]
/**
* [CONTRACT]
* Определяет все возможные состояния для экрана "Дэшборд".
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/
sealed interface DashboardUiState {
// [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('GroupStatistics')]
// [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LocationOutCount')]
// [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('LabelOut')]
// [RELATION: DataClass('Success') -> [DEPENDS_ON] -> Class('ItemSummary')]
/**
* [CONTRACT]
* Состояние успешной загрузки данных.
@@ -30,20 +36,27 @@ sealed interface DashboardUiState {
val statistics: GroupStatistics,
val locations: List<LocationOutCount>,
val labels: List<LabelOut>,
val recentlyAddedItems: List<com.homebox.lens.domain.model.ItemSummary>,
val recentlyAddedItems: List<ItemSummary>,
) : DashboardUiState
// [END_ENTITY: DataClass('Success')]
// [ENTITY: DataClass('Error')]
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : DashboardUiState
// [END_ENTITY: DataClass('Error')]
// [ENTITY: DataObject('Loading')]
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
data object Loading : DashboardUiState
object Loading : DashboardUiState
// [END_ENTITY: DataObject('Loading')]
}
// [END_FILE_DashboardUiState.kt]
// [END_ENTITY: SealedInterface('DashboardUiState')]
// [END_CONTRACT]
// [END_FILE_DashboardUiState.kt]

View File

@@ -2,6 +2,7 @@
// [FILE] DashboardViewModel.kt
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
package com.homebox.lens.ui.screen.dashboard
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -14,10 +15,16 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('DashboardViewModel')]
// [RELATION: ViewModel('DashboardViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetStatisticsUseCase')]
// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetAllLocationsUseCase')]
// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetAllLabelsUseCase')]
// [RELATION: ViewModel('DashboardViewModel') -> [DEPENDS_ON] -> Class('GetRecentlyAddedItemsUseCase')]
/**
* [CONTRACT]
* @summary ViewModel для главного экрана (Dashboard).
@@ -47,6 +54,19 @@ class DashboardViewModel
loadDashboardData()
}
// [ENTITY: Function('loadDashboardData')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadDashboardData') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('flow')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getStatisticsUseCase')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLocationsUseCase')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getAllLabelsUseCase')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('getRecentlyAddedItemsUseCase')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('combine')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('catch')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('Timber.e')]
// [RELATION: Function('loadDashboardData') -> [CALLS] -> Function('collect')]
/**
* [CONTRACT]
* @summary Загружает все необходимые данные для экрана Dashboard.
@@ -55,7 +75,6 @@ class DashboardViewModel
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/
fun loadDashboardData() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
Timber.i("[ACTION] Starting dashboard data collection.")
@@ -84,6 +103,8 @@ class DashboardViewModel
}
}
}
// [END_CLASS_DashboardViewModel]
// [END_ENTITY: Function('loadDashboardData')]
}
// [END_FILE_DashboardViewModel.kt]
// [END_ENTITY: ViewModel('DashboardViewModel')]
// [END_CONTRACT]
// [END_FILE_DashboardViewModel.kt]

View File

@@ -1,38 +1,219 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListScreen.kt
// [SEMANTICS] ui, screen, inventory, list
// [SEMANTICS] ui, screen, inventory, list, compose
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
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.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Card
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.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
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.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
import com.homebox.lens.domain.model.Item
import timber.log.Timber
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Function('InventoryListScreen')]
// [RELATION: Function('InventoryListScreen') -> [DEPENDS_ON] -> Class('InventoryListViewModel')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('FloatingActionButton')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('SearchBar')]
// [RELATION: Function('InventoryListScreen') -> [CALLS] -> Function('InventoryListContent')]
/**
* [CONTRACT]
* @summary Composable-функция для экрана "Список инвентаря".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* [MAIN-CONTRACT]
* Экран для отображения списка инвентарных позиций.
*
* Реализует спецификацию `screen_inventory_list`. Позволяет просматривать,
* искать и синхронизировать инвентарь.
*
* @param onItemClick Обработчик нажатия на элемент инвентаря.
* @param onNavigateBack Обработчик для возврата на предыдущий экран.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InventoryListScreen(
currentRoute: String?,
navigationActions: NavigationActions,
viewModel: InventoryListViewModel = hiltViewModel(),
onItemClick: (Item) -> Unit,
onNavigateBack: () -> Unit
) {
// [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.inventory_list_title),
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
// [CORE-LOGIC]
Text(text = "TODO: Inventory List Screen")
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [ACTION]
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.inventory_list_title)) }, // Corrected string resource name
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
)
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = {
Timber.i("[INFO][ACTION][ui_interaction] Sync inventory triggered.")
viewModel.onSyncClicked()
}) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(id = R.string.content_desc_sync_inventory)
)
}
}
) { innerPadding ->
// [DELEGATES]
Column(modifier = Modifier.padding(innerPadding)) {
SearchBar(
query = uiState.searchQuery,
onQueryChange = viewModel::onSearchQueryChanged
)
InventoryListContent(
isLoading = uiState.isLoading,
items = uiState.items,
onItemClick = onItemClick
)
}
}
// [END_FUNCTION_InventoryListScreen]
}
// [END_ENTITY: Function('InventoryListScreen')]
// [ENTITY: Function('SearchBar')]
// [RELATION: Function('SearchBar') -> [CALLS] -> Function('TextField')]
// [RELATION: Function('SearchBar') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SearchBar') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('SearchBar') -> [CALLS] -> Function('Icon')]
/**
* [CONTRACT]
* Поле для ввода поискового запроса.
*/
@Composable
private fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
TextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
placeholder = { Text(stringResource(id = R.string.search)) }, // Corrected string resource name
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }
)
}
// [END_ENTITY: Function('SearchBar')]
// [ENTITY: Function('InventoryListContent')]
// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('Box')]
// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('InventoryListContent') -> [CALLS] -> Function('ItemCard')]
/**
* [CONTRACT]
* Основной контент: индикатор загрузки или список предметов.
*/
@Composable
private fun InventoryListContent(
isLoading: Boolean,
items: List<Item>,
onItemClick: (Item) -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
if (isLoading) {
// [STATE]
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else if (items.isEmpty()) {
// [FALLBACK]
Text(
text = stringResource(id = R.string.items_not_found),
modifier = Modifier.align(Alignment.Center)
)
} else {
// [CORE-LOGIC]
LazyColumn {
items(items, key = { it.id }) { item ->
ItemCard(item = item, onClick = {
Timber.i("[INFO][ACTION][ui_interaction] Item clicked: ${item.name}")
onItemClick(item)
})
}
}
}
}
}
// [END_ENTITY: Function('InventoryListContent')]
// [ENTITY: Function('ItemCard')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Card')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Column')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemCard') -> [CALLS] -> Function('clickable')]
/**
* [CONTRACT]
* Карточка для отображения одного элемента инвентаря.
*/
@Composable
private fun ItemCard(
item: Item,
onClick: () -> Unit
) {
// [PRECONDITION]
require(item.name.isNotBlank()) { "Item name cannot be blank." }
// [CORE-LOGIC]
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 4.dp)
.clickable(onClick = onClick)
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(text = item.name, style = androidx.compose.material3.MaterialTheme.typography.titleMedium)
Text(text = "Quantity: ${item.quantity.toString()}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall)
item.location?.let {
Text(text = "Location: ${it.name}", style = androidx.compose.material3.MaterialTheme.typography.bodySmall)
}
}
}
}
// [END_ENTITY: Function('ItemCard')]
// [END_CONTRACT]
// [END_FILE_InventoryListScreen.kt]

View File

@@ -1,18 +1,53 @@
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
// [FILE] InventoryListViewModel.kt
// [SEMANTICS] ui_logic, inventory_list, viewmodel
package com.homebox.lens.ui.screen.inventorylist
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.homebox.lens.domain.model.Item
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('InventoryListViewModel')]
// [RELATION: ViewModel('InventoryListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('InventoryListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
/**
* [CONTRACT]
* @summary ViewModel for the InventoryListScreen.
*/
@HiltViewModel
class InventoryListViewModel
@Inject
constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
private val _uiState = MutableStateFlow(InventoryListUiState())
val uiState: StateFlow<InventoryListUiState> = _uiState.asStateFlow()
fun onSyncClicked() {
// TODO: Implement sync logic
}
fun onSearchQueryChanged(query: String) {
// TODO: Implement search query change logic
}
}
// [END_ENTITY: ViewModel('InventoryListViewModel')]
// [END_CONTRACT]
// [END_FILE_InventoryListViewModel.kt]
// [CONTRACT]
// [ENTITY: DataClass('InventoryListUiState')]
// [RELATION: DataClass('InventoryListUiState') -> [DEPENDS_ON] -> Class('Item')]
data class InventoryListUiState(
val searchQuery: String = "",
val isLoading: Boolean = false,
val items: List<Item> = emptyList()
)
// [END_ENTITY: DataClass('InventoryListUiState')]

View File

@@ -1,38 +1,208 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
// [FILE] ItemDetailsScreen.kt
// [SEMANTICS] ui, screen, item, details
// [SEMANTICS] ui, screen, item, details, compose
package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
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.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
// [ENTRYPOINT]
import com.homebox.lens.domain.model.Item
import timber.log.Timber
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Function('ItemDetailsScreen')]
// [RELATION: Function('ItemDetailsScreen') -> [DEPENDS_ON] -> Class('ItemDetailsViewModel')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('ItemDetailsScreen') -> [CALLS] -> Function('ItemDetailsContent')]
/**
* [CONTRACT]
* @summary Composable-функция для экрана "Детали элемента".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* [MAIN-CONTRACT]
* Экран для отображения детальной информации о товаре.
*
* Реализует спецификацию `screen_item_details`.
*
* @param onNavigateBack Обработчик для возврата на предыдущий экран.
* @param onEditClick Обработчик нажатия на кнопку редактирования.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemDetailsScreen(
currentRoute: String?,
navigationActions: NavigationActions,
viewModel: ItemDetailsViewModel = hiltViewModel(),
onNavigateBack: () -> Unit,
onEditClick: (Int) -> Unit
) {
// [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.item_details_title),
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
// [CORE-LOGIC]
Text(text = "TODO: Item Details Screen")
// [STATE]
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text(uiState.item?.name ?: stringResource(id = R.string.item_details_title)) }, // Corrected string resource name
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back))
}
},
actions = {
IconButton(onClick = {
uiState.item?.id?.let {
Timber.i("[INFO][ACTION][ui_interaction] Edit item clicked: id=$it")
onEditClick(it.toInt())
}
}) {
Icon(Icons.Default.Edit, contentDescription = stringResource(id = R.string.content_desc_edit_item))
}
IconButton(onClick = {
Timber.w("[WARN][ACTION][ui_interaction] Delete item clicked: id=${uiState.item?.id}")
viewModel.deleteItem()
// После удаления нужно навигироваться назад
onNavigateBack()
}) {
Icon(Icons.Default.Delete, contentDescription = stringResource(id = R.string.content_desc_delete_item))
}
}
)
}
) { innerPadding ->
ItemDetailsContent(
modifier = Modifier.padding(innerPadding),
isLoading = uiState.isLoading,
item = uiState.item
)
}
// [END_FUNCTION_ItemDetailsScreen]
}
// [END_ENTITY: Function('ItemDetailsScreen')]
// [ENTITY: Function('ItemDetailsContent')]
// [RELATION: Function('ItemDetailsContent') -> [DEPENDS_ON] -> Class('Item')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Box')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('Column')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('verticalScroll')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('rememberScrollState')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('DetailsSection')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('InfoRow')]
// [RELATION: Function('ItemDetailsContent') -> [CALLS] -> Function('AssistChip')]
/**
* [CONTRACT]
* Отображает контент экрана: индикатор загрузки или детали товара.
*/
@Composable
private fun ItemDetailsContent(
modifier: Modifier = Modifier,
isLoading: Boolean,
item: Item?
) {
Box(modifier = modifier.fillMaxSize()) {
when {
isLoading -> {
// [STATE]
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
item == null -> {
// [FALLBACK]
Text(stringResource(id = R.string.items_not_found), modifier = Modifier.align(Alignment.Center))
}
else -> {
// [CORE-LOGIC]
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// TODO: ImageCarousel
// Text("Image Carousel Placeholder")
DetailsSection(title = stringResource(id = R.string.section_title_description)) {
Text(text = item.description ?: stringResource(id = R.string.placeholder_no_description))
}
DetailsSection(title = stringResource(id = R.string.section_title_details)) {
InfoRow(label = stringResource(id = R.string.label_quantity), value = item.quantity.toString())
item.location?.let {
InfoRow(label = stringResource(id = R.string.label_location), value = it.name)
}
}
if (item.labels.isNotEmpty()) {
DetailsSection(title = stringResource(id = R.string.section_title_labels)) {
// TODO: Use FlowRow for better layout
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
item.labels.forEach { label ->
AssistChip(onClick = { /* No-op */ }, label = { Text(label.name) })
}
}
}
}
// TODO: CustomFieldsGrid
}
}
}
}
}
// [END_ENTITY: Function('ItemDetailsContent')]
// [ENTITY: Function('DetailsSection')]
// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Column')]
// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Text')]
// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('DetailsSection') -> [CALLS] -> Function('Divider')]
/**
* [CONTRACT]
* Секция с заголовком и контентом.
*/
@Composable
private fun DetailsSection(title: String, content: @Composable ColumnScope.() -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = title, style = MaterialTheme.typography.titleMedium)
Divider()
content()
}
}
// [END_ENTITY: Function('DetailsSection')]
// [ENTITY: Function('InfoRow')]
// [RELATION: Function('InfoRow') -> [CALLS] -> Function('Row')]
// [RELATION: Function('InfoRow') -> [CALLS] -> Function('Text')]
// [RELATION: Function('InfoRow') -> [CALLS] -> Function('MaterialTheme.typography.bodyLarge')]
/**
* [CONTRACT]
* Строка для отображения пары "метка: значение".
*/
@Composable
private fun InfoRow(label: String, value: String) {
Row {
Text(text = "$label: ", style = MaterialTheme.typography.bodyLarge)
Text(text = value, style = MaterialTheme.typography.bodyLarge)
}
}
// [END_ENTITY: Function('InfoRow')]
// [END_CONTRACT]
// [END_FILE_ItemDetailsScreen.kt]

View File

@@ -3,16 +3,41 @@
package com.homebox.lens.ui.screen.itemdetails
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import com.homebox.lens.domain.model.Item
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('ItemDetailsViewModel')]
// [RELATION: ViewModel('ItemDetailsViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('ItemDetailsViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
/**
* [CONTRACT]
* @summary ViewModel for the ItemDetailsScreen.
*/
@HiltViewModel
class ItemDetailsViewModel
@Inject
constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
val uiState = MutableStateFlow(ItemDetailsUiState()).asStateFlow()
fun deleteItem() {
// TODO: Implement delete item logic
}
}
// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
// [END_CONTRACT]
// [END_FILE_ItemDetailsViewModel.kt]
// Placeholder for ItemDetailsUiState to resolve compilation errors
data class ItemDetailsUiState(
val item: Item? = null,
val isLoading: Boolean = false
)

View File

@@ -1,38 +1,162 @@
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
// [FILE] ItemEditScreen.kt
// [SEMANTICS] ui, screen, item, edit
// [SEMANTICS] ui, screen, item, edit, create, compose
package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
import timber.log.Timber
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Function('ItemEditScreen')]
// [RELATION: Function('ItemEditScreen') -> [DEPENDS_ON] -> Class('ItemEditViewModel')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('LaunchedEffect')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('ItemEditScreen') -> [CALLS] -> Function('ItemEditContent')]
/**
* [CONTRACT]
* @summary Composable-функция для экрана "Редактирование элемента".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* [MAIN-CONTRACT]
* Экран для создания или редактирования товара.
*
* Реализует спецификацию `screen_item_edit`.
*
* @param onNavigateBack Обработчик для возврата на предыдущий экран после сохранения или отмены.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ItemEditScreen(
currentRoute: String?,
navigationActions: NavigationActions,
viewModel: ItemEditViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
// [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.item_edit_title),
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
// [CORE-LOGIC]
Text(text = "TODO: Item Edit Screen")
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [SIDE-EFFECT]
LaunchedEffect(uiState.isSaved) {
if (uiState.isSaved) {
Timber.i("[INFO][SIDE_EFFECT][navigation] Item saved, navigating back.")
onNavigateBack()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = if (uiState.isEditing) R.string.item_edit_title else R.string.item_edit_title_create)) }, // Corrected string resource names
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.content_desc_navigate_back))
}
},
actions = {
IconButton(onClick = {
Timber.i("[INFO][ACTION][ui_interaction] Save item clicked.")
viewModel.saveItem()
}) {
Icon(Icons.Default.Done, contentDescription = stringResource(id = R.string.content_desc_save_item))
}
}
)
}
) { innerPadding ->
ItemEditContent(
modifier = Modifier.padding(innerPadding),
state = uiState,
onNameChange = { viewModel.onNameChange(it) },
onDescriptionChange = { viewModel.onDescriptionChange(it) },
onQuantityChange = { viewModel.onQuantityChange(it) }
)
}
// [END_FUNCTION_ItemEditScreen]
}
// [END_ENTITY: Function('ItemEditScreen')]
// [ENTITY: Function('ItemEditContent')]
// [RELATION: Function('ItemEditContent') -> [DEPENDS_ON] -> Class('ItemEditUiState')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('Column')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('verticalScroll')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('rememberScrollState')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('OutlinedTextField')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('ItemEditContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
/**
* [CONTRACT]
* Отображает форму для редактирования данных товара.
*/
@Composable
private fun ItemEditContent(
modifier: Modifier = Modifier,
state: ItemEditUiState,
onNameChange: (String) -> Unit,
onDescriptionChange: (String) -> Unit,
onQuantityChange: (String) -> Unit
) {
// [CORE-LOGIC]
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedTextField(
value = state.name,
onValueChange = onNameChange,
label = { Text(stringResource(id = R.string.label_name)) },
modifier = Modifier.fillMaxWidth(),
isError = state.nameError != null
)
state.nameError?.let {
Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error)
}
OutlinedTextField(
value = state.description,
onValueChange = onDescriptionChange,
label = { Text(stringResource(id = R.string.label_description)) },
modifier = Modifier.fillMaxWidth(),
minLines = 3
)
OutlinedTextField(
value = state.quantity,
onValueChange = onQuantityChange,
label = { Text(stringResource(id = R.string.label_quantity)) },
modifier = Modifier.fillMaxWidth(),
isError = state.quantityError != null
)
state.quantityError?.let {
Text(text = stringResource(id = it), color = MaterialTheme.colorScheme.error)
}
// TODO: Location Dropdown
// TODO: Labels ChipGroup
// TODO: ImagePicker
}
}
// [END_ENTITY: Function('ItemEditContent')]
// [END_CONTRACT]
// [END_FILE_ItemEditScreen.kt]

View File

@@ -3,16 +3,57 @@
package com.homebox.lens.ui.screen.itemedit
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('ItemEditViewModel')]
// [RELATION: ViewModel('ItemEditViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('ItemEditViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
/**
* [CONTRACT]
* @summary ViewModel for the ItemEditScreen.
*/
@HiltViewModel
class ItemEditViewModel
@Inject
constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
val uiState = MutableStateFlow(ItemEditUiState()).asStateFlow()
fun saveItem() {
// TODO: Implement save item logic
}
fun onNameChange(name: String) {
// TODO: Implement name change logic
}
fun onDescriptionChange(description: String) {
// TODO: Implement description change logic
}
fun onQuantityChange(quantity: String) {
// TODO: Implement quantity change logic
}
}
// [END_ENTITY: ViewModel('ItemEditViewModel')]
// [END_CONTRACT]
// [END_FILE_ItemEditViewModel.kt]
// Placeholder for ItemEditUiState to resolve compilation errors
data class ItemEditUiState(
val isSaved: Boolean = false,
val isEditing: Boolean = false,
val name: String = "",
val description: String = "",
val quantity: String = "",
val nameError: Int? = null,
val quantityError: Int? = null
)

View File

@@ -1,11 +1,14 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt
// [SEMANTICS] ui, screen, jetpack_compose, labels_list, state_management
// [PACKAGE]com.homebox.lens.ui.screen.labelslist
// [FILE]LabelsListScreen.kt
// [SEMANTICS]ui, screen, labels, list, compose
package com.homebox.lens.ui.screen.labelslist
// [SECTION] Imports
// [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.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -13,6 +16,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Label
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
@@ -22,184 +26,178 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.homebox.lens.R
import com.homebox.lens.domain.model.Label
import com.homebox.lens.ui.theme.HomeboxLensTheme
import com.homebox.lens.ui.screen.labelslist.LabelsListUiState
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Class('LabelsListScreen')]
// [RELATION: Class('LabelsListScreen')] -> [DEPENDS_ON] -> [Class('LabelsListViewModel')]
// [RELATION: Class('LabelsListScreen')] -> [READS_FROM] -> [DataStructure('LabelsListUiState')]
// [CONTRACT]
// [ENTITY: Function('LabelsListScreen')]
// [RELATION: Function('LabelsListScreen') -> [DEPENDS_ON] -> SealedInterface('LabelsListUiState')]
// [RELATION: Function('LabelsListScreen') -> [CREATES_INSTANCE_OF] -> Class('Scaffold')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('LabelsListContent')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('FloatingActionButton')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LabelsListScreen') -> [CALLS] -> Function('CircularProgressIndicator')]
/**
* [MAIN-CONTRACT]
* Экран для отображения списка всех меток.
*
* @param onLabelClick Функция обратного вызова при нажатии на метку. Передает ID метки.
* @param onAddNewLabelClick Функция обратного вызова для инициирования процесса создания новой метки.
* Этот Composable является точкой входа для UI, определенного в спецификации `screen_labels_list`.
* Он получает состояние от [LabelsListViewModel] и отображает его, делегируя обработку
* пользовательских событий в ViewModel.
*
* @param uiState Текущее состояние UI для экрана списка меток.
* @param onLabelClick Функция обратного вызова для обработки нажатия на метку.
* @param onAddClick Функция обратного вызова для обработки нажатия на кнопку добавления метки.
* @param onNavigateBack Функция обратного вызова для навигации назад.
* @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LabelsListScreen(
onLabelClick: (String) -> Unit,
onAddNewLabelClick: () -> Unit,
fun labelsListScreen(
uiState: LabelsListUiState,
onLabelClick: (Label) -> Unit,
onAddClick: () -> Unit,
onNavigateBack: () -> Unit,
viewModel: LabelsListViewModel = hiltViewModel(),
) {
// [STATE]
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// [CONTRACT_VALIDATOR]
// В Compose UI контракты проверяются через состояние и события.
Scaffold(
topBar = {
// [ENTITY: Function('LabelsTopAppBar')]
LabelsTopAppBar(onNavigateBack = onNavigateBack)
TopAppBar(
title = { Text(stringResource(id = R.string.screen_title_labels)) },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_desc_navigate_back)
)
}
},
)
},
floatingActionButton = {
// [ENTITY: Function('LabelsFloatingActionButton')]
LabelsFloatingActionButton(onAddNewLabelClick = onAddNewLabelClick)
},
) { innerPadding ->
// [ENTITY: Function('LabelsListContent')]
// [RELATION: Function('LabelsListContent')] -> [CALLS] -> [Function('onLabelClick')]
LabelsListContent(
modifier = Modifier.padding(innerPadding),
labels = uiState.labels,
onLabelClick = onLabelClick,
)
}
}
/**
* [CONTRACT]
* Верхняя панель для экрана списка меток.
* @param onNavigateBack Функция для навигации назад.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LabelsTopAppBar(onNavigateBack: () -> Unit) {
// [PRECONDITION]
require(true) { "onNavigateBack must be a valid function." } // В Compose предусловия часто неявные
TopAppBar(
title = { Text(stringResource(id = R.string.screen_title_labels)) },
navigationIcon = {
IconButton(onClick = {
// [ACTION]
Timber.i("[INFO][ACTION][navigating_back] Navigate back from LabelsListScreen.")
onNavigateBack()
}) {
FloatingActionButton(onClick = onAddClick) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.content_desc_navigate_back),
imageVector = Icons.Filled.Add,
contentDescription = stringResource(id = R.string.content_desc_add_label)
)
}
},
)
}
/**
* [CONTRACT]
* Плавающая кнопка действия для добавления новой метки.
* @param onAddNewLabelClick Функция для вызова экрана создания метки.
*/
@Composable
private fun LabelsFloatingActionButton(onAddNewLabelClick: () -> Unit) {
// [PRECONDITION]
require(true) { "onAddNewLabelClick must be a valid function." }
FloatingActionButton(
onClick = {
// [ACTION]
Timber.i("[INFO][ACTION][initiating_add_new_label] FAB clicked to add a new label.")
onAddNewLabelClick()
},
) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = stringResource(id = R.string.content_desc_add_label),
)
}
) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) {
when (uiState) {
is LabelsListUiState.Loading -> {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
CircularProgressIndicator()
}
}
is LabelsListUiState.Success -> {
LabelsListContent(
uiState = uiState,
onLabelClick = onLabelClick
)
}
is LabelsListUiState.Error -> {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = uiState.message)
}
}
}
}
}
}
// [END_ENTITY: Function('LabelsListScreen')]
// [ENTITY: Function('LabelsListContent')]
// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('LabelListItem')]
// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LabelsListContent') -> [CALLS] -> Function('LazyColumn')]
/**
* [CONTRACT]
* Основной контент экрана - список меток.
* Отображает основной контент экрана: список меток.
*
* @param modifier Модификатор для компоновки.
* @param labels Список меток для отображения.
* @param onLabelClick Обработчик нажатия на метку.
* @sideeffect Вызывает [onLabelClick] при взаимодействии пользователя.
* @param uiState Состояние успеха, содержащее список меток.
* @param onLabelClick Обработчик нажатия на элемент списка.
* @sideeffect Отсутствуют.
*/
@Composable
private fun LabelsListContent(
modifier: Modifier = Modifier,
labels: List<Label>,
onLabelClick: (String) -> Unit,
uiState: LabelsListUiState.Success,
onLabelClick: (Label) -> Unit
) {
if (uiState.labels.isEmpty()) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = stringResource(id = R.string.no_labels_found))
}
} else {
LazyColumn {
items(uiState.labels, key = { it.id }) { label ->
LabelListItem(
label = label,
onClick = {
Timber.i("[INFO][ACTION][ui_interaction] Label clicked: ${label.name}")
onLabelClick(label)
}
)
}
}
}
}
// [END_ENTITY: Function('LabelsListContent')]
// [ENTITY: Function('LabelListItem')]
// [RELATION: Function('LabelListItem') -> [CALLS] -> Function('ListItem')]
// [RELATION: Function('LabelListItem') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LabelListItem') -> [CALLS] -> Function('Icon')]
/**
* [CONTRACT]
* Отображает один элемент в списке меток.
*
* @param label Метка для отображения.
* @param onClick Обработчик нажатия на элемент.
* @sideeffect Отсутствуют.
*/
@Composable
private fun LabelListItem(
label: Label,
onClick: () -> Unit
) {
// [PRECONDITION]
requireNotNull(labels) { "Labels list cannot be null." }
require(label.name.isNotBlank()) { "Label name cannot be blank." }
LazyColumn(modifier = modifier) {
items(labels, key = { it.id }) { label ->
// [ENTITY: DataStructure('LabelListItem')]
ListItem(
headlineContent = { Text(label.name) },
leadingContent = {
Icon(
imageVector = Icons.AutoMirrored.Filled.Label,
contentDescription = null, // Декоративная иконка
)
},
modifier =
Modifier.clickable {
// [ACTION]
Timber.i("[INFO][ACTION][handling_label_click] Label clicked: id='${label.id}', name='${label.name}'")
onLabelClick(label.id)
},
// [CORE-LOGIC]
ListItem(
headlineContent = { Text(label.name) },
leadingContent = {
Icon(
imageVector = Icons.AutoMirrored.Filled.Label,
contentDescription = null // Декоративный элемент
)
}
}
// [POSTCONDITION]
// В декларативном UI постусловие - это корректное отображение предоставленного состояния.
check(true) { "LazyColumn rendering is managed by Compose runtime." }
},
modifier = Modifier.clickable(onClick = onClick)
)
}
// [SECTION] Previews
@Preview(showBackground = true)
@Composable
private fun LabelsListScreenPreview() {
HomeboxLensTheme {
val sampleLabels =
listOf(
Label(id = "1", name = "Electronics", color = "#FF0000"),
Label(id = "2", name = "Books", color = "#00FF00"),
Label(id = "3", name = "Documents", color = "#0000FF"),
)
// [HELPER]
// Для превью мы не можем использовать реальный ViewModel, поэтому создаем заглушки.
Scaffold(
topBar = { LabelsTopAppBar(onNavigateBack = {}) },
floatingActionButton = { LabelsFloatingActionButton(onAddNewLabelClick = {}) },
) { padding ->
LabelsListContent(
modifier = Modifier.padding(padding),
labels = sampleLabels,
onLabelClick = {},
)
}
}
}
// [COHERENCE_CHECK_PASSED]
// [END_ENTITY: Function('LabelListItem')]
// [END_CONTRACT]
// [END_FILE_LabelsListScreen.kt]

View File

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

View File

@@ -15,10 +15,13 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('LabelsListViewModel')]
// [RELATION: ViewModel('LabelsListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('LabelsListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('LabelsListViewModel') -> [DEPENDS_ON] -> Class('GetAllLabelsUseCase')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком меток.
@@ -40,6 +43,15 @@ class LabelsListViewModel
loadLabels()
}
// [ENTITY: Function('loadLabels')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadLabels') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('runCatching')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('getAllLabelsUseCase')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('result.fold')]
// [RELATION: Function('loadLabels') -> [CALLS] -> Function('Timber.e')]
// [RELATION: Function('loadLabels') -> [CREATES_INSTANCE_OF] -> Class('Label')]
/**
* [CONTRACT]
* @summary Загружает список меток.
@@ -49,7 +61,6 @@ class LabelsListViewModel
*/
// [ACTION]
fun loadLabels() {
// [ENTRYPOINT]
viewModelScope.launch {
_uiState.value = LabelsListUiState.Loading
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
@@ -85,7 +96,11 @@ class LabelsListViewModel
)
}
}
// [END_ENTITY: Function('loadLabels')]
// [ENTITY: Function('onShowCreateDialog')]
// [RELATION: Function('onShowCreateDialog') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('onShowCreateDialog') -> [CALLS] -> Function('_uiState.update')]
/**
* [CONTRACT]
* @summary Инициирует отображение диалога для создания метки.
@@ -101,11 +116,15 @@ class LabelsListViewModel
}
}
}
// [END_ENTITY: Function('onShowCreateDialog')]
// [ENTITY: Function('onDismissCreateDialog')]
// [RELATION: Function('onDismissCreateDialog') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('onDismissCreateDialog') -> [CALLS] -> Function('_uiState.update')]
/**
* [CONTRACT]
* @summary Скрывает диалог создания метки.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`..
* @sideeffect Обновляет `_uiState`.
*/
// [ACTION]
@@ -117,7 +136,12 @@ class LabelsListViewModel
}
}
}
// [END_ENTITY: Function('onDismissCreateDialog')]
// [ENTITY: Function('createLabel')]
// [RELATION: Function('createLabel') -> [CALLS] -> Function('require')]
// [RELATION: Function('createLabel') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('createLabel') -> [CALLS] -> Function('onDismissCreateDialog')]
/**
* [CONTRACT]
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
@@ -132,7 +156,6 @@ class LabelsListViewModel
// [PRECONDITION]
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
// [ENTRYPOINT]
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
@@ -142,4 +165,6 @@ class LabelsListViewModel
// [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список.
}
}
// [END_CLASS_LabelsListViewModel]
// [END_ENTITY: ViewModel('LabelsListViewModel')]
// [END_CONTRACT]
// [END_FILE_LabelsListViewModel.kt]

View File

@@ -15,9 +15,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.homebox.lens.R
// [END_IMPORTS]
// [ENTRYPOINT]
// [CONTRACT]
// [ENTITY: Function('LocationEditScreen')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('Box')]
// [RELATION: Function('LocationEditScreen') -> [CALLS] -> Function('Text')]
/**
* [CONTRACT]
* @summary Composable-функция для экрана "Редактирование местоположения".
@@ -44,3 +49,6 @@ fun LocationEditScreen(locationId: String?) {
}
}
}
// [END_ENTITY: Function('LocationEditScreen')]
// [END_CONTRACT]
// [END_FILE_LocationEditScreen.kt]

View File

@@ -49,9 +49,20 @@ 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]
// [ENTRYPOINT]
// [CONTRACT]
// [ENTITY: Function('LocationsListScreen')]
// [RELATION: Function('LocationsListScreen') -> [DEPENDS_ON] -> Class('NavigationActions')]
// [RELATION: Function('LocationsListScreen') -> [DEPENDS_ON] -> Class('LocationsListViewModel')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('MainScaffold')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('FloatingActionButton')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('LocationsListScreen') -> [CALLS] -> Function('LocationsListContent')]
/**
* [CONTRACT]
* @summary Composable-функция для экрана "Список местоположений".
@@ -99,9 +110,17 @@ fun LocationsListScreen(
}
}
}
// [END_ENTITY: Function('LocationsListScreen')]
// [HELPER]
// [ENTITY: Function('LocationsListContent')]
// [RELATION: Function('LocationsListContent') -> [DEPENDS_ON] -> SealedInterface('LocationsListUiState')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('Box')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('LocationsListContent') -> [CALLS] -> Function('LocationCard')]
/**
* [CONTRACT]
* @summary Отображает основной контент экрана в зависимости от `uiState`.
@@ -164,9 +183,26 @@ private fun LocationsListContent(
}
}
}
// [END_ENTITY: Function('LocationsListContent')]
// [UI_COMPONENT]
// [ENTITY: Function('LocationCard')]
// [RELATION: Function('LocationCard') -> [DEPENDS_ON] -> Class('LocationOutCount')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('remember')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('mutableStateOf')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Card')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('clickable')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Row')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Column')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Text')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('MaterialTheme.typography.titleMedium')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('MaterialTheme.typography.bodyMedium')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Spacer')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Box')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('DropdownMenu')]
// [RELATION: Function('LocationCard') -> [CALLS] -> Function('DropdownMenuItem')]
/**
* [CONTRACT]
* @summary Карточка для отображения одного местоположения.
@@ -230,7 +266,13 @@ private fun LocationCard(
}
}
}
// [END_ENTITY: Function('LocationCard')]
// [ENTITY: Function('LocationsListSuccessPreview')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('LocationOutCount')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListSuccessPreview') -> [CALLS] -> Function('LocationsListUiState.Success')]
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Success")
@Composable
@@ -250,7 +292,12 @@ fun LocationsListSuccessPreview() {
)
}
}
// [END_ENTITY: Function('LocationsListSuccessPreview')]
// [ENTITY: Function('LocationsListEmptyPreview')]
// [RELATION: Function('LocationsListEmptyPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListEmptyPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListEmptyPreview') -> [CALLS] -> Function('LocationsListUiState.Success')]
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Empty")
@Composable
@@ -264,7 +311,12 @@ fun LocationsListEmptyPreview() {
)
}
}
// [END_ENTITY: Function('LocationsListEmptyPreview')]
// [ENTITY: Function('LocationsListLoadingPreview')]
// [RELATION: Function('LocationsListLoadingPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListLoadingPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListLoadingPreview') -> [CALLS] -> Function('LocationsListUiState.Loading')]
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Loading")
@Composable
@@ -278,7 +330,13 @@ fun LocationsListLoadingPreview() {
)
}
}
// [END_ENTITY: Function('LocationsListLoadingPreview')]
// [ENTITY: Function('LocationsListErrorPreview')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('HomeboxLensTheme')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('LocationsListContent')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('LocationsListUiState.Error')]
// [RELATION: Function('LocationsListErrorPreview') -> [CALLS] -> Function('stringResource')]
// [PREVIEW]
@Preview(showBackground = true, name = "Locations List Error")
@Composable
@@ -292,3 +350,6 @@ fun LocationsListErrorPreview() {
)
}
}
// [END_ENTITY: Function('LocationsListErrorPreview')]
// [END_CONTRACT]
// [END_FILE_LocationsListScreen.kt]

View File

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

View File

@@ -4,6 +4,7 @@
package com.homebox.lens.ui.screen.locationslist
// [IMPORTS]
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
@@ -13,9 +14,13 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
// [END_IMPORTS]
// [CORE-LOGIC]
// [CONTRACT]
// [ENTITY: ViewModel('LocationsListViewModel')]
// [RELATION: ViewModel('LocationsListViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('LocationsListViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('LocationsListViewModel') -> [DEPENDS_ON] -> Class('GetAllLocationsUseCase')]
/**
* [CONTRACT]
* @summary ViewModel для экрана списка местоположений.
@@ -38,8 +43,10 @@ class LocationsListViewModel
loadLocations()
}
// [ACTION]
// [ENTITY: Function('loadLocations')]
// [RELATION: Function('loadLocations') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadLocations') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('loadLocations') -> [CALLS] -> Function('getAllLocationsUseCase')]
/**
* [CONTRACT]
* @summary Загружает список местоположений из репозитория.
@@ -56,6 +63,8 @@ class LocationsListViewModel
}
}
}
// [END_CLASS_LocationsListViewModel]
// [END_ENTITY: Function('loadLocations')]
}
// [END_FILE_LocationsListViewModel.kt]
// [END_ENTITY: ViewModel('LocationsListViewModel')]
// [END_CONTRACT]
// [END_FILE_LocationsListViewModel.kt]

View File

@@ -1,38 +1,129 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchScreen.kt
// [SEMANTICS] ui, screen, search
// [SEMANTICS] ui, screen, search, compose
package com.homebox.lens.ui.screen.search
// [IMPORTS]
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
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.material3.*
import androidx.compose.runtime.*
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.navigation.NavigationActions
import com.homebox.lens.ui.common.MainScaffold
// [ENTRYPOINT]
import com.homebox.lens.domain.model.Item
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Function('SearchScreen')]
// [RELATION: Function('SearchScreen') -> [DEPENDS_ON] -> Class('SearchViewModel')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('Scaffold')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('TopAppBar')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('TextField')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('IconButton')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('Icon')]
// [RELATION: Function('SearchScreen') -> [CALLS] -> Function('SearchContent')]
/**
* [CONTRACT]
* @summary Composable-функция для экрана "Поиск".
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigationActions Объект с навигационными действиями.
* [MAIN-CONTRACT]
* Специализированный экран для поиска товаров.
*
* Реализует спецификацию `screen_search`.
*
* @param onNavigateBack Обработчик для возврата на предыдущий экран.
* @param onItemClick Обработчик нажатия на найденный товар.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchScreen(
currentRoute: String?,
navigationActions: NavigationActions,
viewModel: SearchViewModel = hiltViewModel(),
onNavigateBack: () -> Unit,
onItemClick: (Item) -> Unit
) {
// [UI_COMPONENT]
MainScaffold(
topBarTitle = stringResource(id = R.string.search_title),
currentRoute = currentRoute,
navigationActions = navigationActions,
) {
// [CORE-LOGIC]
Text(text = "TODO: Search Screen")
// [STATE]
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
TextField(
value = uiState.searchQuery,
onValueChange = viewModel::onSearchQueryChanged,
placeholder = { Text(stringResource(R.string.placeholder_search_items)) },
modifier = Modifier.fillMaxWidth()
)
},
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.content_desc_navigate_back))
}
}
)
}
) { innerPadding ->
SearchContent(
modifier = Modifier.padding(innerPadding),
isLoading = uiState.isLoading,
results = uiState.results,
onItemClick = onItemClick
)
}
// [END_FUNCTION_SearchScreen]
}
// [END_ENTITY: Function('SearchScreen')]
// [ENTITY: Function('SearchContent')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('LazyColumn')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('ListItem')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SearchContent') -> [CALLS] -> Function('clickable')]
/**
* [CONTRACT]
* Отображает основной контент экрана: фильтры и результаты поиска.
*/
@Composable
private fun SearchContent(
modifier: Modifier = Modifier,
isLoading: Boolean,
results: List<Item>,
onItemClick: (Item) -> Unit
) {
Column(modifier = modifier.fillMaxSize()) {
// [SECTION] FILTERS
// TODO: Implement FilterSection with chips for locations/labels
// Spacer(modifier = Modifier.height(8.dp))
// [SECTION] RESULTS
Box(modifier = Modifier.weight(1f)) {
if (isLoading) {
// [STATE]
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
} else {
// [CORE-LOGIC]
LazyColumn {
items(results, key = { it.id }) { item ->
ListItem(
headlineContent = { Text(item.name) },
supportingContent = { Text(item.location?.name ?: "") },
modifier = Modifier.then(Modifier.clickable { onItemClick(item) })
)
}
}
}
}
}
}
// [END_ENTITY: Function('SearchContent')]
// [END_CONTRACT]
// [END_FILE_SearchScreen.kt]

View File

@@ -1,18 +1,44 @@
// [PACKAGE] com.homebox.lens.ui.screen.search
// [FILE] SearchViewModel.kt
// [SEMANTICS] ui_logic, search, viewmodel
package com.homebox.lens.ui.screen.search
// [IMPORTS]
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('SearchViewModel')]
// [RELATION: ViewModel('SearchViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('SearchViewModel') -> [DEPENDS_ON] -> Annotation('HiltAndroidApp')]
/**
* [CONTRACT]
* @summary ViewModel for the SearchScreen.
*/
@HiltViewModel
class SearchViewModel
@Inject
constructor() : ViewModel() {
// [STATE]
// TODO: Implement UI state
val uiState = MutableStateFlow(SearchUiState()).asStateFlow()
fun onSearchQueryChanged(query: String) {
// TODO: Implement search query change logic
}
}
// [END_ENTITY: ViewModel('SearchViewModel')]
// [END_CONTRACT]
// [END_FILE_SearchViewModel.kt]
// Placeholder for SearchUiState to resolve compilation errors
data class SearchUiState(
val searchQuery: String = "",
val isLoading: Boolean = false,
val results: List<com.homebox.lens.domain.model.Item> = emptyList()
)

View File

@@ -1,144 +1,126 @@
// [PACKAGE] com.homebox.lens.ui.screen.setup
// [FILE] SetupScreen.kt
// [SEMANTICS] ui, screen, setup, compose
@file:OptIn(ExperimentalMaterial3Api::class)
// [SEMANTICS] ui, screen, setup, login, compose
package com.homebox.lens.ui.screen.setup
// [IMPORTS]
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.*
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.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
import timber.log.Timber
// [END_IMPORTS]
// [ENTRYPOINT]
// [CONTRACT]
// [ENTITY: Function('SetupScreen')]
// [RELATION: Function('SetupScreen') -> [DEPENDS_ON] -> Class('SetupViewModel')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('hiltViewModel')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('collectAsState')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('LaunchedEffect')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Timber.i')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Box')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Column')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Text')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('stringResource')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('MaterialTheme.typography.headlineMedium')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('OutlinedTextField')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('KeyboardOptions')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('KeyboardType.Uri')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('PasswordVisualTransformation')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('CircularProgressIndicator')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('Button')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('MaterialTheme.colorScheme.error')]
// [RELATION: Function('SetupScreen') -> [CALLS] -> Function('MaterialTheme.typography.bodyMedium')]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана настройки соединения с сервером.
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
* @sideeffect Вызывает `onSetupComplete` при изменении `uiState.isSetupComplete`.
* [MAIN-CONTRACT]
* Экран для начальной настройки соединения с сервером Homebox.
*
* @param onSetupComplete Обработчик, вызываемый после успешной настройки и входа.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SetupScreen(
viewModel: SetupViewModel = hiltViewModel(),
onSetupComplete: () -> Unit,
onSetupComplete: () -> Unit
) {
// [STATE]
val uiState by viewModel.uiState.collectAsState()
// [CORE-LOGIC]
if (uiState.isSetupComplete) {
onSetupComplete()
// [SIDE-EFFECT]
LaunchedEffect(uiState.isSetupComplete) {
if (uiState.isSetupComplete) {
Timber.i("[INFO][SIDE_EFFECT][navigation] Setup complete, navigating to main screen.")
onSetupComplete()
}
}
// [UI_COMPONENT]
SetupScreenContent(
uiState = uiState,
onServerUrlChange = viewModel::onServerUrlChange,
onUsernameChange = viewModel::onUsernameChange,
onPasswordChange = viewModel::onPasswordChange,
onConnectClick = viewModel::connect,
)
// [END_FUNCTION_SetupScreen]
}
// [HELPER]
/**
* [CONTRACT]
* @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 ->
// [CORE-LOGIC]
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
modifier =
Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(text = stringResource(id = R.string.screen_title_setup), style = MaterialTheme.typography.headlineMedium)
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
onValueChange = viewModel::onServerUrlChange,
label = { Text(stringResource(id = R.string.setup_server_url_label)) },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
isError = uiState.error != null
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = uiState.username,
onValueChange = onUsernameChange,
label = { Text(stringResource(id = R.string.setup_username_label)) },
value = uiState.password, // Changed from uiState.apiKey to uiState.password
onValueChange = viewModel::onPasswordChange, // Changed from viewModel::onApiKeyChange to viewModel::onPasswordChange
label = { Text(stringResource(id = R.string.setup_password_label)) }, // Changed from label_api_key to setup_password_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(),
isError = uiState.error != null
)
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))
if (uiState.isLoading) {
// [STATE]
CircularProgressIndicator()
} else {
// [ACTION]
Button(
onClick = {
Timber.i("[INFO][ACTION][ui_interaction] Login button clicked.")
viewModel.connect() // Changed from viewModel.login() to viewModel.connect()
},
modifier = Modifier.fillMaxWidth()
) {
Text(text = stringResource(id = R.string.setup_connect_button)) // Changed from button_connect to setup_connect_button
}
}
uiState.error?.let {
Spacer(modifier = Modifier.height(8.dp))
Text(text = it, color = MaterialTheme.colorScheme.error)
// [FALLBACK]
Text(
text = it,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
// [END_FUNCTION_SetupScreenContent]
}
// [PREVIEW]
@Preview(showBackground = true)
@Composable
fun SetupScreenPreview() {
SetupScreenContent(
uiState = SetupUiState(error = "Failed to connect"),
onServerUrlChange = {},
onUsernameChange = {},
onPasswordChange = {},
onConnectClick = {},
)
}
// [END_FILE_SetupScreen.kt]
// [END_ENTITY: Function('SetupScreen')]
// [END_CONTRACT]
// [END_FILE_SetupScreen.kt]

View File

@@ -4,6 +4,11 @@
package com.homebox.lens.ui.screen.setup
// [IMPORTS]
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: DataClass('SetupUiState')]
/**
* [ENTITY: DataClass('SetupUiState')]
* [CONTRACT]
@@ -24,4 +29,6 @@ data class SetupUiState(
val error: String? = null,
val isSetupComplete: Boolean = false,
)
// [END_FILE_SetupUiState.kt]
// [END_ENTITY: DataClass('SetupUiState')]
// [END_CONTRACT]
// [END_FILE_SetupUiState.kt]

View File

@@ -2,6 +2,7 @@
// [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
@@ -14,10 +15,14 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
// [END_IMPORTS]
// [VIEWMODEL]
// [CONTRACT]
// [ENTITY: ViewModel('SetupViewModel')]
// [RELATION: ViewModel('SetupViewModel') -> [INHERITS_FROM] -> Class('ViewModel')]
// [RELATION: ViewModel('SetupViewModel') -> [DEPENDS_ON] -> Annotation('HiltViewModel')]
// [RELATION: ViewModel('SetupViewModel') -> [DEPENDS_ON] -> Class('CredentialsRepository')]
// [RELATION: ViewModel('SetupViewModel') -> [DEPENDS_ON] -> Class('LoginUseCase')]
/**
* [CONTRACT]
* ViewModel для экрана первоначальной настройки (Setup).
@@ -46,13 +51,17 @@ class SetupViewModel
loadCredentials()
}
// [ENTITY: Function('loadCredentials')]
// [RELATION: Function('loadCredentials') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('loadCredentials') -> [CALLS] -> Function('credentialsRepository.getCredentials')]
// [RELATION: Function('loadCredentials') -> [CALLS] -> Function('collect')]
// [RELATION: Function('loadCredentials') -> [WRITES_TO] -> Property('_uiState')]
/**
* [CONTRACT]
* [HELPER] Загружает учетные данные из репозитория при инициализации.
* @summary Загружает учетные данные из репозитория при инициализации.
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
*/
private fun loadCredentials() {
// [ENTRYPOINT]
viewModelScope.launch {
// [CORE-LOGIC] Подписываемся на поток учетных данных.
credentialsRepository.getCredentials().collect { credentials ->
@@ -69,7 +78,10 @@ class SetupViewModel
}
}
}
// [END_ENTITY: Function('loadCredentials')]
// [ENTITY: Function('onServerUrlChange')]
// [RELATION: Function('onServerUrlChange') -> [WRITES_TO] -> Property('_uiState')]
/**
* [CONTRACT]
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
@@ -79,7 +91,10 @@ class SetupViewModel
fun onServerUrlChange(newUrl: String) {
_uiState.update { it.copy(serverUrl = newUrl) }
}
// [END_ENTITY: Function('onServerUrlChange')]
// [ENTITY: Function('onUsernameChange')]
// [RELATION: Function('onUsernameChange') -> [WRITES_TO] -> Property('_uiState')]
/**
* [CONTRACT]
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
@@ -89,7 +104,10 @@ class SetupViewModel
fun onUsernameChange(newUsername: String) {
_uiState.update { it.copy(username = newUsername) }
}
// [END_ENTITY: Function('onUsernameChange')]
// [ENTITY: Function('onPasswordChange')]
// [RELATION: Function('onPasswordChange') -> [WRITES_TO] -> Property('_uiState')]
/**
* [CONTRACT]
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
@@ -99,7 +117,15 @@ class SetupViewModel
fun onPasswordChange(newPassword: String) {
_uiState.update { it.copy(password = newPassword) }
}
// [END_ENTITY: Function('onPasswordChange')]
// [ENTITY: Function('connect')]
// [RELATION: Function('connect') -> [CALLS] -> Function('viewModelScope.launch')]
// [RELATION: Function('connect') -> [WRITES_TO] -> Property('_uiState')]
// [RELATION: Function('connect') -> [CREATES_INSTANCE_OF] -> Class('Credentials')]
// [RELATION: Function('connect') -> [CALLS] -> Function('credentialsRepository.saveCredentials')]
// [RELATION: Function('connect') -> [CALLS] -> Function('loginUseCase')]
// [RELATION: Function('connect') -> [CALLS] -> Function('fold')]
/**
* [CONTRACT]
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
@@ -111,7 +137,6 @@ class SetupViewModel
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
*/
fun connect() {
// [ENTRYPOINT]
viewModelScope.launch {
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
_uiState.update { it.copy(isLoading = true, error = null) }
@@ -140,6 +165,8 @@ class SetupViewModel
)
}
}
// [END_CLASS_SetupViewModel]
// [END_ENTITY: Function('connect')]
}
// [END_FILE_SetupViewModel.kt]
// [END_ENTITY: ViewModel('SetupViewModel')]
// [END_CONTRACT]
// [END_FILE_SetupViewModel.kt]

View File

@@ -1,16 +1,36 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Color.kt
// [SEMANTICS] ui, theme, color
package com.homebox.lens.ui.theme
// [IMPORTS]
import androidx.compose.ui.graphics.Color
// [END_IMPORTS]
// [CONTRACT]
// [ENTITY: Constant('Purple80')]
val Purple80 = Color(0xFFD0BCFF)
// [END_ENTITY: Constant('Purple80')]
// [ENTITY: Constant('PurpleGrey80')]
val PurpleGrey80 = Color(0xFFCCC2DC)
// [END_ENTITY: Constant('PurpleGrey80')]
// [ENTITY: Constant('Pink80')]
val Pink80 = Color(0xFFEFB8C8)
// [END_ENTITY: Constant('Pink80')]
// [ENTITY: Constant('Purple40')]
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
// [END_ENTITY: Constant('Purple40')]
// [END_FILE_Color.kt]
// [ENTITY: Constant('PurpleGrey40')]
val PurpleGrey40 = Color(0xFF625b71)
// [END_ENTITY: Constant('PurpleGrey40')]
// [ENTITY: Constant('Pink40')]
val Pink40 = Color(0xFF7D5260)
// [END_ENTITY: Constant('Pink40')]
// [END_CONTRACT]
// [END_FILE_Color.kt]

View File

@@ -1,8 +1,10 @@
// [PACKAGE] com.homebox.lens.ui.theme
// [FILE] Theme.kt
// [SEMANTICS] ui, theme, color_scheme
package com.homebox.lens.ui.theme
// [IMPORTS]
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
@@ -17,21 +19,48 @@ 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]
// [CONTRACT]
// [ENTITY: Constant('DarkColorScheme')]
// [RELATION: Constant('DarkColorScheme') -> [CALLS] -> Function('darkColorScheme')]
// [RELATION: Constant('DarkColorScheme') -> [DEPENDS_ON] -> Constant('Purple80')]
// [RELATION: Constant('DarkColorScheme') -> [DEPENDS_ON] -> Constant('PurpleGrey80')]
// [RELATION: Constant('DarkColorScheme') -> [DEPENDS_ON] -> Constant('Pink80')]
private val DarkColorScheme =
darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
)
// [END_ENTITY: Constant('DarkColorScheme')]
// [ENTITY: Constant('LightColorScheme')]
// [RELATION: Constant('LightColorScheme') -> [CALLS] -> Function('lightColorScheme')]
// [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('Purple40')]
// [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('PurpleGrey40')]
// [RELATION: Constant('LightColorScheme') -> [DEPENDS_ON] -> Constant('Pink40')]
private val LightColorScheme =
lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
)
// [END_ENTITY: Constant('LightColorScheme')]
// [ENTITY: Function('HomeboxLensTheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('isSystemInDarkTheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('LocalContext.current')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('dynamicDarkColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('dynamicLightColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('LocalView.current')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('SideEffect')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('toArgb')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('WindowCompat.getInsetsController')]
// [RELATION: Function('HomeboxLensTheme') -> [CALLS] -> Function('MaterialTheme')]
// [RELATION: Function('HomeboxLensTheme') -> [DEPENDS_ON] -> Constant('DarkColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [DEPENDS_ON] -> Constant('LightColorScheme')]
// [RELATION: Function('HomeboxLensTheme') -> [DEPENDS_ON] -> Constant('Typography')]
@Composable
fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
@@ -64,4 +93,6 @@ fun HomeboxLensTheme(
content = content,
)
}
// [END_FILE_Theme.kt]
// [END_ENTITY: Function('HomeboxLensTheme')]
// [END_CONTRACT]
// [END_FILE_Theme.kt]

View File

@@ -1,15 +1,26 @@
// [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]
// Set of Material typography styles to start with
// [CONTRACT]
// [ENTITY: Constant('Typography')]
// [RELATION: Constant('Typography') -> [CALLS] -> Function('Typography')]
// [RELATION: Constant('Typography') -> [CALLS] -> Function('TextStyle')]
// [RELATION: Constant('Typography') -> [DEPENDS_ON] -> Class('FontFamily')]
// [RELATION: Constant('Typography') -> [DEPENDS_ON] -> Class('FontWeight')]
/**
* Set of Material typography styles to start with
*/
val Typography =
Typography(
bodyLarge =
@@ -21,5 +32,6 @@ val Typography =
letterSpacing = 0.5.sp,
),
)
// [END_FILE_Typography.kt]
// [END_ENTITY: Constant('Typography')]
// [END_CONTRACT]
// [END_FILE_Typography.kt]

View File

@@ -3,6 +3,8 @@
<!-- Common -->
<string name="create">Create</string>
<string name="edit">Edit</string>
<string name="delete">Delete</string>
<string name="search">Search</string>
<string name="logout">Logout</string>
<string name="no_location">No location</string>
@@ -14,7 +16,36 @@
<string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Add new location</string>
<string name="cd_add_new_label">Add new label</string>
<string name="content_desc_add_label">Add new label</string>
<string name="content_desc_sync_inventory">Sync inventory</string>
<string name="content_desc_edit_item">Edit item</string>
<string name="content_desc_delete_item">Delete item</string>
<string name="content_desc_save_item">Save item</string>
<string name="content_desc_create_label">Create new label</string>
<string name="content_desc_label_icon">Label icon</string>
<string name="cd_more_options">More options</string>
<!-- Inventory List Screen -->
<string name="inventory_list_title">Inventory</string>
<!-- Item Details Screen -->
<string name="item_details_title">Details</string>
<string name="section_title_description">Description</string>
<string name="placeholder_no_description">No description</string>
<string name="section_title_details">Details</string>
<string name="label_quantity">Quantity</string>
<string name="label_location">Location</string>
<string name="section_title_labels">Labels</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Create item</string>
<string name="item_edit_title">Edit item</string>
<string name="label_name">Name</string>
<string name="label_description">Description</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Search items...</string>
<string name="search_title">Search</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
@@ -34,11 +65,32 @@
<string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string>
<!-- Screen Titles -->
<string name="labels_list_title">Labels</string>
<string name="locations_list_title">Locations</string>
<!-- Location Edit Screen -->
<string name="location_edit_title_create">Create location</string>
<string name="location_edit_title_edit">Edit location</string>
<!-- Locations List Screen -->
<string name="locations_not_found">Locations not found. Press + to add a new one.</string>
<string name="item_count">Items: %1$d</string>
<!-- Setup Screen -->
<string name="screen_title_setup">Setup</string>
<string name="setup_title">Server Setup</string>
<string name="setup_server_url_label">Server URL</string>
<string name="setup_username_label">Username</string>
<string name="setup_password_label">Password</string>
<string name="setup_connect_button">Connect</string>
</resources>
<!-- Labels List Screen -->
<string name="screen_title_labels">Labels</string>
<string name="no_labels_found">No labels found.</string>
<string name="dialog_title_create_label">Create label</string>
<string name="dialog_field_label_name">Label name</string>
<string name="dialog_button_create">Create</string>
<string name="dialog_button_cancel">Cancel</string>
</resources>

View File

@@ -16,7 +16,29 @@
<string name="cd_scan_qr_code">Сканировать QR-код</string>
<string name="cd_navigate_back">Вернуться назад</string>
<string name="cd_add_new_location">Добавить новую локацию</string>
<string name="cd_add_new_label">Добавить новую метку</string>
<string name="content_desc_add_label">Добавить новую метку</string>
<!-- Inventory List Screen -->
<string name="content_desc_sync_inventory">Синхронизировать инвентарь</string>
<!-- Item Details Screen -->
<string name="content_desc_edit_item">Редактировать элемент</string>
<string name="content_desc_delete_item">Удалить элемент</string>
<string name="section_title_description">Описание</string>
<string name="placeholder_no_description">Нет описания</string>
<string name="section_title_details">Детали</string>
<string name="label_quantity">Количество</string>
<string name="label_location">Местоположение</string>
<string name="section_title_labels">Метки</string>
<!-- Item Edit Screen -->
<string name="item_edit_title_create">Создать элемент</string>
<string name="content_desc_save_item">Сохранить элемент</string>
<string name="label_name">Название</string>
<string name="label_description">Описание</string>
<!-- Search Screen -->
<string name="placeholder_search_items">Поиск элементов...</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string>
@@ -54,6 +76,7 @@
<string name="cd_more_options">Больше опций</string>
<!-- Setup Screen -->
<string name="screen_title_setup">Настройка</string>
<string name="setup_title">Настройка сервера</string>
<string name="setup_server_url_label">URL сервера</string>
<string name="setup_username_label">Имя пользователя</string>
@@ -62,15 +85,13 @@
<!-- Labels List Screen -->
<string name="screen_title_labels">Метки</string>
<string name="content_desc_navigate_back">Вернуться назад</string>
<string name="content_desc_navigate_back" translatable="false">Вернуться назад</string>
<string name="content_desc_create_label">Создать новую метку</string>
<string name="content_desc_label_icon">Иконка метки</string>
<string name="labels_list_empty">Метки еще не созданы.</string>
<string name="no_labels_found">Метки не найдены.</string>
<string name="dialog_title_create_label">Создать метку</string>
<string name="dialog_field_label_name">Название метки</string>
<string name="dialog_button_create">Создать</string>
<string name="dialog_button_cancel">Отмена</string>
</resources>
</resources>