feat: Add semantic enrichment to all Kotlin files
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens
|
||||
// [FILE] MainActivity.kt
|
||||
|
||||
// [SEMANTICS] ui, activity, entrypoint
|
||||
package com.homebox.lens
|
||||
|
||||
// [IMPORTS]
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
@@ -16,20 +17,23 @@ 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
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Activity('MainActivity')]
|
||||
/**
|
||||
* [ENTITY: Activity('MainActivity')]
|
||||
* [PURPOSE] Главная и единственная Activity в приложении.
|
||||
* @summary Главная и единственная Activity в приложении.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
// [LIFECYCLE]
|
||||
// [ENTITY: Function('onCreate')]
|
||||
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')]
|
||||
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')]
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
|
||||
setContent {
|
||||
HomeboxLensTheme {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
@@ -39,9 +43,11 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onCreate')]
|
||||
}
|
||||
// [END_ENTITY: Activity('MainActivity')]
|
||||
|
||||
// [HELPER]
|
||||
// [ENTITY: Function('Greeting')]
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
@@ -49,8 +55,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('Greeting')]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('GreetingPreview')]
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
@@ -58,5 +65,6 @@ fun GreetingPreview() {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('GreetingPreview')]
|
||||
|
||||
// [END_FILE_MainActivity.kt]
|
||||
// [END_FILE_MainActivity.kt]
|
||||
@@ -1,28 +1,30 @@
|
||||
// [PACKAGE] com.homebox.lens
|
||||
// [FILE] MainApplication.kt
|
||||
|
||||
// [SEMANTICS] application, hilt, timber
|
||||
package com.homebox.lens
|
||||
|
||||
// [IMPORTS]
|
||||
import android.app.Application
|
||||
import com.homebox.lens.BuildConfig
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CONTRACT]
|
||||
// [ENTITY: Application('MainApplication')]
|
||||
/**
|
||||
* [ENTITY: Application('MainApplication')]
|
||||
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||
* @summary Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class MainApplication : Application() {
|
||||
// [LIFECYCLE]
|
||||
|
||||
// [ENTITY: Function('onCreate')]
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// [ACTION] Initialize Timber for logging
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
Timber.d("[DEBUG][INITIALIZATION][timber_planted] Timber DebugTree planted.")
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onCreate')]
|
||||
}
|
||||
|
||||
// [END_FILE_MainApplication.kt]
|
||||
// [END_ENTITY: Application('MainApplication')]
|
||||
// [END_FILE_MainApplication.kt]
|
||||
@@ -22,11 +22,13 @@ import com.homebox.lens.ui.screen.locationedit.LocationEditScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: Function('NavGraph')]
|
||||
// [RELATION: Function('NavGraph')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
||||
// [RELATION: Function('NavGraph')] -> [CREATES_INSTANCE_OF] -> [Class('NavigationActions')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||
* @param navController Контроллер навигации.
|
||||
* @see Screen
|
||||
* @sideeffect Регистрирует все экраны и управляет состоянием навигации.
|
||||
@@ -36,21 +38,17 @@ import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
fun NavGraph(
|
||||
navController: NavHostController = rememberNavController()
|
||||
) {
|
||||
// [STATE]
|
||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||
val currentRoute = navBackStackEntry?.destination?.route
|
||||
|
||||
// [HELPER]
|
||||
val navigationActions = remember(navController) {
|
||||
NavigationActions(navController)
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Setup.route
|
||||
) {
|
||||
// [COMPOSABLE_SETUP]
|
||||
composable(route = Screen.Setup.route) {
|
||||
SetupScreen(onSetupComplete = {
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
@@ -58,45 +56,39 @@ fun NavGraph(
|
||||
}
|
||||
})
|
||||
}
|
||||
// [COMPOSABLE_DASHBOARD]
|
||||
composable(route = Screen.Dashboard.route) {
|
||||
DashboardScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_INVENTORY_LIST]
|
||||
composable(route = Screen.InventoryList.route) {
|
||||
InventoryListScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_ITEM_DETAILS]
|
||||
composable(route = Screen.ItemDetails.route) {
|
||||
ItemDetailsScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_ITEM_EDIT]
|
||||
composable(route = Screen.ItemEdit.route) {
|
||||
ItemEditScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_LABELS_LIST]
|
||||
composable(Screen.LabelsList.route) {
|
||||
LabelsListScreen(navController = navController)
|
||||
}
|
||||
// [COMPOSABLE_LOCATIONS_LIST]
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
LocationsListScreen(
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions,
|
||||
onLocationClick = { locationId ->
|
||||
// TODO: Navigate to a pre-filtered inventory list screen
|
||||
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen
|
||||
navController.navigate(Screen.InventoryList.route)
|
||||
},
|
||||
onAddNewLocationClick = {
|
||||
@@ -104,14 +96,12 @@ fun NavGraph(
|
||||
}
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_LOCATION_EDIT]
|
||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
||||
LocationEditScreen(
|
||||
locationId = locationId
|
||||
)
|
||||
}
|
||||
// [COMPOSABLE_SEARCH]
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen(
|
||||
currentRoute = currentRoute,
|
||||
@@ -119,6 +109,6 @@ fun NavGraph(
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION_NavGraph]
|
||||
}
|
||||
// [END_FILE_NavGraph.kt]
|
||||
// [END_ENTITY: Function('NavGraph')]
|
||||
// [END_FILE_NavGraph.kt]
|
||||
@@ -2,70 +2,100 @@
|
||||
// [FILE] NavigationActions.kt
|
||||
// [SEMANTICS] navigation, controller, actions
|
||||
package com.homebox.lens.navigation
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.navigation.NavHostController
|
||||
// [CORE-LOGIC]
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Class('NavigationActions')]
|
||||
// [RELATION: Class('NavigationActions')] -> [DEPENDS_ON] -> [Framework('NavHostController')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
|
||||
@param navController Контроллер Jetpack Navigation.
|
||||
@invariant Все навигационные действия должны использовать предоставленный navController.
|
||||
* @summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
|
||||
* @param navController Контроллер Jetpack Navigation.
|
||||
* @invariant Все навигационные действия должны использовать предоставленный navController.
|
||||
*/
|
||||
class NavigationActions(private val navController: NavHostController) {
|
||||
// [ACTION]
|
||||
|
||||
// [ENTITY: Function('navigateToDashboard')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Навигация на главный экран.
|
||||
@sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
||||
* @summary Навигация на главный экран.
|
||||
* @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
||||
*/
|
||||
fun navigateToDashboard() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.")
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
// Используем popUpTo для удаления всех экранов до dashboard из back stack
|
||||
// Это предотвращает создание большой стопки экранов при навигации через drawer
|
||||
popUpTo(navController.graph.startDestinationId)
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToDashboard')]
|
||||
|
||||
// [ENTITY: Function('navigateToLocations')]
|
||||
fun navigateToLocations() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_locations] Navigating to Locations.")
|
||||
navController.navigate(Screen.LocationsList.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToLocations')]
|
||||
|
||||
// [ENTITY: Function('navigateToLabels')]
|
||||
fun navigateToLabels() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_labels] Navigating to Labels.")
|
||||
navController.navigate(Screen.LabelsList.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToLabels')]
|
||||
|
||||
// [ENTITY: Function('navigateToSearch')]
|
||||
fun navigateToSearch() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.")
|
||||
navController.navigate(Screen.Search.route) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToSearch')]
|
||||
|
||||
// [ENTITY: Function('navigateToInventoryListWithLabel')]
|
||||
fun navigateToInventoryListWithLabel(labelId: String) {
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Navigating to Inventory with label: %s", labelId)
|
||||
val route = Screen.InventoryList.withFilter("label", labelId)
|
||||
navController.navigate(route)
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToInventoryListWithLabel')]
|
||||
|
||||
// [ENTITY: Function('navigateToInventoryListWithLocation')]
|
||||
fun navigateToInventoryListWithLocation(locationId: String) {
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Navigating to Inventory with location: %s", locationId)
|
||||
val route = Screen.InventoryList.withFilter("location", locationId)
|
||||
navController.navigate(route)
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToInventoryListWithLocation')]
|
||||
|
||||
// [ENTITY: Function('navigateToCreateItem')]
|
||||
fun navigateToCreateItem() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.")
|
||||
navController.navigate(Screen.ItemEdit.createRoute("new"))
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToCreateItem')]
|
||||
|
||||
// [ENTITY: Function('navigateToLogout')]
|
||||
fun navigateToLogout() {
|
||||
Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.")
|
||||
navController.navigate(Screen.Setup.route) {
|
||||
popUpTo(Screen.Dashboard.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
// [ACTION]
|
||||
// [END_ENTITY: Function('navigateToLogout')]
|
||||
|
||||
// [ENTITY: Function('navigateBack')]
|
||||
fun navigateBack() {
|
||||
Timber.i("[INFO][ACTION][navigate_back] Navigating back.")
|
||||
navController.popBackStack()
|
||||
}
|
||||
// [END_ENTITY: Function('navigateBack')]
|
||||
}
|
||||
// [END_FILE_NavigationActions.kt]
|
||||
// [END_ENTITY: Class('NavigationActions')]
|
||||
// [END_FILE_NavigationActions.kt]
|
||||
|
||||
@@ -3,99 +3,110 @@
|
||||
// [SEMANTICS] navigation, routes, sealed_class
|
||||
package com.homebox.lens.navigation
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: SealedClass('Screen')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Запечатанный класс для определения маршрутов навигации в приложении.
|
||||
* Обеспечивает типобезопасность при навигации.
|
||||
* @property route Строковый идентификатор маршрута.
|
||||
* @summary Запечатанный класс для определения маршрутов навигации в приложении.
|
||||
* @description Обеспечивает типобезопасность при навигации.
|
||||
* @param route Строковый идентификатор маршрута.
|
||||
*/
|
||||
sealed class Screen(val route: String) {
|
||||
// [STATE]
|
||||
// [ENTITY: Object('Setup')]
|
||||
data object Setup : Screen("setup_screen")
|
||||
// [END_ENTITY: Object('Setup')]
|
||||
|
||||
// [ENTITY: Object('Dashboard')]
|
||||
data object Dashboard : Screen("dashboard_screen")
|
||||
// [END_ENTITY: Object('Dashboard')]
|
||||
|
||||
// [ENTITY: Object('InventoryList')]
|
||||
data object InventoryList : Screen("inventory_list_screen") {
|
||||
// [ENTITY: Function('withFilter')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Создает маршрут для экрана списка инвентаря с параметром фильтра.
|
||||
* @summary Создает маршрут для экрана списка инвентаря с параметром фильтра.
|
||||
* @param key Ключ фильтра (например, "label" или "location").
|
||||
* @param value Значение фильтра (например, ID метки или местоположения).
|
||||
* @return Строку полного маршрута с query-параметром.
|
||||
* @throws IllegalArgumentException если ключ или значение пустые.
|
||||
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
|
||||
*/
|
||||
// [HELPER]
|
||||
fun withFilter(key: String, value: String): String {
|
||||
// [PRECONDITION]
|
||||
require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." }
|
||||
require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." }
|
||||
// [ACTION]
|
||||
require(key.isNotBlank()) { "Filter key cannot be blank." }
|
||||
require(value.isNotBlank()) { "Filter value cannot be blank." }
|
||||
val constructedRoute = "inventory_list_screen?$key=$value"
|
||||
// [POSTCONDITION]
|
||||
check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." }
|
||||
check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." }
|
||||
return constructedRoute
|
||||
}
|
||||
// [END_ENTITY: Function('withFilter')]
|
||||
}
|
||||
// [END_ENTITY: Object('InventoryList')]
|
||||
|
||||
// [ENTITY: Object('ItemDetails')]
|
||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||
// [ENTITY: Function('createRoute')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Создает маршрут для экрана деталей элемента с указанным ID.
|
||||
* @summary Создает маршрут для экрана деталей элемента с указанным ID.
|
||||
* @param itemId ID элемента для отображения.
|
||||
* @return Строку полного маршрута.
|
||||
* @throws IllegalArgumentException если itemId пустой.
|
||||
*/
|
||||
// [HELPER]
|
||||
fun createRoute(itemId: String): String {
|
||||
// [PRECONDITION]
|
||||
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
|
||||
// [ACTION]
|
||||
require(itemId.isNotBlank()) { "itemId не может быть пустым." }
|
||||
val route = "item_details_screen/$itemId"
|
||||
// [POSTCONDITION]
|
||||
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
|
||||
check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." }
|
||||
return route
|
||||
}
|
||||
// [END_ENTITY: Function('createRoute')]
|
||||
}
|
||||
// [END_ENTITY: Object('ItemDetails')]
|
||||
|
||||
// [ENTITY: Object('ItemEdit')]
|
||||
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
||||
// [ENTITY: Function('createRoute')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Создает маршрут для экрана редактирования элемента с указанным ID.
|
||||
* @summary Создает маршрут для экрана редактирования элемента с указанным ID.
|
||||
* @param itemId ID элемента для редактирования.
|
||||
* @return Строку полного маршрута.
|
||||
* @throws IllegalArgumentException если itemId пустой.
|
||||
*/
|
||||
// [HELPER]
|
||||
fun createRoute(itemId: String): String {
|
||||
// [PRECONDITION]
|
||||
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
|
||||
// [ACTION]
|
||||
require(itemId.isNotBlank()) { "itemId не может быть пустым." }
|
||||
val route = "item_edit_screen/$itemId"
|
||||
// [POSTCONDITION]
|
||||
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
|
||||
check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." }
|
||||
return route
|
||||
}
|
||||
// [END_ENTITY: Function('createRoute')]
|
||||
}
|
||||
// [END_ENTITY: Object('ItemEdit')]
|
||||
|
||||
// [ENTITY: Object('LabelsList')]
|
||||
data object LabelsList : Screen("labels_list_screen")
|
||||
// [END_ENTITY: Object('LabelsList')]
|
||||
|
||||
// [ENTITY: Object('LocationsList')]
|
||||
data object LocationsList : Screen("locations_list_screen")
|
||||
// [END_ENTITY: Object('LocationsList')]
|
||||
|
||||
// [ENTITY: Object('LocationEdit')]
|
||||
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
|
||||
// [ENTITY: Function('createRoute')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Создает маршрут для экрана редактирования местоположения с указанным ID.
|
||||
* @summary Создает маршрут для экрана редактирования местоположения с указанным ID.
|
||||
* @param locationId ID местоположения для редактирования.
|
||||
* @return Строку полного маршрута.
|
||||
* @throws IllegalArgumentException если locationId пустой.
|
||||
*/
|
||||
// [HELPER]
|
||||
fun createRoute(locationId: String): String {
|
||||
// [PRECONDITION]
|
||||
require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." }
|
||||
// [ACTION]
|
||||
require(locationId.isNotBlank()) { "locationId не может быть пустым." }
|
||||
val route = "location_edit_screen/$locationId"
|
||||
// [POSTCONDITION]
|
||||
check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." }
|
||||
check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." }
|
||||
return route
|
||||
}
|
||||
// [END_ENTITY: Function('createRoute')]
|
||||
}
|
||||
// [END_ENTITY: Object('LocationEdit')]
|
||||
|
||||
// [ENTITY: Object('Search')]
|
||||
data object Search : Screen("search_screen")
|
||||
// [END_ENTITY: Object('Search')]
|
||||
}
|
||||
// [END_FILE_Screen.kt]
|
||||
// [END_ENTITY: SealedClass('Screen')]
|
||||
// [END_FILE_Screen.kt]
|
||||
|
||||
@@ -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,12 +25,15 @@ import androidx.compose.ui.unit.dp
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.navigation.Screen
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: Function('AppDrawerContent')]
|
||||
// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Контент для бокового навигационного меню (Drawer).
|
||||
@param currentRoute Текущий маршрут для подсветки активного элемента.
|
||||
@param navigationActions Объект с навигационными действиями.
|
||||
@param onCloseDrawer Лямбда для закрытия бокового меню.
|
||||
* @summary Контент для бокового навигационного меню (Drawer).
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
* @param onCloseDrawer Лямбда для закрытия бокового меню.
|
||||
*/
|
||||
@Composable
|
||||
internal fun AppDrawerContent(
|
||||
@@ -84,7 +90,7 @@ internal fun AppDrawerContent(
|
||||
onCloseDrawer()
|
||||
}
|
||||
)
|
||||
// TODO: Add Profile and Tools items
|
||||
// [AI_NOTE]: Add Profile and Tools items
|
||||
Divider()
|
||||
NavigationDrawerItem(
|
||||
label = { Text(stringResource(id = R.string.logout)) },
|
||||
@@ -95,4 +101,6 @@ internal fun AppDrawerContent(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('AppDrawerContent')]
|
||||
// [END_FILE_AppDrawer.kt]
|
||||
|
||||
@@ -15,10 +15,12 @@ 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]
|
||||
// [ENTITY: Function('MainScaffold')]
|
||||
// [RELATION: Function('MainScaffold')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('MainScaffold')] -> [CALLS] -> [Function('AppDrawerContent')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
|
||||
* @param topBarTitle Заголовок для TopAppBar.
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
@@ -37,11 +39,9 @@ fun MainScaffold(
|
||||
topBarActions: @Composable () -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
// [STATE]
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// [CORE-LOGIC]
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
@@ -68,10 +68,9 @@ fun MainScaffold(
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
// [ACTION]
|
||||
content(paddingValues)
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION_MainScaffold]
|
||||
}
|
||||
// [END_FILE_MainScaffold.kt]
|
||||
// [END_ENTITY: Function('MainScaffold')]
|
||||
// [END_FILE_MainScaffold.kt]
|
||||
@@ -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,14 +30,18 @@ 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]
|
||||
|
||||
// [ENTITY: Function('DashboardScreen')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Главная Composable-функция для экрана "Панель управления".
|
||||
@param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
@param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
@param navigationActions Объект с навигационными действиями.
|
||||
@sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
* @summary Главная Composable-функция для экрана "Панель управления".
|
||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||
*/
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
@@ -44,9 +49,7 @@ fun DashboardScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
// [STATE]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.dashboard_title),
|
||||
currentRoute = currentRoute,
|
||||
@@ -55,7 +58,7 @@ fun DashboardScreen(
|
||||
IconButton(onClick = { navigationActions.navigateToSearch() }) {
|
||||
Icon(
|
||||
Icons.Default.Search,
|
||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code) // TODO: Rename string resource
|
||||
contentDescription = stringResource(id = R.string.cd_scan_qr_code) // [AI_NOTE]: Rename string resource
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -64,25 +67,26 @@ fun DashboardScreen(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
uiState = uiState,
|
||||
onLocationClick = { location ->
|
||||
Timber.i("[ACTION] Location chip clicked: ${location.id}. Navigating...")
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Location chip clicked: ${location.id}. Navigating...")
|
||||
navigationActions.navigateToInventoryListWithLocation(location.id)
|
||||
},
|
||||
onLabelClick = { label ->
|
||||
Timber.i("[ACTION] Label chip clicked: ${label.id}. Navigating...")
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Label chip clicked: ${label.id}. Navigating...")
|
||||
navigationActions.navigateToInventoryListWithLabel(label.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION_DashboardScreen]
|
||||
}
|
||||
// [HELPER]
|
||||
// [END_ENTITY: Function('DashboardScreen')]
|
||||
|
||||
// [ENTITY: Function('DashboardContent')]
|
||||
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Отображает основной контент экрана в зависимости от uiState.
|
||||
@param modifier Модификатор для стилизации.
|
||||
@param uiState Текущее состояние UI экрана.
|
||||
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
@param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
* @summary Отображает основной контент экрана в зависимости от uiState.
|
||||
* @param modifier Модификатор для стилизации.
|
||||
* @param uiState Текущее состояние UI экрана.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
@Composable
|
||||
private fun DashboardContent(
|
||||
@@ -91,7 +95,6 @@ private fun DashboardContent(
|
||||
onLocationClick: (LocationOutCount) -> Unit,
|
||||
onLabelClick: (LabelOut) -> Unit
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
when (uiState) {
|
||||
is DashboardUiState.Loading -> {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
@@ -123,13 +126,14 @@ private fun DashboardContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION_DashboardContent]
|
||||
}
|
||||
// [UI_COMPONENT]
|
||||
// [END_ENTITY: Function('DashboardContent')]
|
||||
|
||||
// [ENTITY: Function('StatisticsSection')]
|
||||
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения общей статистики.
|
||||
@param statistics Объект со статистическими данными.
|
||||
* @summary Секция для отображения общей статистики.
|
||||
* @param statistics Объект со статистическими данными.
|
||||
*/
|
||||
@Composable
|
||||
private fun StatisticsSection(statistics: GroupStatistics) {
|
||||
@@ -156,12 +160,13 @@ private fun StatisticsSection(statistics: GroupStatistics) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// [UI_COMPONENT]
|
||||
// [END_ENTITY: Function('StatisticsSection')]
|
||||
|
||||
// [ENTITY: Function('StatisticCard')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Карточка для отображения одного статистического показателя.
|
||||
@param title Название показателя.
|
||||
@param value Значение показателя.
|
||||
* @summary Карточка для отображения одного статистического показателя.
|
||||
* @param title Название показателя.
|
||||
* @param value Значение показателя.
|
||||
*/
|
||||
@Composable
|
||||
private fun StatisticCard(title: String, value: String) {
|
||||
@@ -170,11 +175,13 @@ private fun StatisticCard(title: String, value: String) {
|
||||
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
// [UI_COMPONENT]
|
||||
// [END_ENTITY: Function('StatisticCard')]
|
||||
|
||||
// [ENTITY: Function('RecentlyAddedSection')]
|
||||
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения недавно добавленных элементов.
|
||||
@param items Список элементов для отображения.
|
||||
* @summary Секция для отображения недавно добавленных элементов.
|
||||
* @param items Список элементов для отображения.
|
||||
*/
|
||||
@Composable
|
||||
private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
||||
@@ -201,17 +208,19 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// [UI_COMPONENT]
|
||||
// [END_ENTITY: Function('RecentlyAddedSection')]
|
||||
|
||||
// [ENTITY: Function('ItemCard')]
|
||||
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Карточка для отображения краткой информации об элементе.
|
||||
@param item Элемент для отображения.
|
||||
* @summary Карточка для отображения краткой информации об элементе.
|
||||
* @param item Элемент для отображения.
|
||||
*/
|
||||
@Composable
|
||||
private fun ItemCard(item: ItemSummary) {
|
||||
Card(modifier = Modifier.width(150.dp)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
// TODO: Add image here from item.image
|
||||
// [AI_NOTE]: Add image here from item.image
|
||||
Spacer(modifier = Modifier
|
||||
.height(80.dp)
|
||||
.fillMaxWidth()
|
||||
@@ -222,12 +231,14 @@ private fun ItemCard(item: ItemSummary) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// [UI_COMPONENT]
|
||||
// [END_ENTITY: Function('ItemCard')]
|
||||
|
||||
// [ENTITY: Function('LocationsSection')]
|
||||
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения местоположений в виде чипсов.
|
||||
@param locations Список местоположений.
|
||||
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
* @summary Секция для отображения местоположений в виде чипсов.
|
||||
* @param locations Список местоположений.
|
||||
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@@ -249,12 +260,14 @@ private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick:
|
||||
}
|
||||
}
|
||||
}
|
||||
// [UI_COMPONENT]
|
||||
// [END_ENTITY: Function('LocationsSection')]
|
||||
|
||||
// [ENTITY: Function('LabelsSection')]
|
||||
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Секция для отображения меток в виде чипсов.
|
||||
@param labels Список меток.
|
||||
@param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
* @summary Секция для отображения меток в виде чипсов.
|
||||
* @param labels Список меток.
|
||||
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||
*/
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@@ -276,7 +289,9 @@ private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Un
|
||||
}
|
||||
}
|
||||
}
|
||||
// [PREVIEW]
|
||||
// [END_ENTITY: Function('LabelsSection')]
|
||||
|
||||
// [ENTITY: Function('DashboardContentSuccessPreview')]
|
||||
@Preview(showBackground = true, name = "Dashboard Success State")
|
||||
@Composable
|
||||
fun DashboardContentSuccessPreview() {
|
||||
@@ -310,7 +325,9 @@ fun DashboardContentSuccessPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [PREVIEW]
|
||||
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
|
||||
|
||||
// [ENTITY: Function('DashboardContentLoadingPreview')]
|
||||
@Preview(showBackground = true, name = "Dashboard Loading State")
|
||||
@Composable
|
||||
fun DashboardContentLoadingPreview() {
|
||||
@@ -322,7 +339,9 @@ fun DashboardContentLoadingPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [PREVIEW]
|
||||
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
|
||||
|
||||
// [ENTITY: Function('DashboardContentErrorPreview')]
|
||||
@Preview(showBackground = true, name = "Dashboard Error State")
|
||||
@Composable
|
||||
fun DashboardContentErrorPreview() {
|
||||
@@ -334,4 +353,5 @@ fun DashboardContentErrorPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_FILE_DashboardScreen.kt]
|
||||
// [END_ENTITY: Function('DashboardContentErrorPreview')]
|
||||
// [END_FILE_DashboardScreen.kt]
|
||||
|
||||
@@ -1,48 +1,55 @@
|
||||
// [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.ItemSummary
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Определяет все возможные состояния для экрана "Дэшборд".
|
||||
* @summary Определяет все возможные состояния для экрана "Дэшборд".
|
||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||
*/
|
||||
sealed interface DashboardUiState {
|
||||
// [ENTITY: DataClass('Success')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние успешной загрузки данных.
|
||||
* @property statistics Статистика по инвентарю.
|
||||
* @property locations Список локаций со счетчиками.
|
||||
* @property labels Список всех меток.
|
||||
* @property recentlyAddedItems Список недавно добавленных товаров.
|
||||
* @summary Состояние успешной загрузки данных.
|
||||
* @param statistics Статистика по инвентарю.
|
||||
* @param locations Список локаций со счетчиками.
|
||||
* @param labels Список всех меток.
|
||||
* @param recentlyAddedItems Список недавно добавленных товаров.
|
||||
*/
|
||||
data class Success(
|
||||
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 Человекочитаемое сообщение об ошибке.
|
||||
* @summary Состояние ошибки во время загрузки данных.
|
||||
* @param message Человекочитаемое сообщение об ошибке.
|
||||
*/
|
||||
data class Error(val message: String) : DashboardUiState
|
||||
// [END_ENTITY: DataClass('Error')]
|
||||
|
||||
// [ENTITY: Object('Loading')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Состояние, когда данные для экрана загружаются.
|
||||
* @summary Состояние, когда данные для экрана загружаются.
|
||||
*/
|
||||
data object Loading : DashboardUiState
|
||||
// [END_ENTITY: Object('Loading')]
|
||||
}
|
||||
// [END_FILE_DashboardUiState.kt]
|
||||
// [END_ENTITY: SealedInterface('DashboardUiState')]
|
||||
// [END_FILE_DashboardUiState.kt]
|
||||
|
||||
@@ -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
|
||||
@@ -9,19 +10,20 @@ import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
||||
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
|
||||
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для главного экрана (Dashboard).
|
||||
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
||||
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||
@@ -35,30 +37,24 @@ class DashboardViewModel @Inject constructor(
|
||||
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
||||
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
|
||||
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
|
||||
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
loadDashboardData()
|
||||
}
|
||||
|
||||
// [ENTITY: Function('loadDashboardData')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
||||
*/
|
||||
fun loadDashboardData() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
_uiState.value = DashboardUiState.Loading
|
||||
Timber.i("[ACTION] Starting dashboard data collection.")
|
||||
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.")
|
||||
|
||||
val statsFlow = flow { emit(getStatisticsUseCase()) }
|
||||
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
|
||||
@@ -73,16 +69,17 @@ class DashboardViewModel @Inject constructor(
|
||||
recentlyAddedItems = recentItems
|
||||
)
|
||||
}.catch { exception ->
|
||||
Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
|
||||
_uiState.value = DashboardUiState.Error(
|
||||
message = exception.message ?: "Could not load dashboard data."
|
||||
)
|
||||
}.collect { successState ->
|
||||
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
|
||||
Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
|
||||
_uiState.value = successState
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_CLASS_DashboardViewModel]
|
||||
// [END_ENTITY: Function('loadDashboardData')]
|
||||
}
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
// [END_ENTITY: ViewModel('DashboardViewModel')]
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
|
||||
@@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTRYPOINT]
|
||||
// [ENTITY: Function('InventoryListScreen')]
|
||||
// [RELATION: Function('InventoryListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('InventoryListScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Список инвентаря".
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
@@ -24,14 +26,14 @@ fun InventoryListScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.inventory_list_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
Text(text = "TODO: Inventory List Screen")
|
||||
// [AI_NOTE]: Implement Inventory List Screen UI
|
||||
Text(text = "Inventory List Screen")
|
||||
}
|
||||
// [END_FUNCTION_InventoryListScreen]
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('InventoryListScreen')]
|
||||
// [END_FILE_InventoryListScreen.kt]
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
|
||||
// [FILE] InventoryListViewModel.kt
|
||||
|
||||
// [SEMANTICS] ui, viewmodel, inventory_list
|
||||
package com.homebox.lens.ui.screen.inventorylist
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('InventoryListViewModel')]
|
||||
/**
|
||||
* @summary ViewModel for the inventory list screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class InventoryListViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
// [AI_NOTE]: Implement UI state
|
||||
}
|
||||
// [END_FILE_InventoryListViewModel.kt]
|
||||
// [END_ENTITY: ViewModel('InventoryListViewModel')]
|
||||
// [END_FILE_InventoryListViewModel.kt]
|
||||
@@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTRYPOINT]
|
||||
// [ENTITY: Function('ItemDetailsScreen')]
|
||||
// [RELATION: Function('ItemDetailsScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('ItemDetailsScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Детали элемента".
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
@@ -24,14 +26,14 @@ fun ItemDetailsScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.item_details_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
Text(text = "TODO: Item Details Screen")
|
||||
// [AI_NOTE]: Implement Item Details Screen UI
|
||||
Text(text = "Item Details Screen")
|
||||
}
|
||||
// [END_FUNCTION_ItemDetailsScreen]
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('ItemDetailsScreen')]
|
||||
// [END_FILE_ItemDetailsScreen.kt]
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
|
||||
// [FILE] ItemDetailsViewModel.kt
|
||||
|
||||
// [SEMANTICS] ui, viewmodel, item_details
|
||||
package com.homebox.lens.ui.screen.itemdetails
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('ItemDetailsViewModel')]
|
||||
/**
|
||||
* @summary ViewModel for the item details screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
// [AI_NOTE]: Implement UI state
|
||||
}
|
||||
// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
|
||||
// [END_FILE_ItemDetailsViewModel.kt]
|
||||
|
||||
@@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTRYPOINT]
|
||||
// [ENTITY: Function('ItemEditScreen')]
|
||||
// [RELATION: Function('ItemEditScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('ItemEditScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Редактирование элемента".
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
@@ -24,14 +26,14 @@ fun ItemEditScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.item_edit_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
Text(text = "TODO: Item Edit Screen")
|
||||
// [AI_NOTE]: Implement Item Edit Screen UI
|
||||
Text(text = "Item Edit Screen")
|
||||
}
|
||||
// [END_FUNCTION_ItemEditScreen]
|
||||
}
|
||||
// [END_ENTITY: Function('ItemEditScreen')]
|
||||
// [END_FILE_ItemEditScreen.kt]
|
||||
@@ -1,16 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
||||
// [FILE] ItemEditViewModel.kt
|
||||
|
||||
// [SEMANTICS] ui, viewmodel, item_edit
|
||||
package com.homebox.lens.ui.screen.itemedit
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('ItemEditViewModel')]
|
||||
/**
|
||||
* @summary ViewModel for the item edit screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class ItemEditViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
// [AI_NOTE]: Implement UI state
|
||||
}
|
||||
// [END_FILE_ItemEditViewModel.kt]
|
||||
// [END_ENTITY: ViewModel('ItemEditViewModel')]
|
||||
// [END_FILE_ItemEditViewModel.kt]
|
||||
@@ -45,23 +45,15 @@ import com.homebox.lens.R
|
||||
import com.homebox.lens.domain.model.Label
|
||||
import com.homebox.lens.navigation.Screen
|
||||
import timber.log.Timber
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [SECTION] Main Screen Composable
|
||||
|
||||
// [ENTITY: Function('LabelsListScreen')]
|
||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LabelsListViewModel')]
|
||||
// [RELATION: Function('LabelsListScreen')] -> [DEPENDS_ON] -> [Framework('NavController')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает экран со списком всех меток.
|
||||
* @description Главная Composable-функция для экрана меток. Она использует Scaffold для структуры,
|
||||
* получает состояние от `LabelsListViewModel`, обрабатывает навигацию и делегирует отображение
|
||||
* списка и диалогов вспомогательным Composable-функциям.
|
||||
*
|
||||
* @param navController Контроллер навигации для перемещения между экранами.
|
||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||
*
|
||||
* @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события.
|
||||
* @precondition `viewModel` должен быть доступен через Hilt.
|
||||
* @postcondition Экран исчерпывающе обрабатывает все состояния из `LabelsListUiState` (Loading, Success, Error).
|
||||
* @sideeffect Пользовательские действия (клики) инициируют вызовы ViewModel и навигационные команды через `navController`.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -69,18 +61,15 @@ fun LabelsListScreen(
|
||||
navController: NavController,
|
||||
viewModel: LabelsListViewModel = hiltViewModel()
|
||||
) {
|
||||
// [ENTRYPOINT]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [CORE-LOGIC]
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
|
||||
navigationIcon = {
|
||||
// [ACTION] Handle back navigation
|
||||
IconButton(onClick = {
|
||||
Timber.i("[ACTION] Navigate up initiated.")
|
||||
Timber.i("[INFO][ACTION][navigate_up] Navigate up initiated.")
|
||||
navController.navigateUp()
|
||||
}) {
|
||||
Icon(
|
||||
@@ -92,9 +81,8 @@ fun LabelsListScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
// [ACTION] Handle create new label initiation
|
||||
FloatingActionButton(onClick = {
|
||||
Timber.i("[ACTION] FAB clicked: Initiate create new label flow.")
|
||||
Timber.i("[INFO][ACTION][show_create_dialog] FAB clicked: Initiate create new label flow.")
|
||||
viewModel.onShowCreateDialog()
|
||||
}) {
|
||||
Icon(
|
||||
@@ -122,7 +110,6 @@ fun LabelsListScreen(
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// [CORE-LOGIC] State-driven UI rendering
|
||||
when (currentState) {
|
||||
is LabelsListUiState.Loading -> {
|
||||
CircularProgressIndicator()
|
||||
@@ -137,9 +124,7 @@ fun LabelsListScreen(
|
||||
LabelsList(
|
||||
labels = currentState.labels,
|
||||
onLabelClick = { label ->
|
||||
// [ACTION] Handle label click
|
||||
Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.")
|
||||
// [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр.
|
||||
Timber.i("[INFO][ACTION][navigate_to_inventory] Label clicked: ${label.id}. Navigating to inventory list.")
|
||||
val route = Screen.InventoryList.withFilter("label", label.id)
|
||||
navController.navigate(route)
|
||||
}
|
||||
@@ -149,14 +134,12 @@ fun LabelsListScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [COHERENCE_CHECK_PASSED]
|
||||
}
|
||||
// [END_FUNCTION] LabelsListScreen
|
||||
|
||||
// [SECTION] Helper Composables
|
||||
// [END_ENTITY: Function('LabelsListScreen')]
|
||||
|
||||
// [ENTITY: Function('LabelsList')]
|
||||
// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для отображения списка меток.
|
||||
* @param labels Список объектов `Label` для отображения.
|
||||
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
||||
@@ -168,7 +151,6 @@ private fun LabelsList(
|
||||
onLabelClick: (Label) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
@@ -182,10 +164,11 @@ private fun LabelsList(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION] LabelsList
|
||||
// [END_ENTITY: Function('LabelsList')]
|
||||
|
||||
// [ENTITY: Function('LabelListItem')]
|
||||
// [RELATION: Function('LabelListItem')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для отображения одного элемента в списке меток.
|
||||
* @param label Объект `Label`, который нужно отобразить.
|
||||
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
||||
@@ -195,7 +178,6 @@ private fun LabelListItem(
|
||||
label: Label,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
ListItem(
|
||||
headlineContent = { Text(text = label.name) },
|
||||
leadingContent = {
|
||||
@@ -207,10 +189,10 @@ private fun LabelListItem(
|
||||
modifier = Modifier.clickable(onClick = onClick)
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION] LabelListItem
|
||||
// [END_ENTITY: Function('LabelListItem')]
|
||||
|
||||
// [ENTITY: Function('CreateLabelDialog')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Диалоговое окно для создания новой метки.
|
||||
* @param onConfirm Лямбда-функция, вызываемая при подтверждении создания с именем метки.
|
||||
* @param onDismiss Лямбда-функция, вызываемая при закрытии диалога.
|
||||
@@ -220,11 +202,9 @@ private fun CreateLabelDialog(
|
||||
onConfirm: (String) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
// [STATE]
|
||||
var text by remember { mutableStateOf("") }
|
||||
val isConfirmEnabled = text.isNotBlank()
|
||||
|
||||
// [CORE-LOGIC]
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.dialog_title_create_label)) },
|
||||
@@ -252,6 +232,5 @@ private fun CreateLabelDialog(
|
||||
}
|
||||
)
|
||||
}
|
||||
// [END_FUNCTION] CreateLabelDialog
|
||||
|
||||
// [END_FILE] LabelsListScreen.kt
|
||||
// [END_ENTITY: Function('CreateLabelDialog')]
|
||||
// [END_FILE_LabelsListScreen.kt]
|
||||
@@ -2,35 +2,47 @@
|
||||
// [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]
|
||||
|
||||
// [ENTITY: SealedInterface('LabelsListUiState')]
|
||||
/**
|
||||
[CONTRACT]
|
||||
@summary Определяет все возможные состояния для UI экрана со списком меток.
|
||||
@description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
|
||||
* @summary Определяет все возможные состояния для UI экрана со списком меток.
|
||||
* @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
|
||||
*/
|
||||
sealed interface LabelsListUiState {
|
||||
// [ENTITY: DataClass('Success')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||
/**
|
||||
@summary Состояние успеха, содержит список меток и состояние диалога.
|
||||
@property labels Список меток для отображения.
|
||||
@property isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
|
||||
@invariant labels не может быть null.
|
||||
* @summary Состояние успеха, содержит список меток и состояние диалога.
|
||||
* @param labels Список меток для отображения.
|
||||
* @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
|
||||
* @invariant labels не может быть null.
|
||||
*/
|
||||
data class Success(
|
||||
val labels: List<Label>,
|
||||
val isShowingCreateDialog: Boolean = false
|
||||
) : LabelsListUiState
|
||||
// [END_ENTITY: DataClass('Success')]
|
||||
|
||||
// [ENTITY: DataClass('Error')]
|
||||
/**
|
||||
@summary Состояние ошибки.
|
||||
@property message Текст ошибки для отображения пользователю.
|
||||
@invariant message не может быть пустой.
|
||||
* @summary Состояние ошибки.
|
||||
* @param message Текст ошибки для отображения пользователю.
|
||||
* @invariant message не может быть пустой.
|
||||
*/
|
||||
data class Error(val message: String) : LabelsListUiState
|
||||
// [END_ENTITY: DataClass('Error')]
|
||||
|
||||
// [ENTITY: Object('Loading')]
|
||||
/**
|
||||
@summary Состояние загрузки данных.
|
||||
@description Указывает, что идет процесс загрузки меток.
|
||||
* @summary Состояние загрузки данных.
|
||||
* @description Указывает, что идет процесс загрузки меток.
|
||||
*/
|
||||
data object Loading : LabelsListUiState
|
||||
// [END_ENTITY: Object('Loading')]
|
||||
}
|
||||
// [END_ENTITY: SealedInterface('LabelsListUiState')]
|
||||
// [END_FILE_LabelsListUiState.kt]
|
||||
@@ -15,11 +15,12 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('LabelsListViewModel')]
|
||||
// [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
||||
// [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для экрана со списком меток.
|
||||
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
|
||||
@@ -29,40 +30,32 @@ class LabelsListViewModel @Inject constructor(
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [INIT]
|
||||
init {
|
||||
loadLabels()
|
||||
}
|
||||
|
||||
// [ENTITY: Function('loadLabels')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает список меток.
|
||||
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
||||
*/
|
||||
// [ACTION]
|
||||
fun loadLabels() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
_uiState.value = LabelsListUiState.Loading
|
||||
Timber.i("[ACTION] Starting labels list load. State -> Loading.")
|
||||
Timber.i("[INFO][ENTRYPOINT][loading_labels] Starting labels list load. State -> Loading.")
|
||||
|
||||
// [CORE-LOGIC]
|
||||
val result = runCatching {
|
||||
getAllLabelsUseCase()
|
||||
}
|
||||
|
||||
// [RESULT_HANDLER]
|
||||
result.fold(
|
||||
onSuccess = { labelOuts ->
|
||||
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
|
||||
// [DATA-FLOW] Map List<LabelOut> to List<Label> for the UI state.
|
||||
// The 'Label' model for the UI is simpler and only contains 'id' and 'name'.
|
||||
Timber.i("[INFO][SUCCESS][labels_loaded] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
|
||||
val labels = labelOuts.map { labelOut ->
|
||||
Label(
|
||||
id = labelOut.id,
|
||||
@@ -72,7 +65,7 @@ class LabelsListViewModel @Inject constructor(
|
||||
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.")
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load labels. State -> Error.")
|
||||
_uiState.value = LabelsListUiState.Error(
|
||||
message = exception.message ?: "Could not load labels."
|
||||
)
|
||||
@@ -80,41 +73,42 @@ class LabelsListViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('loadLabels')]
|
||||
|
||||
// [ENTITY: Function('onShowCreateDialog')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Инициирует отображение диалога для создания метки.
|
||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
|
||||
* @sideeffect Обновляет `_uiState`.
|
||||
*/
|
||||
// [ACTION]
|
||||
fun onShowCreateDialog() {
|
||||
Timber.i("[ACTION] Show create label dialog requested.")
|
||||
Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.")
|
||||
if (_uiState.value is LabelsListUiState.Success) {
|
||||
_uiState.update {
|
||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onShowCreateDialog')]
|
||||
|
||||
// [ENTITY: Function('onDismissCreateDialog')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Скрывает диалог создания метки.
|
||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
|
||||
* @sideeffect Обновляет `_uiState`.
|
||||
*/
|
||||
// [ACTION]
|
||||
fun onDismissCreateDialog() {
|
||||
Timber.i("[ACTION] Dismiss create label dialog requested.")
|
||||
Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.")
|
||||
if (_uiState.value is LabelsListUiState.Success) {
|
||||
_uiState.update {
|
||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('onDismissCreateDialog')]
|
||||
|
||||
// [ENTITY: Function('createLabel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
|
||||
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
|
||||
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
|
||||
@@ -122,19 +116,16 @@ class LabelsListViewModel @Inject constructor(
|
||||
* @precondition `name` не должен быть пустым.
|
||||
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
|
||||
*/
|
||||
// [ACTION]
|
||||
fun createLabel(name: String) {
|
||||
// [PRECONDITION]
|
||||
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
|
||||
|
||||
// [ENTRYPOINT]
|
||||
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
|
||||
Timber.i("[INFO][ACTION][create_label] Create label called with name: '$name'. [STUBBED]")
|
||||
|
||||
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
|
||||
// [AI_NOTE]: Здесь будет вызов CreateLabelUseCase.
|
||||
|
||||
// [POSTCONDITION] Скрываем диалог после "создания".
|
||||
onDismissCreateDialog()
|
||||
// [REFACTORING_NOTE] На следующем этапе нужно добавить вызов UseCase и обновить список.
|
||||
}
|
||||
// [END_ENTITY: Function('createLabel')]
|
||||
}
|
||||
// [END_CLASS_LabelsListViewModel]
|
||||
// [END_ENTITY: ViewModel('LabelsListViewModel')]
|
||||
// [END_FILE_LabelsListViewModel.kt]
|
||||
@@ -15,10 +15,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.homebox.lens.R
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTRYPOINT]
|
||||
// [ENTITY: Function('LocationEditScreen')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Редактирование местоположения".
|
||||
* @param locationId ID местоположения для редактирования или "new" для создания.
|
||||
*/
|
||||
@@ -39,7 +39,10 @@ fun LocationEditScreen(
|
||||
.padding(paddingValues),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = "TODO: Location Edit Screen for ID: $locationId")
|
||||
// [AI_NOTE]: Implement Location Edit Screen UI
|
||||
Text(text = "Location Edit Screen for ID: $locationId")
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationEditScreen')]
|
||||
// [END_FILE_LocationEditScreen.kt]
|
||||
|
||||
@@ -49,10 +49,13 @@ 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]
|
||||
// [ENTITY: Function('LocationsListScreen')]
|
||||
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [ViewModel('LocationsListViewModel')]
|
||||
// [RELATION: Function('LocationsListScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('LocationsListScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Список местоположений".
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
@@ -68,10 +71,8 @@ fun LocationsListScreen(
|
||||
onAddNewLocationClick: () -> Unit,
|
||||
viewModel: LocationsListViewModel = hiltViewModel()
|
||||
) {
|
||||
// [STATE]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.locations_list_title),
|
||||
currentRoute = currentRoute,
|
||||
@@ -92,16 +93,17 @@ fun LocationsListScreen(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
uiState = uiState,
|
||||
onLocationClick = onLocationClick,
|
||||
onEditLocation = { /* TODO */ },
|
||||
onDeleteLocation = { /* TODO */ }
|
||||
onEditLocation = { /* [AI_NOTE]: Implement onEditLocation */ },
|
||||
onDeleteLocation = { /* [AI_NOTE]: Implement onDeleteLocation */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsListScreen')]
|
||||
|
||||
// [HELPER]
|
||||
// [ENTITY: Function('LocationsListContent')]
|
||||
// [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает основной контент экрана в зависимости от `uiState`.
|
||||
* @param modifier Модификатор для стилизации.
|
||||
* @param uiState Текущее состояние UI.
|
||||
@@ -160,10 +162,11 @@ private fun LocationsListContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsListContent')]
|
||||
|
||||
// [UI_COMPONENT]
|
||||
// [ENTITY: Function('LocationCard')]
|
||||
// [RELATION: Function('LocationCard')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Карточка для отображения одного местоположения.
|
||||
* @param location Данные о местоположении.
|
||||
* @param onClick Лямбда-обработчик нажатия на карточку.
|
||||
@@ -224,8 +227,9 @@ private fun LocationCard(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationCard')]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('LocationsListSuccessPreview')]
|
||||
@Preview(showBackground = true, name = "Locations List Success")
|
||||
@Composable
|
||||
fun LocationsListSuccessPreview() {
|
||||
@@ -243,8 +247,9 @@ fun LocationsListSuccessPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsListSuccessPreview')]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('LocationsListEmptyPreview')]
|
||||
@Preview(showBackground = true, name = "Locations List Empty")
|
||||
@Composable
|
||||
fun LocationsListEmptyPreview() {
|
||||
@@ -257,8 +262,9 @@ fun LocationsListEmptyPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsListEmptyPreview')]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('LocationsListLoadingPreview')]
|
||||
@Preview(showBackground = true, name = "Locations List Loading")
|
||||
@Composable
|
||||
fun LocationsListLoadingPreview() {
|
||||
@@ -271,8 +277,9 @@ fun LocationsListLoadingPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsListLoadingPreview')]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('LocationsListErrorPreview')]
|
||||
@Preview(showBackground = true, name = "Locations List Error")
|
||||
@Composable
|
||||
fun LocationsListErrorPreview() {
|
||||
@@ -285,3 +292,5 @@ fun LocationsListErrorPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('LocationsListErrorPreview')]
|
||||
// [END_FILE_LocationsListScreen.kt]
|
||||
|
||||
@@ -4,32 +4,39 @@
|
||||
|
||||
package com.homebox.lens.ui.screen.locationslist
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTITY: SealedInterface('LocationsListUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Определяет возможные состояния UI для экрана списка местоположений.
|
||||
* @see LocationsListViewModel
|
||||
*/
|
||||
sealed interface LocationsListUiState {
|
||||
// [ENTITY: DataClass('Success')]
|
||||
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('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: Object('Loading')]
|
||||
/**
|
||||
* [STATE]
|
||||
* @summary Состояние загрузки данных.
|
||||
*/
|
||||
object Loading : LocationsListUiState
|
||||
// [END_ENTITY: Object('Loading')]
|
||||
}
|
||||
// [END_ENTITY: SealedInterface('LocationsListUiState')]
|
||||
// [END_FILE_LocationsListUiState.kt]
|
||||
|
||||
@@ -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
|
||||
@@ -12,11 +13,14 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [ENTITY: ViewModel('LocationsListViewModel')]
|
||||
// [RELATION: ViewModel('LocationsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
|
||||
// [RELATION: ViewModel('LocationsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LocationsListUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для экрана списка местоположений.
|
||||
* @param getAllLocationsUseCase Use case для получения всех местоположений.
|
||||
* @property uiState Поток, содержащий текущее состояние UI.
|
||||
@@ -27,32 +31,34 @@ class LocationsListViewModel @Inject constructor(
|
||||
private val getAllLocationsUseCase: GetAllLocationsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
||||
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
|
||||
|
||||
// [INITIALIZER]
|
||||
init {
|
||||
loadLocations()
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
// [ENTITY: Function('loadLocations')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает список местоположений из репозитория.
|
||||
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
|
||||
*/
|
||||
fun loadLocations() {
|
||||
Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.")
|
||||
viewModelScope.launch {
|
||||
_uiState.value = LocationsListUiState.Loading
|
||||
try {
|
||||
Timber.d("[DEBUG][ACTION][fetching_locations] Fetching locations from use case.")
|
||||
val locations = getAllLocationsUseCase()
|
||||
_uiState.value = LocationsListUiState.Success(locations)
|
||||
Timber.d("[DEBUG][SUCCESS][locations_loaded] Successfully loaded locations.")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "[ERROR][EXCEPTION][loading_failed] Failed to load locations.")
|
||||
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_CLASS_LocationsListViewModel]
|
||||
// [END_ENTITY: Function('loadLocations')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('LocationsListViewModel')]
|
||||
// [END_FILE_LocationsListViewModel.kt]
|
||||
@@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource
|
||||
import com.homebox.lens.R
|
||||
import com.homebox.lens.navigation.NavigationActions
|
||||
import com.homebox.lens.ui.common.MainScaffold
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTRYPOINT]
|
||||
// [ENTITY: Function('SearchScreen')]
|
||||
// [RELATION: Function('SearchScreen')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||
// [RELATION: Function('SearchScreen')] -> [CALLS] -> [Function('MainScaffold')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Composable-функция для экрана "Поиск".
|
||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||
* @param navigationActions Объект с навигационными действиями.
|
||||
@@ -24,14 +26,14 @@ fun SearchScreen(
|
||||
currentRoute: String?,
|
||||
navigationActions: NavigationActions
|
||||
) {
|
||||
// [UI_COMPONENT]
|
||||
MainScaffold(
|
||||
topBarTitle = stringResource(id = R.string.search_title),
|
||||
currentRoute = currentRoute,
|
||||
navigationActions = navigationActions
|
||||
) {
|
||||
// [CORE-LOGIC]
|
||||
Text(text = "TODO: Search Screen")
|
||||
// [AI_NOTE]: Implement Search Screen UI
|
||||
Text(text = "Search Screen")
|
||||
}
|
||||
// [END_FUNCTION_SearchScreen]
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('SearchScreen')]
|
||||
// [END_FILE_SearchScreen.kt]
|
||||
@@ -1,16 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.search
|
||||
// [FILE] SearchViewModel.kt
|
||||
|
||||
// [SEMANTICS] ui, viewmodel, search
|
||||
package com.homebox.lens.ui.screen.search
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('SearchViewModel')]
|
||||
/**
|
||||
* @summary ViewModel for the search screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SearchViewModel @Inject constructor() : ViewModel() {
|
||||
// [STATE]
|
||||
// TODO: Implement UI state
|
||||
// [AI_NOTE]: Implement UI state
|
||||
}
|
||||
// [END_ENTITY: ViewModel('SearchViewModel')]
|
||||
// [END_FILE_SearchViewModel.kt]
|
||||
|
||||
@@ -20,10 +20,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.homebox.lens.R
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [ENTRYPOINT]
|
||||
// [ENTITY: Function('SetupScreen')]
|
||||
// [RELATION: Function('SetupScreen')] -> [DEPENDS_ON] -> [ViewModel('SetupViewModel')]
|
||||
// [RELATION: Function('SetupScreen')] -> [CALLS] -> [Function('SetupScreenContent')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Главная Composable-функция для экрана настройки соединения с сервером.
|
||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
|
||||
@@ -34,15 +36,12 @@ fun SetupScreen(
|
||||
viewModel: SetupViewModel = hiltViewModel(),
|
||||
onSetupComplete: () -> Unit
|
||||
) {
|
||||
// [STATE]
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
// [CORE-LOGIC]
|
||||
if (uiState.isSetupComplete) {
|
||||
onSetupComplete()
|
||||
}
|
||||
|
||||
// [UI_COMPONENT]
|
||||
SetupScreenContent(
|
||||
uiState = uiState,
|
||||
onServerUrlChange = viewModel::onServerUrlChange,
|
||||
@@ -50,12 +49,12 @@ fun SetupScreen(
|
||||
onPasswordChange = viewModel::onPasswordChange,
|
||||
onConnectClick = viewModel::connect
|
||||
)
|
||||
// [END_FUNCTION_SetupScreen]
|
||||
}
|
||||
// [END_ENTITY: Function('SetupScreen')]
|
||||
|
||||
// [HELPER]
|
||||
// [ENTITY: Function('SetupScreenContent')]
|
||||
// [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Отображает контент экрана настройки: поля ввода и кнопку.
|
||||
* @param uiState Текущее состояние UI.
|
||||
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
|
||||
@@ -123,10 +122,10 @@ private fun SetupScreenContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FUNCTION_SetupScreenContent]
|
||||
}
|
||||
// [END_ENTITY: Function('SetupScreenContent')]
|
||||
|
||||
// [PREVIEW]
|
||||
// [ENTITY: Function('SetupScreenPreview')]
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SetupScreenPreview() {
|
||||
@@ -138,4 +137,5 @@ fun SetupScreenPreview() {
|
||||
onConnectClick = {}
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('SetupScreenPreview')]
|
||||
// [END_FILE_SetupScreen.kt]
|
||||
|
||||
@@ -4,17 +4,16 @@
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
// [ENTITY: DataClass('SetupUiState')]
|
||||
/**
|
||||
* [ENTITY: DataClass('SetupUiState')]
|
||||
* [CONTRACT]
|
||||
* Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
||||
* Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
||||
* @property serverUrl URL-адрес сервера Homebox.
|
||||
* @property username Имя пользователя для входа.
|
||||
* @property password Пароль пользователя.
|
||||
* @property isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
||||
* @property error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
||||
* @property isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
|
||||
* @summary Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
||||
* @description Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
||||
* @param serverUrl URL-адрес сервера Homebox.
|
||||
* @param username Имя пользователя для входа.
|
||||
* @param password Пароль пользователя.
|
||||
* @param isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
||||
* @param error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
||||
* @param isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
|
||||
*/
|
||||
data class SetupUiState(
|
||||
val serverUrl: String = "",
|
||||
@@ -24,4 +23,5 @@ data class SetupUiState(
|
||||
val error: String? = null,
|
||||
val isSetupComplete: Boolean = false
|
||||
)
|
||||
// [END_ENTITY: DataClass('SetupUiState')]
|
||||
// [END_FILE_SetupUiState.kt]
|
||||
@@ -2,31 +2,30 @@
|
||||
// [FILE] SetupViewModel.kt
|
||||
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.usecase.LoginUseCase
|
||||
import com.homebox.lens.ui.screen.setup.SetupUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
// [END_IMPORTS]
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('SetupViewModel')]
|
||||
// [RELATION: ViewModel('SetupViewModel')] -> [DEPENDS_ON] -> [Repository('CredentialsRepository')]
|
||||
// [RELATION: ViewModel('SetupViewModel')] -> [DEPENDS_ON] -> [UseCase('LoginUseCase')]
|
||||
// [RELATION: ViewModel('SetupViewModel')] -> [EMITS_STATE] -> [DataClass('SetupUiState')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* ViewModel для экрана первоначальной настройки (Setup).
|
||||
* Отвечает за:
|
||||
* 1. Загрузку и сохранение учетных данных (URL сервера, логин, пароль).
|
||||
* 2. Управление состоянием UI экрана (`SetupUiState`).
|
||||
* 3. Инициацию процесса входа в систему через `LoginUseCase`.
|
||||
* @property credentialsRepository Репозиторий для операций с учетными данными.
|
||||
* @property loginUseCase Use case для выполнения логики входа.
|
||||
* @summary ViewModel для экрана первоначальной настройки (Setup).
|
||||
* @param credentialsRepository Репозиторий для операций с учетными данными.
|
||||
* @param loginUseCase Use case для выполнения логики входа.
|
||||
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
|
||||
*/
|
||||
@HiltViewModel
|
||||
@@ -35,28 +34,20 @@ class SetupViewModel @Inject constructor(
|
||||
private val loginUseCase: LoginUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow(SetupUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
// [ACTION] Загружаем учетные данные при создании ViewModel.
|
||||
loadCredentials()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [HELPER] Загружает учетные данные из репозитория при инициализации.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
|
||||
*/
|
||||
// [ENTITY: Function('loadCredentials')]
|
||||
private fun loadCredentials() {
|
||||
// [ENTRYPOINT]
|
||||
Timber.d("[DEBUG][ENTRYPOINT][loading_credentials] Loading credentials from repository.")
|
||||
viewModelScope.launch {
|
||||
// [CORE-LOGIC] Подписываемся на поток учетных данных.
|
||||
credentialsRepository.getCredentials().collect { credentials ->
|
||||
// [ACTION] Обновляем состояние, если учетные данные существуют.
|
||||
if (credentials != null) {
|
||||
Timber.d("[DEBUG][ACTION][updating_state] Credentials found, updating UI state.")
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
serverUrl = credentials.serverUrl,
|
||||
@@ -68,76 +59,55 @@ class SetupViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_ENTITY: Function('loadCredentials')]
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newUrl Новое значение URL.
|
||||
* @sideeffect Обновляет поле `serverUrl` в `_uiState`.
|
||||
*/
|
||||
// [ENTITY: Function('onServerUrlChange')]
|
||||
fun onServerUrlChange(newUrl: String) {
|
||||
_uiState.update { it.copy(serverUrl = newUrl) }
|
||||
}
|
||||
// [END_ENTITY: Function('onServerUrlChange')]
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newUsername Новое значение имени пользователя.
|
||||
* @sideeffect Обновляет поле `username` в `_uiState`.
|
||||
*/
|
||||
// [ENTITY: Function('onUsernameChange')]
|
||||
fun onUsernameChange(newUsername: String) {
|
||||
_uiState.update { it.copy(username = newUsername) }
|
||||
}
|
||||
// [END_ENTITY: Function('onUsernameChange')]
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newPassword Новое значение пароля.
|
||||
* @sideeffect Обновляет поле `password` в `_uiState`.
|
||||
*/
|
||||
// [ENTITY: Function('onPasswordChange')]
|
||||
fun onPasswordChange(newPassword: String) {
|
||||
_uiState.update { it.copy(password = newPassword) }
|
||||
}
|
||||
// [END_ENTITY: Function('onPasswordChange')]
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
|
||||
* Выполняет две основные операции:
|
||||
* 1. Сохраняет введенные учетные данные для последующих сессий.
|
||||
* 2. Выполняет вход в систему с использованием этих данных.
|
||||
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
|
||||
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
|
||||
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
|
||||
*/
|
||||
// [ENTITY: Function('connect')]
|
||||
fun connect() {
|
||||
// [ENTRYPOINT]
|
||||
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
||||
viewModelScope.launch {
|
||||
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
|
||||
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
|
||||
val credentials = Credentials(
|
||||
serverUrl = _uiState.value.serverUrl.trim(),
|
||||
username = _uiState.value.username.trim(),
|
||||
password = _uiState.value.password
|
||||
)
|
||||
|
||||
// [ACTION] Сохраняем учетные данные для будущего использования.
|
||||
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
||||
credentialsRepository.saveCredentials(credentials)
|
||||
|
||||
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
|
||||
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
||||
loginUseCase(credentials).fold(
|
||||
onSuccess = {
|
||||
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
|
||||
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
|
||||
Timber.e(exception, "[ERROR][EXCEPTION][login_failed] Login failed.")
|
||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_SetupViewModel]
|
||||
// [END_ENTITY: Function('connect')]
|
||||
}
|
||||
// [END_ENTITY: ViewModel('SetupViewModel')]
|
||||
// [END_FILE_SetupViewModel.kt]
|
||||
@@ -1,9 +1,11 @@
|
||||
// [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]
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.theme
|
||||
// [FILE] Theme.kt
|
||||
|
||||
// [SEMANTICS] ui, theme
|
||||
package com.homebox.lens.ui.theme
|
||||
|
||||
// [IMPORTS]
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
@@ -17,6 +18,7 @@ import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
// [END_IMPORTS]
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
@@ -30,10 +32,17 @@ private val LightColorScheme = lightColorScheme(
|
||||
tertiary = Pink40
|
||||
)
|
||||
|
||||
// [ENTITY: Function('HomeboxLensTheme')]
|
||||
// [RELATION: Function('HomeboxLensTheme')] -> [DEPENDS_ON] -> [DataStructure('Typography')]
|
||||
/**
|
||||
* @summary The main theme for the Homebox Lens application.
|
||||
* @param darkTheme Whether the theme should be dark or light.
|
||||
* @param dynamicColor Whether to use dynamic color (on Android 12+).
|
||||
* @param content The content to be displayed within the theme.
|
||||
*/
|
||||
@Composable
|
||||
fun HomeboxLensTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
@@ -61,4 +70,5 @@ fun HomeboxLensTheme(
|
||||
content = content
|
||||
)
|
||||
}
|
||||
// [END_ENTITY: Function('HomeboxLensTheme')]
|
||||
// [END_FILE_Theme.kt]
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
// [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
|
||||
// [ENTITY: DataStructure('Typography')]
|
||||
/**
|
||||
* @summary Defines the typography for the application.
|
||||
*/
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
@@ -19,5 +24,6 @@ val Typography = Typography(
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
)
|
||||
// [END_ENTITY: DataStructure('Typography')]
|
||||
|
||||
// [END_FILE_Typography.kt]
|
||||
|
||||
Reference in New Issue
Block a user