feat: Add semantic enrichment to all Kotlin files
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
// [PACKAGE] com.homebox.lens
|
// [PACKAGE] com.homebox.lens
|
||||||
// [FILE] MainActivity.kt
|
// [FILE] MainActivity.kt
|
||||||
|
// [SEMANTICS] ui, activity, entrypoint
|
||||||
package com.homebox.lens
|
package com.homebox.lens
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
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.navigation.NavGraph
|
||||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import timber.log.Timber
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Activity('MainActivity')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Activity('MainActivity')]
|
* @summary Главная и единственная Activity в приложении.
|
||||||
* [PURPOSE] Главная и единственная Activity в приложении.
|
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
// [LIFECYCLE]
|
// [ENTITY: Function('onCreate')]
|
||||||
|
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('HomeboxLensTheme')]
|
||||||
|
// [RELATION: Function('onCreate')] -> [CALLS] -> [Function('NavGraph')]
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
Timber.d("[DEBUG][LIFECYCLE][creating_activity] MainActivity created.")
|
||||||
setContent {
|
setContent {
|
||||||
HomeboxLensTheme {
|
HomeboxLensTheme {
|
||||||
// A surface container using the 'background' color from the theme
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.background
|
color = MaterialTheme.colorScheme.background
|
||||||
@@ -39,9 +43,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onCreate')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Activity('MainActivity')]
|
||||||
|
|
||||||
// [HELPER]
|
// [ENTITY: Function('Greeting')]
|
||||||
@Composable
|
@Composable
|
||||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||||
Text(
|
Text(
|
||||||
@@ -49,8 +55,9 @@ fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('Greeting')]
|
||||||
|
|
||||||
// [PREVIEW]
|
// [ENTITY: Function('GreetingPreview')]
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun GreetingPreview() {
|
fun GreetingPreview() {
|
||||||
@@ -58,5 +65,6 @@ fun GreetingPreview() {
|
|||||||
Greeting("Android")
|
Greeting("Android")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('GreetingPreview')]
|
||||||
|
|
||||||
// [END_FILE_MainActivity.kt]
|
// [END_FILE_MainActivity.kt]
|
||||||
@@ -1,28 +1,30 @@
|
|||||||
// [PACKAGE] com.homebox.lens
|
// [PACKAGE] com.homebox.lens
|
||||||
// [FILE] MainApplication.kt
|
// [FILE] MainApplication.kt
|
||||||
|
// [SEMANTICS] application, hilt, timber
|
||||||
package com.homebox.lens
|
package com.homebox.lens
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.homebox.lens.BuildConfig
|
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Application('MainApplication')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Application('MainApplication')]
|
* @summary Точка входа в приложение. Инициализирует Hilt и Timber.
|
||||||
* [PURPOSE] Точка входа в приложение. Инициализирует Hilt и Timber.
|
|
||||||
*/
|
*/
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class MainApplication : Application() {
|
class MainApplication : Application() {
|
||||||
// [LIFECYCLE]
|
|
||||||
|
// [ENTITY: Function('onCreate')]
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
// [ACTION] Initialize Timber for logging
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Timber.plant(Timber.DebugTree())
|
Timber.plant(Timber.DebugTree())
|
||||||
|
Timber.d("[DEBUG][INITIALIZATION][timber_planted] Timber DebugTree planted.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onCreate')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Application('MainApplication')]
|
||||||
// [END_FILE_MainApplication.kt]
|
// [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.locationslist.LocationsListScreen
|
||||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
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]
|
* @summary Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
||||||
* Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
|
|
||||||
* @param navController Контроллер навигации.
|
* @param navController Контроллер навигации.
|
||||||
* @see Screen
|
* @see Screen
|
||||||
* @sideeffect Регистрирует все экраны и управляет состоянием навигации.
|
* @sideeffect Регистрирует все экраны и управляет состоянием навигации.
|
||||||
@@ -36,21 +38,17 @@ import com.homebox.lens.ui.screen.setup.SetupScreen
|
|||||||
fun NavGraph(
|
fun NavGraph(
|
||||||
navController: NavHostController = rememberNavController()
|
navController: NavHostController = rememberNavController()
|
||||||
) {
|
) {
|
||||||
// [STATE]
|
|
||||||
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
||||||
val currentRoute = navBackStackEntry?.destination?.route
|
val currentRoute = navBackStackEntry?.destination?.route
|
||||||
|
|
||||||
// [HELPER]
|
|
||||||
val navigationActions = remember(navController) {
|
val navigationActions = remember(navController) {
|
||||||
NavigationActions(navController)
|
NavigationActions(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
// [ACTION]
|
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Screen.Setup.route
|
startDestination = Screen.Setup.route
|
||||||
) {
|
) {
|
||||||
// [COMPOSABLE_SETUP]
|
|
||||||
composable(route = Screen.Setup.route) {
|
composable(route = Screen.Setup.route) {
|
||||||
SetupScreen(onSetupComplete = {
|
SetupScreen(onSetupComplete = {
|
||||||
navController.navigate(Screen.Dashboard.route) {
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
@@ -58,45 +56,39 @@ fun NavGraph(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_DASHBOARD]
|
|
||||||
composable(route = Screen.Dashboard.route) {
|
composable(route = Screen.Dashboard.route) {
|
||||||
DashboardScreen(
|
DashboardScreen(
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_INVENTORY_LIST]
|
|
||||||
composable(route = Screen.InventoryList.route) {
|
composable(route = Screen.InventoryList.route) {
|
||||||
InventoryListScreen(
|
InventoryListScreen(
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_ITEM_DETAILS]
|
|
||||||
composable(route = Screen.ItemDetails.route) {
|
composable(route = Screen.ItemDetails.route) {
|
||||||
ItemDetailsScreen(
|
ItemDetailsScreen(
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_ITEM_EDIT]
|
|
||||||
composable(route = Screen.ItemEdit.route) {
|
composable(route = Screen.ItemEdit.route) {
|
||||||
ItemEditScreen(
|
ItemEditScreen(
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_LABELS_LIST]
|
|
||||||
composable(Screen.LabelsList.route) {
|
composable(Screen.LabelsList.route) {
|
||||||
LabelsListScreen(navController = navController)
|
LabelsListScreen(navController = navController)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_LOCATIONS_LIST]
|
|
||||||
composable(route = Screen.LocationsList.route) {
|
composable(route = Screen.LocationsList.route) {
|
||||||
LocationsListScreen(
|
LocationsListScreen(
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions,
|
navigationActions = navigationActions,
|
||||||
onLocationClick = { locationId ->
|
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)
|
navController.navigate(Screen.InventoryList.route)
|
||||||
},
|
},
|
||||||
onAddNewLocationClick = {
|
onAddNewLocationClick = {
|
||||||
@@ -104,14 +96,12 @@ fun NavGraph(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_LOCATION_EDIT]
|
|
||||||
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
composable(route = Screen.LocationEdit.route) { backStackEntry ->
|
||||||
val locationId = backStackEntry.arguments?.getString("locationId")
|
val locationId = backStackEntry.arguments?.getString("locationId")
|
||||||
LocationEditScreen(
|
LocationEditScreen(
|
||||||
locationId = locationId
|
locationId = locationId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [COMPOSABLE_SEARCH]
|
|
||||||
composable(route = Screen.Search.route) {
|
composable(route = Screen.Search.route) {
|
||||||
SearchScreen(
|
SearchScreen(
|
||||||
currentRoute = currentRoute,
|
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
|
// [FILE] NavigationActions.kt
|
||||||
// [SEMANTICS] navigation, controller, actions
|
// [SEMANTICS] navigation, controller, actions
|
||||||
package com.homebox.lens.navigation
|
package com.homebox.lens.navigation
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.navigation.NavHostController
|
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 для предоставления типизированных навигационных действий.
|
||||||
@summary Класс-обертка над NavHostController для предоставления типизированных навигационных действий.
|
* @param navController Контроллер Jetpack Navigation.
|
||||||
@param navController Контроллер Jetpack Navigation.
|
* @invariant Все навигационные действия должны использовать предоставленный navController.
|
||||||
@invariant Все навигационные действия должны использовать предоставленный navController.
|
|
||||||
*/
|
*/
|
||||||
class NavigationActions(private val navController: NavHostController) {
|
class NavigationActions(private val navController: NavHostController) {
|
||||||
// [ACTION]
|
|
||||||
|
// [ENTITY: Function('navigateToDashboard')]
|
||||||
/**
|
/**
|
||||||
[CONTRACT]
|
* @summary Навигация на главный экран.
|
||||||
@summary Навигация на главный экран.
|
* @sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
||||||
@sideeffect Очищает back stack до главного экрана, чтобы избежать циклов.
|
|
||||||
*/
|
*/
|
||||||
fun navigateToDashboard() {
|
fun navigateToDashboard() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_to_dashboard] Navigating to Dashboard.")
|
||||||
navController.navigate(Screen.Dashboard.route) {
|
navController.navigate(Screen.Dashboard.route) {
|
||||||
// Используем popUpTo для удаления всех экранов до dashboard из back stack
|
|
||||||
// Это предотвращает создание большой стопки экранов при навигации через drawer
|
|
||||||
popUpTo(navController.graph.startDestinationId)
|
popUpTo(navController.graph.startDestinationId)
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToDashboard')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToLocations')]
|
||||||
fun navigateToLocations() {
|
fun navigateToLocations() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_to_locations] Navigating to Locations.")
|
||||||
navController.navigate(Screen.LocationsList.route) {
|
navController.navigate(Screen.LocationsList.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToLocations')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToLabels')]
|
||||||
fun navigateToLabels() {
|
fun navigateToLabels() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_to_labels] Navigating to Labels.")
|
||||||
navController.navigate(Screen.LabelsList.route) {
|
navController.navigate(Screen.LabelsList.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToLabels')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToSearch')]
|
||||||
fun navigateToSearch() {
|
fun navigateToSearch() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_to_search] Navigating to Search.")
|
||||||
navController.navigate(Screen.Search.route) {
|
navController.navigate(Screen.Search.route) {
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToSearch')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToInventoryListWithLabel')]
|
||||||
fun navigateToInventoryListWithLabel(labelId: String) {
|
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)
|
val route = Screen.InventoryList.withFilter("label", labelId)
|
||||||
navController.navigate(route)
|
navController.navigate(route)
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToInventoryListWithLabel')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToInventoryListWithLocation')]
|
||||||
fun navigateToInventoryListWithLocation(locationId: String) {
|
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)
|
val route = Screen.InventoryList.withFilter("location", locationId)
|
||||||
navController.navigate(route)
|
navController.navigate(route)
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToInventoryListWithLocation')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToCreateItem')]
|
||||||
fun navigateToCreateItem() {
|
fun navigateToCreateItem() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_to_create_item] Navigating to Create Item.")
|
||||||
navController.navigate(Screen.ItemEdit.createRoute("new"))
|
navController.navigate(Screen.ItemEdit.createRoute("new"))
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToCreateItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateToLogout')]
|
||||||
fun navigateToLogout() {
|
fun navigateToLogout() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_to_logout] Navigating to Logout.")
|
||||||
navController.navigate(Screen.Setup.route) {
|
navController.navigate(Screen.Setup.route) {
|
||||||
popUpTo(Screen.Dashboard.route) { inclusive = true }
|
popUpTo(Screen.Dashboard.route) { inclusive = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [ACTION]
|
// [END_ENTITY: Function('navigateToLogout')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('navigateBack')]
|
||||||
fun navigateBack() {
|
fun navigateBack() {
|
||||||
|
Timber.i("[INFO][ACTION][navigate_back] Navigating back.")
|
||||||
navController.popBackStack()
|
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
|
// [SEMANTICS] navigation, routes, sealed_class
|
||||||
package com.homebox.lens.navigation
|
package com.homebox.lens.navigation
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: SealedClass('Screen')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Запечатанный класс для определения маршрутов навигации в приложении.
|
||||||
* Запечатанный класс для определения маршрутов навигации в приложении.
|
* @description Обеспечивает типобезопасность при навигации.
|
||||||
* Обеспечивает типобезопасность при навигации.
|
* @param route Строковый идентификатор маршрута.
|
||||||
* @property route Строковый идентификатор маршрута.
|
|
||||||
*/
|
*/
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
// [STATE]
|
// [ENTITY: Object('Setup')]
|
||||||
data object Setup : Screen("setup_screen")
|
data object Setup : Screen("setup_screen")
|
||||||
|
// [END_ENTITY: Object('Setup')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Dashboard')]
|
||||||
data object Dashboard : Screen("dashboard_screen")
|
data object Dashboard : Screen("dashboard_screen")
|
||||||
|
// [END_ENTITY: Object('Dashboard')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('InventoryList')]
|
||||||
data object InventoryList : Screen("inventory_list_screen") {
|
data object InventoryList : Screen("inventory_list_screen") {
|
||||||
|
// [ENTITY: Function('withFilter')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Создает маршрут для экрана списка инвентаря с параметром фильтра.
|
||||||
* Создает маршрут для экрана списка инвентаря с параметром фильтра.
|
|
||||||
* @param key Ключ фильтра (например, "label" или "location").
|
* @param key Ключ фильтра (например, "label" или "location").
|
||||||
* @param value Значение фильтра (например, ID метки или местоположения).
|
* @param value Значение фильтра (например, ID метки или местоположения).
|
||||||
* @return Строку полного маршрута с query-параметром.
|
* @return Строку полного маршрута с query-параметром.
|
||||||
* @throws IllegalArgumentException если ключ или значение пустые.
|
* @throws IllegalArgumentException если ключ или значение пустые.
|
||||||
* @sideeffect [ARCH-IMPLICATION] NavGraph должен быть настроен для приема этого опционального query-параметра (например, 'navArgument("label") { nullable = true }').
|
|
||||||
*/
|
*/
|
||||||
// [HELPER]
|
|
||||||
fun withFilter(key: String, value: String): String {
|
fun withFilter(key: String, value: String): String {
|
||||||
// [PRECONDITION]
|
require(key.isNotBlank()) { "Filter key cannot be blank." }
|
||||||
require(key.isNotBlank()) { "[PRECONDITION_FAILED] Filter key cannot be blank." }
|
require(value.isNotBlank()) { "Filter value cannot be blank." }
|
||||||
require(value.isNotBlank()) { "[PRECONDITION_FAILED] Filter value cannot be blank." }
|
|
||||||
// [ACTION]
|
|
||||||
val constructedRoute = "inventory_list_screen?$key=$value"
|
val constructedRoute = "inventory_list_screen?$key=$value"
|
||||||
// [POSTCONDITION]
|
check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." }
|
||||||
check(constructedRoute.contains("?$key=$value")) { "[POSTCONDITION_FAILED] Route must contain the filter query." }
|
|
||||||
return constructedRoute
|
return constructedRoute
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('withFilter')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Object('InventoryList')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('ItemDetails')]
|
||||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||||
|
// [ENTITY: Function('createRoute')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Создает маршрут для экрана деталей элемента с указанным ID.
|
||||||
* Создает маршрут для экрана деталей элемента с указанным ID.
|
|
||||||
* @param itemId ID элемента для отображения.
|
* @param itemId ID элемента для отображения.
|
||||||
* @return Строку полного маршрута.
|
* @return Строку полного маршрута.
|
||||||
* @throws IllegalArgumentException если itemId пустой.
|
* @throws IllegalArgumentException если itemId пустой.
|
||||||
*/
|
*/
|
||||||
// [HELPER]
|
|
||||||
fun createRoute(itemId: String): String {
|
fun createRoute(itemId: String): String {
|
||||||
// [PRECONDITION]
|
require(itemId.isNotBlank()) { "itemId не может быть пустым." }
|
||||||
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
|
|
||||||
// [ACTION]
|
|
||||||
val route = "item_details_screen/$itemId"
|
val route = "item_details_screen/$itemId"
|
||||||
// [POSTCONDITION]
|
check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." }
|
||||||
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('createRoute')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Object('ItemDetails')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('ItemEdit')]
|
||||||
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
||||||
|
// [ENTITY: Function('createRoute')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Создает маршрут для экрана редактирования элемента с указанным ID.
|
||||||
* Создает маршрут для экрана редактирования элемента с указанным ID.
|
|
||||||
* @param itemId ID элемента для редактирования.
|
* @param itemId ID элемента для редактирования.
|
||||||
* @return Строку полного маршрута.
|
* @return Строку полного маршрута.
|
||||||
* @throws IllegalArgumentException если itemId пустой.
|
* @throws IllegalArgumentException если itemId пустой.
|
||||||
*/
|
*/
|
||||||
// [HELPER]
|
|
||||||
fun createRoute(itemId: String): String {
|
fun createRoute(itemId: String): String {
|
||||||
// [PRECONDITION]
|
require(itemId.isNotBlank()) { "itemId не может быть пустым." }
|
||||||
require(itemId.isNotBlank()) { "[PRECONDITION_FAILED] itemId не может быть пустым." }
|
|
||||||
// [ACTION]
|
|
||||||
val route = "item_edit_screen/$itemId"
|
val route = "item_edit_screen/$itemId"
|
||||||
// [POSTCONDITION]
|
check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." }
|
||||||
check(route.endsWith(itemId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на itemId." }
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('createRoute')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Object('ItemEdit')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('LabelsList')]
|
||||||
data object LabelsList : Screen("labels_list_screen")
|
data object LabelsList : Screen("labels_list_screen")
|
||||||
|
// [END_ENTITY: Object('LabelsList')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('LocationsList')]
|
||||||
data object LocationsList : Screen("locations_list_screen")
|
data object LocationsList : Screen("locations_list_screen")
|
||||||
|
// [END_ENTITY: Object('LocationsList')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('LocationEdit')]
|
||||||
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
|
data object LocationEdit : Screen("location_edit_screen/{locationId}") {
|
||||||
|
// [ENTITY: Function('createRoute')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Создает маршрут для экрана редактирования местоположения с указанным ID.
|
||||||
* Создает маршрут для экрана редактирования местоположения с указанным ID.
|
|
||||||
* @param locationId ID местоположения для редактирования.
|
* @param locationId ID местоположения для редактирования.
|
||||||
* @return Строку полного маршрута.
|
* @return Строку полного маршрута.
|
||||||
* @throws IllegalArgumentException если locationId пустой.
|
* @throws IllegalArgumentException если locationId пустой.
|
||||||
*/
|
*/
|
||||||
// [HELPER]
|
|
||||||
fun createRoute(locationId: String): String {
|
fun createRoute(locationId: String): String {
|
||||||
// [PRECONDITION]
|
require(locationId.isNotBlank()) { "locationId не может быть пустым." }
|
||||||
require(locationId.isNotBlank()) { "[PRECONDITION_FAILED] locationId не может быть пустым." }
|
|
||||||
// [ACTION]
|
|
||||||
val route = "location_edit_screen/$locationId"
|
val route = "location_edit_screen/$locationId"
|
||||||
// [POSTCONDITION]
|
check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." }
|
||||||
check(route.endsWith(locationId)) { "[POSTCONDITION_FAILED] Маршрут должен заканчиваться на locationId." }
|
|
||||||
return route
|
return route
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('createRoute')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Object('LocationEdit')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Search')]
|
||||||
data object Search : Screen("search_screen")
|
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
|
// [PACKAGE] com.homebox.lens.ui.common
|
||||||
// [FILE] AppDrawer.kt
|
// [FILE] AppDrawer.kt
|
||||||
|
// [SEMANTICS] ui, common, navigation_drawer
|
||||||
package com.homebox.lens.ui.common
|
package com.homebox.lens.ui.common
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
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.R
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import com.homebox.lens.navigation.Screen
|
import com.homebox.lens.navigation.Screen
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('AppDrawerContent')]
|
||||||
|
// [RELATION: Function('AppDrawerContent')] -> [DEPENDS_ON] -> [Class('NavigationActions')]
|
||||||
/**
|
/**
|
||||||
[CONTRACT]
|
* @summary Контент для бокового навигационного меню (Drawer).
|
||||||
@summary Контент для бокового навигационного меню (Drawer).
|
* @param currentRoute Текущий маршрут для подсветки активного элемента.
|
||||||
@param currentRoute Текущий маршрут для подсветки активного элемента.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@param navigationActions Объект с навигационными действиями.
|
* @param onCloseDrawer Лямбда для закрытия бокового меню.
|
||||||
@param onCloseDrawer Лямбда для закрытия бокового меню.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AppDrawerContent(
|
internal fun AppDrawerContent(
|
||||||
@@ -84,7 +90,7 @@ internal fun AppDrawerContent(
|
|||||||
onCloseDrawer()
|
onCloseDrawer()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// TODO: Add Profile and Tools items
|
// [AI_NOTE]: Add Profile and Tools items
|
||||||
Divider()
|
Divider()
|
||||||
NavigationDrawerItem(
|
NavigationDrawerItem(
|
||||||
label = { Text(stringResource(id = R.string.logout)) },
|
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.R
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import kotlinx.coroutines.launch
|
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.
|
* @summary Общая обертка для экранов, включающая Scaffold и Navigation Drawer.
|
||||||
* @param topBarTitle Заголовок для TopAppBar.
|
* @param topBarTitle Заголовок для TopAppBar.
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
@@ -37,11 +39,9 @@ fun MainScaffold(
|
|||||||
topBarActions: @Composable () -> Unit = {},
|
topBarActions: @Composable () -> Unit = {},
|
||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
// [STATE]
|
|
||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
@@ -68,10 +68,9 @@ fun MainScaffold(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
// [ACTION]
|
|
||||||
content(paddingValues)
|
content(paddingValues)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_FUNCTION_MainScaffold]
|
|
||||||
}
|
}
|
||||||
// [END_FILE_MainScaffold.kt]
|
// [END_ENTITY: Function('MainScaffold')]
|
||||||
|
// [END_FILE_MainScaffold.kt]
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// [FILE] DashboardScreen.kt
|
// [FILE] DashboardScreen.kt
|
||||||
// [SEMANTICS] ui, screen, dashboard, compose, navigation
|
// [SEMANTICS] ui, screen, dashboard, compose, navigation
|
||||||
package com.homebox.lens.ui.screen.dashboard
|
package com.homebox.lens.ui.screen.dashboard
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
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.common.MainScaffold
|
||||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
||||||
import timber.log.Timber
|
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-функция для экрана "Панель управления".
|
||||||
@summary Главная Composable-функция для экрана "Панель управления".
|
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||||
@param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
@param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@param navigationActions Объект с навигационными действиями.
|
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
||||||
@sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardScreen(
|
fun DashboardScreen(
|
||||||
@@ -44,9 +49,7 @@ fun DashboardScreen(
|
|||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
navigationActions: NavigationActions
|
navigationActions: NavigationActions
|
||||||
) {
|
) {
|
||||||
// [STATE]
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
// [UI_COMPONENT]
|
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.dashboard_title),
|
topBarTitle = stringResource(id = R.string.dashboard_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
@@ -55,7 +58,7 @@ fun DashboardScreen(
|
|||||||
IconButton(onClick = { navigationActions.navigateToSearch() }) {
|
IconButton(onClick = { navigationActions.navigateToSearch() }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Search,
|
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),
|
modifier = Modifier.padding(paddingValues),
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onLocationClick = { location ->
|
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)
|
navigationActions.navigateToInventoryListWithLocation(location.id)
|
||||||
},
|
},
|
||||||
onLabelClick = { label ->
|
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)
|
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.
|
||||||
@summary Отображает основной контент экрана в зависимости от uiState.
|
* @param modifier Модификатор для стилизации.
|
||||||
@param modifier Модификатор для стилизации.
|
* @param uiState Текущее состояние UI экрана.
|
||||||
@param uiState Текущее состояние UI экрана.
|
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||||
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||||
@param onLabelClick Лямбда-обработчик нажатия на метку.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun DashboardContent(
|
private fun DashboardContent(
|
||||||
@@ -91,7 +95,6 @@ private fun DashboardContent(
|
|||||||
onLocationClick: (LocationOutCount) -> Unit,
|
onLocationClick: (LocationOutCount) -> Unit,
|
||||||
onLabelClick: (LabelOut) -> Unit
|
onLabelClick: (LabelOut) -> Unit
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
|
||||||
when (uiState) {
|
when (uiState) {
|
||||||
is DashboardUiState.Loading -> {
|
is DashboardUiState.Loading -> {
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
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 Секция для отображения общей статистики.
|
||||||
@summary Секция для отображения общей статистики.
|
* @param statistics Объект со статистическими данными.
|
||||||
@param statistics Объект со статистическими данными.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatisticsSection(statistics: GroupStatistics) {
|
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 Карточка для отображения одного статистического показателя.
|
||||||
@summary Карточка для отображения одного статистического показателя.
|
* @param title Название показателя.
|
||||||
@param title Название показателя.
|
* @param value Значение показателя.
|
||||||
@param value Значение показателя.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatisticCard(title: String, value: String) {
|
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)
|
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 Секция для отображения недавно добавленных элементов.
|
||||||
@summary Секция для отображения недавно добавленных элементов.
|
* @param items Список элементов для отображения.
|
||||||
@param items Список элементов для отображения.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun RecentlyAddedSection(items: List<ItemSummary>) {
|
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 Карточка для отображения краткой информации об элементе.
|
||||||
@summary Карточка для отображения краткой информации об элементе.
|
* @param item Элемент для отображения.
|
||||||
@param item Элемент для отображения.
|
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun ItemCard(item: ItemSummary) {
|
private fun ItemCard(item: ItemSummary) {
|
||||||
Card(modifier = Modifier.width(150.dp)) {
|
Card(modifier = Modifier.width(150.dp)) {
|
||||||
Column(modifier = Modifier.padding(8.dp)) {
|
Column(modifier = Modifier.padding(8.dp)) {
|
||||||
// TODO: Add image here from item.image
|
// [AI_NOTE]: Add image here from item.image
|
||||||
Spacer(modifier = Modifier
|
Spacer(modifier = Modifier
|
||||||
.height(80.dp)
|
.height(80.dp)
|
||||||
.fillMaxWidth()
|
.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 Секция для отображения местоположений в виде чипсов.
|
||||||
@summary Секция для отображения местоположений в виде чипсов.
|
* @param locations Список местоположений.
|
||||||
@param locations Список местоположений.
|
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
||||||
@param onLocationClick Лямбда-обработчик нажатия на местоположение.
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@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 Секция для отображения меток в виде чипсов.
|
||||||
@summary Секция для отображения меток в виде чипсов.
|
* @param labels Список меток.
|
||||||
@param labels Список меток.
|
* @param onLabelClick Лямбда-обработчик нажатия на метку.
|
||||||
@param onLabelClick Лямбда-обработчик нажатия на метку.
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@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")
|
@Preview(showBackground = true, name = "Dashboard Success State")
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardContentSuccessPreview() {
|
fun DashboardContentSuccessPreview() {
|
||||||
@@ -310,7 +325,9 @@ fun DashboardContentSuccessPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [PREVIEW]
|
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DashboardContentLoadingPreview')]
|
||||||
@Preview(showBackground = true, name = "Dashboard Loading State")
|
@Preview(showBackground = true, name = "Dashboard Loading State")
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardContentLoadingPreview() {
|
fun DashboardContentLoadingPreview() {
|
||||||
@@ -322,7 +339,9 @@ fun DashboardContentLoadingPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [PREVIEW]
|
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('DashboardContentErrorPreview')]
|
||||||
@Preview(showBackground = true, name = "Dashboard Error State")
|
@Preview(showBackground = true, name = "Dashboard Error State")
|
||||||
@Composable
|
@Composable
|
||||||
fun DashboardContentErrorPreview() {
|
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
|
// [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
|
// [SEMANTICS] ui, state, dashboard
|
||||||
|
|
||||||
// [IMPORTS]
|
|
||||||
package com.homebox.lens.ui.screen.dashboard
|
package com.homebox.lens.ui.screen.dashboard
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.GroupStatistics
|
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.LabelOut
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
import com.homebox.lens.domain.model.LocationOutCount
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
// [ENTITY: SealedInterface('DashboardUiState')]
|
// [ENTITY: SealedInterface('DashboardUiState')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Определяет все возможные состояния для экрана "Дэшборд".
|
||||||
* Определяет все возможные состояния для экрана "Дэшборд".
|
|
||||||
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
|
||||||
*/
|
*/
|
||||||
sealed interface DashboardUiState {
|
sealed interface DashboardUiState {
|
||||||
|
// [ENTITY: DataClass('Success')]
|
||||||
|
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
|
||||||
|
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||||
|
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
|
||||||
|
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Состояние успешной загрузки данных.
|
||||||
* Состояние успешной загрузки данных.
|
* @param statistics Статистика по инвентарю.
|
||||||
* @property statistics Статистика по инвентарю.
|
* @param locations Список локаций со счетчиками.
|
||||||
* @property locations Список локаций со счетчиками.
|
* @param labels Список всех меток.
|
||||||
* @property labels Список всех меток.
|
* @param recentlyAddedItems Список недавно добавленных товаров.
|
||||||
* @property recentlyAddedItems Список недавно добавленных товаров.
|
|
||||||
*/
|
*/
|
||||||
data class Success(
|
data class Success(
|
||||||
val statistics: GroupStatistics,
|
val statistics: GroupStatistics,
|
||||||
val locations: List<LocationOutCount>,
|
val locations: List<LocationOutCount>,
|
||||||
val labels: List<LabelOut>,
|
val labels: List<LabelOut>,
|
||||||
val recentlyAddedItems: List<com.homebox.lens.domain.model.ItemSummary>
|
val recentlyAddedItems: List<ItemSummary>
|
||||||
) : DashboardUiState
|
) : DashboardUiState
|
||||||
|
// [END_ENTITY: DataClass('Success')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('Error')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Состояние ошибки во время загрузки данных.
|
||||||
* Состояние ошибки во время загрузки данных.
|
* @param message Человекочитаемое сообщение об ошибке.
|
||||||
* @property message Человекочитаемое сообщение об ошибке.
|
|
||||||
*/
|
*/
|
||||||
data class Error(val message: String) : DashboardUiState
|
data class Error(val message: String) : DashboardUiState
|
||||||
|
// [END_ENTITY: DataClass('Error')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Loading')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Состояние, когда данные для экрана загружаются.
|
||||||
* Состояние, когда данные для экрана загружаются.
|
|
||||||
*/
|
*/
|
||||||
data object Loading : DashboardUiState
|
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
|
// [FILE] DashboardViewModel.kt
|
||||||
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
|
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
|
||||||
package com.homebox.lens.ui.screen.dashboard
|
package com.homebox.lens.ui.screen.dashboard
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.GetAllLocationsUseCase
|
||||||
import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
|
import com.homebox.lens.domain.usecase.GetRecentlyAddedItemsUseCase
|
||||||
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
||||||
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
|
||||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
// [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).
|
* @summary ViewModel для главного экрана (Dashboard).
|
||||||
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
||||||
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||||
@@ -35,30 +37,24 @@ class DashboardViewModel @Inject constructor(
|
|||||||
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
|
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// [STATE]
|
|
||||||
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
||||||
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
|
|
||||||
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
|
|
||||||
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
|
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
// [LIFECYCLE_HANDLER]
|
|
||||||
init {
|
init {
|
||||||
loadDashboardData()
|
loadDashboardData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [ENTITY: Function('loadDashboardData')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
* @summary Загружает все необходимые данные для экрана Dashboard.
|
||||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
||||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
||||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
||||||
*/
|
*/
|
||||||
fun loadDashboardData() {
|
fun loadDashboardData() {
|
||||||
// [ENTRYPOINT]
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = DashboardUiState.Loading
|
_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 statsFlow = flow { emit(getStatisticsUseCase()) }
|
||||||
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
|
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
|
||||||
@@ -73,16 +69,17 @@ class DashboardViewModel @Inject constructor(
|
|||||||
recentlyAddedItems = recentItems
|
recentlyAddedItems = recentItems
|
||||||
)
|
)
|
||||||
}.catch { exception ->
|
}.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(
|
_uiState.value = DashboardUiState.Error(
|
||||||
message = exception.message ?: "Could not load dashboard data."
|
message = exception.message ?: "Could not load dashboard data."
|
||||||
)
|
)
|
||||||
}.collect { successState ->
|
}.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
|
_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.R
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
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-функция для экрана "Список инвентаря".
|
* @summary Composable-функция для экрана "Список инвентаря".
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@@ -24,14 +26,14 @@ fun InventoryListScreen(
|
|||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
navigationActions: NavigationActions
|
navigationActions: NavigationActions
|
||||||
) {
|
) {
|
||||||
// [UI_COMPONENT]
|
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.inventory_list_title),
|
topBarTitle = stringResource(id = R.string.inventory_list_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
// [AI_NOTE]: Implement Inventory List Screen UI
|
||||||
Text(text = "TODO: Inventory List Screen")
|
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
|
// [PACKAGE] com.homebox.lens.ui.screen.inventorylist
|
||||||
// [FILE] InventoryListViewModel.kt
|
// [FILE] InventoryListViewModel.kt
|
||||||
|
// [SEMANTICS] ui, viewmodel, inventory_list
|
||||||
package com.homebox.lens.ui.screen.inventorylist
|
package com.homebox.lens.ui.screen.inventorylist
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
// [ENTITY: ViewModel('InventoryListViewModel')]
|
||||||
|
/**
|
||||||
|
* @summary ViewModel for the inventory list screen.
|
||||||
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class InventoryListViewModel @Inject constructor() : ViewModel() {
|
class InventoryListViewModel @Inject constructor() : ViewModel() {
|
||||||
// [STATE]
|
// [AI_NOTE]: Implement UI state
|
||||||
// TODO: 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.R
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
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-функция для экрана "Детали элемента".
|
* @summary Composable-функция для экрана "Детали элемента".
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@@ -24,14 +26,14 @@ fun ItemDetailsScreen(
|
|||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
navigationActions: NavigationActions
|
navigationActions: NavigationActions
|
||||||
) {
|
) {
|
||||||
// [UI_COMPONENT]
|
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.item_details_title),
|
topBarTitle = stringResource(id = R.string.item_details_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
// [AI_NOTE]: Implement Item Details Screen UI
|
||||||
Text(text = "TODO: Item Details Screen")
|
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
|
// [PACKAGE] com.homebox.lens.ui.screen.itemdetails
|
||||||
// [FILE] ItemDetailsViewModel.kt
|
// [FILE] ItemDetailsViewModel.kt
|
||||||
|
// [SEMANTICS] ui, viewmodel, item_details
|
||||||
package com.homebox.lens.ui.screen.itemdetails
|
package com.homebox.lens.ui.screen.itemdetails
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
// [ENTITY: ViewModel('ItemDetailsViewModel')]
|
||||||
|
/**
|
||||||
|
* @summary ViewModel for the item details screen.
|
||||||
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
|
class ItemDetailsViewModel @Inject constructor() : ViewModel() {
|
||||||
// [STATE]
|
// [AI_NOTE]: Implement UI state
|
||||||
// TODO: Implement UI state
|
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: ViewModel('ItemDetailsViewModel')]
|
||||||
// [END_FILE_ItemDetailsViewModel.kt]
|
// [END_FILE_ItemDetailsViewModel.kt]
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import com.homebox.lens.R
|
import com.homebox.lens.R
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
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-функция для экрана "Редактирование элемента".
|
* @summary Composable-функция для экрана "Редактирование элемента".
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@@ -24,14 +26,14 @@ fun ItemEditScreen(
|
|||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
navigationActions: NavigationActions
|
navigationActions: NavigationActions
|
||||||
) {
|
) {
|
||||||
// [UI_COMPONENT]
|
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.item_edit_title),
|
topBarTitle = stringResource(id = R.string.item_edit_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
// [AI_NOTE]: Implement Item Edit Screen UI
|
||||||
Text(text = "TODO: Item Edit Screen")
|
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
|
// [PACKAGE] com.homebox.lens.ui.screen.itemedit
|
||||||
// [FILE] ItemEditViewModel.kt
|
// [FILE] ItemEditViewModel.kt
|
||||||
|
// [SEMANTICS] ui, viewmodel, item_edit
|
||||||
package com.homebox.lens.ui.screen.itemedit
|
package com.homebox.lens.ui.screen.itemedit
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
// [ENTITY: ViewModel('ItemEditViewModel')]
|
||||||
|
/**
|
||||||
|
* @summary ViewModel for the item edit screen.
|
||||||
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ItemEditViewModel @Inject constructor() : ViewModel() {
|
class ItemEditViewModel @Inject constructor() : ViewModel() {
|
||||||
// [STATE]
|
// [AI_NOTE]: Implement UI state
|
||||||
// TODO: 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.domain.model.Label
|
||||||
import com.homebox.lens.navigation.Screen
|
import com.homebox.lens.navigation.Screen
|
||||||
import timber.log.Timber
|
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 Отображает экран со списком всех меток.
|
* @summary Отображает экран со списком всех меток.
|
||||||
* @description Главная Composable-функция для экрана меток. Она использует Scaffold для структуры,
|
|
||||||
* получает состояние от `LabelsListViewModel`, обрабатывает навигацию и делегирует отображение
|
|
||||||
* списка и диалогов вспомогательным Composable-функциям.
|
|
||||||
*
|
|
||||||
* @param navController Контроллер навигации для перемещения между экранами.
|
* @param navController Контроллер навигации для перемещения между экранами.
|
||||||
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
* @param viewModel ViewModel, предоставляющая состояние UI для экрана меток.
|
||||||
*
|
|
||||||
* @precondition `navController` должен быть корректно инициализирован и способен обрабатывать навигационные события.
|
|
||||||
* @precondition `viewModel` должен быть доступен через Hilt.
|
|
||||||
* @postcondition Экран исчерпывающе обрабатывает все состояния из `LabelsListUiState` (Loading, Success, Error).
|
|
||||||
* @sideeffect Пользовательские действия (клики) инициируют вызовы ViewModel и навигационные команды через `navController`.
|
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -69,18 +61,15 @@ fun LabelsListScreen(
|
|||||||
navController: NavController,
|
navController: NavController,
|
||||||
viewModel: LabelsListViewModel = hiltViewModel()
|
viewModel: LabelsListViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// [ENTRYPOINT]
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
|
title = { Text(text = stringResource(id = R.string.screen_title_labels)) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
// [ACTION] Handle back navigation
|
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
Timber.i("[ACTION] Navigate up initiated.")
|
Timber.i("[INFO][ACTION][navigate_up] Navigate up initiated.")
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -92,9 +81,8 @@ fun LabelsListScreen(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
// [ACTION] Handle create new label initiation
|
|
||||||
FloatingActionButton(onClick = {
|
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()
|
viewModel.onShowCreateDialog()
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -122,7 +110,6 @@ fun LabelsListScreen(
|
|||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC] State-driven UI rendering
|
|
||||||
when (currentState) {
|
when (currentState) {
|
||||||
is LabelsListUiState.Loading -> {
|
is LabelsListUiState.Loading -> {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
@@ -137,9 +124,7 @@ fun LabelsListScreen(
|
|||||||
LabelsList(
|
LabelsList(
|
||||||
labels = currentState.labels,
|
labels = currentState.labels,
|
||||||
onLabelClick = { label ->
|
onLabelClick = { label ->
|
||||||
// [ACTION] Handle label click
|
Timber.i("[INFO][ACTION][navigate_to_inventory] Label clicked: ${label.id}. Navigating to inventory list.")
|
||||||
Timber.i("[ACTION] Label clicked: ${label.id}. Navigating to inventory list.")
|
|
||||||
// [DESIGN-DECISION] Использовать существующий экран списка инвентаря, передавая фильтр.
|
|
||||||
val route = Screen.InventoryList.withFilter("label", label.id)
|
val route = Screen.InventoryList.withFilter("label", label.id)
|
||||||
navController.navigate(route)
|
navController.navigate(route)
|
||||||
}
|
}
|
||||||
@@ -149,14 +134,12 @@ fun LabelsListScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [COHERENCE_CHECK_PASSED]
|
|
||||||
}
|
}
|
||||||
// [END_FUNCTION] LabelsListScreen
|
// [END_ENTITY: Function('LabelsListScreen')]
|
||||||
|
|
||||||
// [SECTION] Helper Composables
|
|
||||||
|
|
||||||
|
// [ENTITY: Function('LabelsList')]
|
||||||
|
// [RELATION: Function('LabelsList')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Composable-функция для отображения списка меток.
|
* @summary Composable-функция для отображения списка меток.
|
||||||
* @param labels Список объектов `Label` для отображения.
|
* @param labels Список объектов `Label` для отображения.
|
||||||
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
* @param onLabelClick Лямбда-функция, вызываемая при нажатии на элемент списка.
|
||||||
@@ -168,7 +151,6 @@ private fun LabelsList(
|
|||||||
onLabelClick: (Label) -> Unit,
|
onLabelClick: (Label) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
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-функция для отображения одного элемента в списке меток.
|
* @summary Composable-функция для отображения одного элемента в списке меток.
|
||||||
* @param label Объект `Label`, который нужно отобразить.
|
* @param label Объект `Label`, который нужно отобразить.
|
||||||
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
* @param onClick Лямбда-функция, вызываемая при нажатии на элемент.
|
||||||
@@ -195,7 +178,6 @@ private fun LabelListItem(
|
|||||||
label: Label,
|
label: Label,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = label.name) },
|
headlineContent = { Text(text = label.name) },
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
@@ -207,10 +189,10 @@ private fun LabelListItem(
|
|||||||
modifier = Modifier.clickable(onClick = onClick)
|
modifier = Modifier.clickable(onClick = onClick)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_FUNCTION] LabelListItem
|
// [END_ENTITY: Function('LabelListItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('CreateLabelDialog')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Диалоговое окно для создания новой метки.
|
* @summary Диалоговое окно для создания новой метки.
|
||||||
* @param onConfirm Лямбда-функция, вызываемая при подтверждении создания с именем метки.
|
* @param onConfirm Лямбда-функция, вызываемая при подтверждении создания с именем метки.
|
||||||
* @param onDismiss Лямбда-функция, вызываемая при закрытии диалога.
|
* @param onDismiss Лямбда-функция, вызываемая при закрытии диалога.
|
||||||
@@ -220,11 +202,9 @@ private fun CreateLabelDialog(
|
|||||||
onConfirm: (String) -> Unit,
|
onConfirm: (String) -> Unit,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
// [STATE]
|
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
val isConfirmEnabled = text.isNotBlank()
|
val isConfirmEnabled = text.isNotBlank()
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(text = stringResource(R.string.dialog_title_create_label)) },
|
title = { Text(text = stringResource(R.string.dialog_title_create_label)) },
|
||||||
@@ -252,6 +232,5 @@ private fun CreateLabelDialog(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_FUNCTION] CreateLabelDialog
|
// [END_ENTITY: Function('CreateLabelDialog')]
|
||||||
|
// [END_FILE_LabelsListScreen.kt]
|
||||||
// [END_FILE] LabelsListScreen.kt
|
|
||||||
@@ -2,35 +2,47 @@
|
|||||||
// [FILE] LabelsListUiState.kt
|
// [FILE] LabelsListUiState.kt
|
||||||
// [SEMANTICS] ui_state, sealed_interface, contract
|
// [SEMANTICS] ui_state, sealed_interface, contract
|
||||||
package com.homebox.lens.ui.screen.labelslist
|
package com.homebox.lens.ui.screen.labelslist
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.Label
|
import com.homebox.lens.domain.model.Label
|
||||||
// [CONTRACT]
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: SealedInterface('LabelsListUiState')]
|
||||||
/**
|
/**
|
||||||
[CONTRACT]
|
* @summary Определяет все возможные состояния для UI экрана со списком меток.
|
||||||
@summary Определяет все возможные состояния для UI экрана со списком меток.
|
* @description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
|
||||||
@description Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях.
|
|
||||||
*/
|
*/
|
||||||
sealed interface LabelsListUiState {
|
sealed interface LabelsListUiState {
|
||||||
|
// [ENTITY: DataClass('Success')]
|
||||||
|
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||||
/**
|
/**
|
||||||
@summary Состояние успеха, содержит список меток и состояние диалога.
|
* @summary Состояние успеха, содержит список меток и состояние диалога.
|
||||||
@property labels Список меток для отображения.
|
* @param labels Список меток для отображения.
|
||||||
@property isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
|
* @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки.
|
||||||
@invariant labels не может быть null.
|
* @invariant labels не может быть null.
|
||||||
*/
|
*/
|
||||||
data class Success(
|
data class Success(
|
||||||
val labels: List<Label>,
|
val labels: List<Label>,
|
||||||
val isShowingCreateDialog: Boolean = false
|
val isShowingCreateDialog: Boolean = false
|
||||||
) : LabelsListUiState
|
) : LabelsListUiState
|
||||||
|
// [END_ENTITY: DataClass('Success')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('Error')]
|
||||||
/**
|
/**
|
||||||
@summary Состояние ошибки.
|
* @summary Состояние ошибки.
|
||||||
@property message Текст ошибки для отображения пользователю.
|
* @param message Текст ошибки для отображения пользователю.
|
||||||
@invariant message не может быть пустой.
|
* @invariant message не может быть пустой.
|
||||||
*/
|
*/
|
||||||
data class Error(val message: String) : LabelsListUiState
|
data class Error(val message: String) : LabelsListUiState
|
||||||
|
// [END_ENTITY: DataClass('Error')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Loading')]
|
||||||
/**
|
/**
|
||||||
@summary Состояние загрузки данных.
|
* @summary Состояние загрузки данных.
|
||||||
@description Указывает, что идет процесс загрузки меток.
|
* @description Указывает, что идет процесс загрузки меток.
|
||||||
*/
|
*/
|
||||||
data object Loading : LabelsListUiState
|
data object Loading : LabelsListUiState
|
||||||
|
// [END_ENTITY: Object('Loading')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: SealedInterface('LabelsListUiState')]
|
||||||
// [END_FILE_LabelsListUiState.kt]
|
// [END_FILE_LabelsListUiState.kt]
|
||||||
@@ -15,11 +15,12 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
|
||||||
// [ENTITY: ViewModel('LabelsListViewModel')]
|
// [ENTITY: ViewModel('LabelsListViewModel')]
|
||||||
|
// [RELATION: ViewModel('LabelsListViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
|
||||||
|
// [RELATION: ViewModel('LabelsListViewModel')] -> [EMITS_STATE] -> [SealedInterface('LabelsListUiState')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary ViewModel для экрана со списком меток.
|
* @summary ViewModel для экрана со списком меток.
|
||||||
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
|
* @description Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки.
|
||||||
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
|
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
|
||||||
@@ -29,40 +30,32 @@ class LabelsListViewModel @Inject constructor(
|
|||||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// [STATE]
|
|
||||||
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading)
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
// [INIT]
|
|
||||||
init {
|
init {
|
||||||
loadLabels()
|
loadLabels()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [ENTITY: Function('loadLabels')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Загружает список меток.
|
* @summary Загружает список меток.
|
||||||
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
|
* @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его
|
||||||
* между состояниями `Loading`, `Success` и `Error`.
|
* между состояниями `Loading`, `Success` и `Error`.
|
||||||
* @sideeffect Асинхронно обновляет `_uiState`.
|
* @sideeffect Асинхронно обновляет `_uiState`.
|
||||||
*/
|
*/
|
||||||
// [ACTION]
|
|
||||||
fun loadLabels() {
|
fun loadLabels() {
|
||||||
// [ENTRYPOINT]
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = LabelsListUiState.Loading
|
_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 {
|
val result = runCatching {
|
||||||
getAllLabelsUseCase()
|
getAllLabelsUseCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
// [RESULT_HANDLER]
|
|
||||||
result.fold(
|
result.fold(
|
||||||
onSuccess = { labelOuts ->
|
onSuccess = { labelOuts ->
|
||||||
Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.")
|
Timber.i("[INFO][SUCCESS][labels_loaded] 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'.
|
|
||||||
val labels = labelOuts.map { labelOut ->
|
val labels = labelOuts.map { labelOut ->
|
||||||
Label(
|
Label(
|
||||||
id = labelOut.id,
|
id = labelOut.id,
|
||||||
@@ -72,7 +65,7 @@ class LabelsListViewModel @Inject constructor(
|
|||||||
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
|
_uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false)
|
||||||
},
|
},
|
||||||
onFailure = { exception ->
|
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(
|
_uiState.value = LabelsListUiState.Error(
|
||||||
message = exception.message ?: "Could not load labels."
|
message = exception.message ?: "Could not load labels."
|
||||||
)
|
)
|
||||||
@@ -80,41 +73,42 @@ class LabelsListViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('loadLabels')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('onShowCreateDialog')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Инициирует отображение диалога для создания метки.
|
* @summary Инициирует отображение диалога для создания метки.
|
||||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
|
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`.
|
||||||
* @sideeffect Обновляет `_uiState`.
|
* @sideeffect Обновляет `_uiState`.
|
||||||
*/
|
*/
|
||||||
// [ACTION]
|
|
||||||
fun onShowCreateDialog() {
|
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) {
|
if (_uiState.value is LabelsListUiState.Success) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
|
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onShowCreateDialog')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('onDismissCreateDialog')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Скрывает диалог создания метки.
|
* @summary Скрывает диалог создания метки.
|
||||||
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
|
* @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`.
|
||||||
* @sideeffect Обновляет `_uiState`.
|
* @sideeffect Обновляет `_uiState`.
|
||||||
*/
|
*/
|
||||||
// [ACTION]
|
|
||||||
fun onDismissCreateDialog() {
|
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) {
|
if (_uiState.value is LabelsListUiState.Success) {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
|
(it as LabelsListUiState.Success).copy(isShowingCreateDialog = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onDismissCreateDialog')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('createLabel')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
|
* @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА.
|
||||||
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
|
* @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие
|
||||||
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
|
* и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе.
|
||||||
@@ -122,19 +116,16 @@ class LabelsListViewModel @Inject constructor(
|
|||||||
* @precondition `name` не должен быть пустым.
|
* @precondition `name` не должен быть пустым.
|
||||||
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
|
* @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог.
|
||||||
*/
|
*/
|
||||||
// [ACTION]
|
|
||||||
fun createLabel(name: String) {
|
fun createLabel(name: String) {
|
||||||
// [PRECONDITION]
|
|
||||||
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
|
require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." }
|
||||||
|
|
||||||
// [ENTRYPOINT]
|
Timber.i("[INFO][ACTION][create_label] Create label called with name: '$name'. [STUBBED]")
|
||||||
Timber.i("[ACTION] Create label called with name: '$name'. [STUBBED]")
|
|
||||||
|
|
||||||
// [CORE-LOGIC] - Заглушка. Здесь будет вызов CreateLabelUseCase.
|
// [AI_NOTE]: Здесь будет вызов CreateLabelUseCase.
|
||||||
|
|
||||||
// [POSTCONDITION] Скрываем диалог после "создания".
|
|
||||||
onDismissCreateDialog()
|
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.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.homebox.lens.R
|
import com.homebox.lens.R
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [ENTRYPOINT]
|
// [ENTITY: Function('LocationEditScreen')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Composable-функция для экрана "Редактирование местоположения".
|
* @summary Composable-функция для экрана "Редактирование местоположения".
|
||||||
* @param locationId ID местоположения для редактирования или "new" для создания.
|
* @param locationId ID местоположения для редактирования или "new" для создания.
|
||||||
*/
|
*/
|
||||||
@@ -39,7 +39,10 @@ fun LocationEditScreen(
|
|||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
contentAlignment = Alignment.Center
|
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.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
import com.homebox.lens.ui.common.MainScaffold
|
||||||
import com.homebox.lens.ui.theme.HomeboxLensTheme
|
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-функция для экрана "Список местоположений".
|
* @summary Composable-функция для экрана "Список местоположений".
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@@ -68,10 +71,8 @@ fun LocationsListScreen(
|
|||||||
onAddNewLocationClick: () -> Unit,
|
onAddNewLocationClick: () -> Unit,
|
||||||
viewModel: LocationsListViewModel = hiltViewModel()
|
viewModel: LocationsListViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
// [STATE]
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// [UI_COMPONENT]
|
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.locations_list_title),
|
topBarTitle = stringResource(id = R.string.locations_list_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
@@ -92,16 +93,17 @@ fun LocationsListScreen(
|
|||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(innerPadding),
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onLocationClick = onLocationClick,
|
onLocationClick = onLocationClick,
|
||||||
onEditLocation = { /* TODO */ },
|
onEditLocation = { /* [AI_NOTE]: Implement onEditLocation */ },
|
||||||
onDeleteLocation = { /* TODO */ }
|
onDeleteLocation = { /* [AI_NOTE]: Implement onDeleteLocation */ }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('LocationsListScreen')]
|
||||||
|
|
||||||
// [HELPER]
|
// [ENTITY: Function('LocationsListContent')]
|
||||||
|
// [RELATION: Function('LocationsListContent')] -> [CONSUMES_STATE] -> [SealedInterface('LocationsListUiState')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Отображает основной контент экрана в зависимости от `uiState`.
|
* @summary Отображает основной контент экрана в зависимости от `uiState`.
|
||||||
* @param modifier Модификатор для стилизации.
|
* @param modifier Модификатор для стилизации.
|
||||||
* @param uiState Текущее состояние UI.
|
* @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 Карточка для отображения одного местоположения.
|
* @summary Карточка для отображения одного местоположения.
|
||||||
* @param location Данные о местоположении.
|
* @param location Данные о местоположении.
|
||||||
* @param onClick Лямбда-обработчик нажатия на карточку.
|
* @param onClick Лямбда-обработчик нажатия на карточку.
|
||||||
@@ -224,8 +227,9 @@ private fun LocationCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('LocationCard')]
|
||||||
|
|
||||||
// [PREVIEW]
|
// [ENTITY: Function('LocationsListSuccessPreview')]
|
||||||
@Preview(showBackground = true, name = "Locations List Success")
|
@Preview(showBackground = true, name = "Locations List Success")
|
||||||
@Composable
|
@Composable
|
||||||
fun LocationsListSuccessPreview() {
|
fun LocationsListSuccessPreview() {
|
||||||
@@ -243,8 +247,9 @@ fun LocationsListSuccessPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('LocationsListSuccessPreview')]
|
||||||
|
|
||||||
// [PREVIEW]
|
// [ENTITY: Function('LocationsListEmptyPreview')]
|
||||||
@Preview(showBackground = true, name = "Locations List Empty")
|
@Preview(showBackground = true, name = "Locations List Empty")
|
||||||
@Composable
|
@Composable
|
||||||
fun LocationsListEmptyPreview() {
|
fun LocationsListEmptyPreview() {
|
||||||
@@ -257,8 +262,9 @@ fun LocationsListEmptyPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('LocationsListEmptyPreview')]
|
||||||
|
|
||||||
// [PREVIEW]
|
// [ENTITY: Function('LocationsListLoadingPreview')]
|
||||||
@Preview(showBackground = true, name = "Locations List Loading")
|
@Preview(showBackground = true, name = "Locations List Loading")
|
||||||
@Composable
|
@Composable
|
||||||
fun LocationsListLoadingPreview() {
|
fun LocationsListLoadingPreview() {
|
||||||
@@ -271,8 +277,9 @@ fun LocationsListLoadingPreview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('LocationsListLoadingPreview')]
|
||||||
|
|
||||||
// [PREVIEW]
|
// [ENTITY: Function('LocationsListErrorPreview')]
|
||||||
@Preview(showBackground = true, name = "Locations List Error")
|
@Preview(showBackground = true, name = "Locations List Error")
|
||||||
@Composable
|
@Composable
|
||||||
fun LocationsListErrorPreview() {
|
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
|
package com.homebox.lens.ui.screen.locationslist
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
import com.homebox.lens.domain.model.LocationOutCount
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: SealedInterface('LocationsListUiState')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Определяет возможные состояния UI для экрана списка местоположений.
|
* @summary Определяет возможные состояния UI для экрана списка местоположений.
|
||||||
* @see LocationsListViewModel
|
* @see LocationsListViewModel
|
||||||
*/
|
*/
|
||||||
sealed interface LocationsListUiState {
|
sealed interface LocationsListUiState {
|
||||||
|
// [ENTITY: DataClass('Success')]
|
||||||
|
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
|
||||||
/**
|
/**
|
||||||
* [STATE]
|
|
||||||
* @summary Состояние успешной загрузки данных.
|
* @summary Состояние успешной загрузки данных.
|
||||||
* @param locations Список местоположений для отображения.
|
* @param locations Список местоположений для отображения.
|
||||||
*/
|
*/
|
||||||
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
|
data class Success(val locations: List<LocationOutCount>) : LocationsListUiState
|
||||||
|
// [END_ENTITY: DataClass('Success')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('Error')]
|
||||||
/**
|
/**
|
||||||
* [STATE]
|
|
||||||
* @summary Состояние ошибки.
|
* @summary Состояние ошибки.
|
||||||
* @param message Сообщение об ошибке.
|
* @param message Сообщение об ошибке.
|
||||||
*/
|
*/
|
||||||
data class Error(val message: String) : LocationsListUiState
|
data class Error(val message: String) : LocationsListUiState
|
||||||
|
// [END_ENTITY: DataClass('Error')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Loading')]
|
||||||
/**
|
/**
|
||||||
* [STATE]
|
|
||||||
* @summary Состояние загрузки данных.
|
* @summary Состояние загрузки данных.
|
||||||
*/
|
*/
|
||||||
object Loading : LocationsListUiState
|
object Loading : LocationsListUiState
|
||||||
|
// [END_ENTITY: Object('Loading')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: SealedInterface('LocationsListUiState')]
|
||||||
// [END_FILE_LocationsListUiState.kt]
|
// [END_FILE_LocationsListUiState.kt]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package com.homebox.lens.ui.screen.locationslist
|
package com.homebox.lens.ui.screen.locationslist
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
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.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
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 для экрана списка местоположений.
|
* @summary ViewModel для экрана списка местоположений.
|
||||||
* @param getAllLocationsUseCase Use case для получения всех местоположений.
|
* @param getAllLocationsUseCase Use case для получения всех местоположений.
|
||||||
* @property uiState Поток, содержащий текущее состояние UI.
|
* @property uiState Поток, содержащий текущее состояние UI.
|
||||||
@@ -27,32 +31,34 @@ class LocationsListViewModel @Inject constructor(
|
|||||||
private val getAllLocationsUseCase: GetAllLocationsUseCase
|
private val getAllLocationsUseCase: GetAllLocationsUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// [STATE]
|
|
||||||
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
private val _uiState = MutableStateFlow<LocationsListUiState>(LocationsListUiState.Loading)
|
||||||
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<LocationsListUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
// [INITIALIZER]
|
|
||||||
init {
|
init {
|
||||||
loadLocations()
|
loadLocations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// [ACTION]
|
// [ENTITY: Function('loadLocations')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Загружает список местоположений из репозитория.
|
* @summary Загружает список местоположений из репозитория.
|
||||||
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
|
* @sideeffect Обновляет `_uiState` в зависимости от результата: Loading -> Success/Error.
|
||||||
*/
|
*/
|
||||||
fun loadLocations() {
|
fun loadLocations() {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][loading_locations] Starting to load locations.")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.value = LocationsListUiState.Loading
|
_uiState.value = LocationsListUiState.Loading
|
||||||
try {
|
try {
|
||||||
|
Timber.d("[DEBUG][ACTION][fetching_locations] Fetching locations from use case.")
|
||||||
val locations = getAllLocationsUseCase()
|
val locations = getAllLocationsUseCase()
|
||||||
_uiState.value = LocationsListUiState.Success(locations)
|
_uiState.value = LocationsListUiState.Success(locations)
|
||||||
|
Timber.d("[DEBUG][SUCCESS][locations_loaded] Successfully loaded locations.")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "[ERROR][EXCEPTION][loading_failed] Failed to load locations.")
|
||||||
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
|
_uiState.value = LocationsListUiState.Error(e.message ?: "Unknown error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_CLASS_LocationsListViewModel]
|
// [END_ENTITY: Function('loadLocations')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: ViewModel('LocationsListViewModel')]
|
||||||
// [END_FILE_LocationsListViewModel.kt]
|
// [END_FILE_LocationsListViewModel.kt]
|
||||||
@@ -11,10 +11,12 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import com.homebox.lens.R
|
import com.homebox.lens.R
|
||||||
import com.homebox.lens.navigation.NavigationActions
|
import com.homebox.lens.navigation.NavigationActions
|
||||||
import com.homebox.lens.ui.common.MainScaffold
|
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-функция для экрана "Поиск".
|
* @summary Composable-функция для экрана "Поиск".
|
||||||
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
|
||||||
* @param navigationActions Объект с навигационными действиями.
|
* @param navigationActions Объект с навигационными действиями.
|
||||||
@@ -24,14 +26,14 @@ fun SearchScreen(
|
|||||||
currentRoute: String?,
|
currentRoute: String?,
|
||||||
navigationActions: NavigationActions
|
navigationActions: NavigationActions
|
||||||
) {
|
) {
|
||||||
// [UI_COMPONENT]
|
|
||||||
MainScaffold(
|
MainScaffold(
|
||||||
topBarTitle = stringResource(id = R.string.search_title),
|
topBarTitle = stringResource(id = R.string.search_title),
|
||||||
currentRoute = currentRoute,
|
currentRoute = currentRoute,
|
||||||
navigationActions = navigationActions
|
navigationActions = navigationActions
|
||||||
) {
|
) {
|
||||||
// [CORE-LOGIC]
|
// [AI_NOTE]: Implement Search Screen UI
|
||||||
Text(text = "TODO: Search Screen")
|
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
|
// [PACKAGE] com.homebox.lens.ui.screen.search
|
||||||
// [FILE] SearchViewModel.kt
|
// [FILE] SearchViewModel.kt
|
||||||
|
// [SEMANTICS] ui, viewmodel, search
|
||||||
package com.homebox.lens.ui.screen.search
|
package com.homebox.lens.ui.screen.search
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
// [ENTITY: ViewModel('SearchViewModel')]
|
||||||
|
/**
|
||||||
|
* @summary ViewModel for the search screen.
|
||||||
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SearchViewModel @Inject constructor() : ViewModel() {
|
class SearchViewModel @Inject constructor() : ViewModel() {
|
||||||
// [STATE]
|
// [AI_NOTE]: Implement UI state
|
||||||
// TODO: Implement UI state
|
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: ViewModel('SearchViewModel')]
|
||||||
// [END_FILE_SearchViewModel.kt]
|
// [END_FILE_SearchViewModel.kt]
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.homebox.lens.R
|
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-функция для экрана настройки соединения с сервером.
|
* @summary Главная Composable-функция для экрана настройки соединения с сервером.
|
||||||
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
|
||||||
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
|
* @param onSetupComplete Лямбда, вызываемая после успешной настройки и входа.
|
||||||
@@ -34,15 +36,12 @@ fun SetupScreen(
|
|||||||
viewModel: SetupViewModel = hiltViewModel(),
|
viewModel: SetupViewModel = hiltViewModel(),
|
||||||
onSetupComplete: () -> Unit
|
onSetupComplete: () -> Unit
|
||||||
) {
|
) {
|
||||||
// [STATE]
|
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
if (uiState.isSetupComplete) {
|
if (uiState.isSetupComplete) {
|
||||||
onSetupComplete()
|
onSetupComplete()
|
||||||
}
|
}
|
||||||
|
|
||||||
// [UI_COMPONENT]
|
|
||||||
SetupScreenContent(
|
SetupScreenContent(
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
onServerUrlChange = viewModel::onServerUrlChange,
|
onServerUrlChange = viewModel::onServerUrlChange,
|
||||||
@@ -50,12 +49,12 @@ fun SetupScreen(
|
|||||||
onPasswordChange = viewModel::onPasswordChange,
|
onPasswordChange = viewModel::onPasswordChange,
|
||||||
onConnectClick = viewModel::connect
|
onConnectClick = viewModel::connect
|
||||||
)
|
)
|
||||||
// [END_FUNCTION_SetupScreen]
|
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('SetupScreen')]
|
||||||
|
|
||||||
// [HELPER]
|
// [ENTITY: Function('SetupScreenContent')]
|
||||||
|
// [RELATION: Function('SetupScreenContent')] -> [CONSUMES_STATE] -> [DataClass('SetupUiState')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Отображает контент экрана настройки: поля ввода и кнопку.
|
* @summary Отображает контент экрана настройки: поля ввода и кнопку.
|
||||||
* @param uiState Текущее состояние UI.
|
* @param uiState Текущее состояние UI.
|
||||||
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
|
* @param onServerUrlChange Лямбда-обработчик изменения URL сервера.
|
||||||
@@ -123,10 +122,10 @@ private fun SetupScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_FUNCTION_SetupScreenContent]
|
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('SetupScreenContent')]
|
||||||
|
|
||||||
// [PREVIEW]
|
// [ENTITY: Function('SetupScreenPreview')]
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
fun SetupScreenPreview() {
|
fun SetupScreenPreview() {
|
||||||
@@ -138,4 +137,5 @@ fun SetupScreenPreview() {
|
|||||||
onConnectClick = {}
|
onConnectClick = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('SetupScreenPreview')]
|
||||||
// [END_FILE_SetupScreen.kt]
|
// [END_FILE_SetupScreen.kt]
|
||||||
|
|||||||
@@ -4,17 +4,16 @@
|
|||||||
|
|
||||||
package com.homebox.lens.ui.screen.setup
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('SetupUiState')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('SetupUiState')]
|
* @summary Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
||||||
* [CONTRACT]
|
* @description Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
||||||
* Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
* @param serverUrl URL-адрес сервера Homebox.
|
||||||
* Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
* @param username Имя пользователя для входа.
|
||||||
* @property serverUrl URL-адрес сервера Homebox.
|
* @param password Пароль пользователя.
|
||||||
* @property username Имя пользователя для входа.
|
* @param isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
||||||
* @property password Пароль пользователя.
|
* @param error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
||||||
* @property isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
* @param isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
|
||||||
* @property error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
|
||||||
* @property isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
|
|
||||||
*/
|
*/
|
||||||
data class SetupUiState(
|
data class SetupUiState(
|
||||||
val serverUrl: String = "",
|
val serverUrl: String = "",
|
||||||
@@ -24,4 +23,5 @@ data class SetupUiState(
|
|||||||
val error: String? = null,
|
val error: String? = null,
|
||||||
val isSetupComplete: Boolean = false
|
val isSetupComplete: Boolean = false
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('SetupUiState')]
|
||||||
// [END_FILE_SetupUiState.kt]
|
// [END_FILE_SetupUiState.kt]
|
||||||
@@ -2,31 +2,30 @@
|
|||||||
// [FILE] SetupViewModel.kt
|
// [FILE] SetupViewModel.kt
|
||||||
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
|
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
|
||||||
package com.homebox.lens.ui.screen.setup
|
package com.homebox.lens.ui.screen.setup
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.homebox.lens.domain.model.Credentials
|
import com.homebox.lens.domain.model.Credentials
|
||||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||||
import com.homebox.lens.domain.usecase.LoginUseCase
|
import com.homebox.lens.domain.usecase.LoginUseCase
|
||||||
import com.homebox.lens.ui.screen.setup.SetupUiState
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [VIEWMODEL]
|
|
||||||
// [ENTITY: ViewModel('SetupViewModel')]
|
// [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]
|
* @summary ViewModel для экрана первоначальной настройки (Setup).
|
||||||
* ViewModel для экрана первоначальной настройки (Setup).
|
* @param credentialsRepository Репозиторий для операций с учетными данными.
|
||||||
* Отвечает за:
|
* @param loginUseCase Use case для выполнения логики входа.
|
||||||
* 1. Загрузку и сохранение учетных данных (URL сервера, логин, пароль).
|
|
||||||
* 2. Управление состоянием UI экрана (`SetupUiState`).
|
|
||||||
* 3. Инициацию процесса входа в систему через `LoginUseCase`.
|
|
||||||
* @property credentialsRepository Репозиторий для операций с учетными данными.
|
|
||||||
* @property loginUseCase Use case для выполнения логики входа.
|
|
||||||
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
|
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -35,28 +34,20 @@ class SetupViewModel @Inject constructor(
|
|||||||
private val loginUseCase: LoginUseCase
|
private val loginUseCase: LoginUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// [STATE]
|
|
||||||
private val _uiState = MutableStateFlow(SetupUiState())
|
private val _uiState = MutableStateFlow(SetupUiState())
|
||||||
val uiState = _uiState.asStateFlow()
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
// [LIFECYCLE_HANDLER]
|
|
||||||
init {
|
init {
|
||||||
// [ACTION] Загружаем учетные данные при создании ViewModel.
|
|
||||||
loadCredentials()
|
loadCredentials()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('loadCredentials')]
|
||||||
* [CONTRACT]
|
|
||||||
* [HELPER] Загружает учетные данные из репозитория при инициализации.
|
|
||||||
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
|
|
||||||
*/
|
|
||||||
private fun loadCredentials() {
|
private fun loadCredentials() {
|
||||||
// [ENTRYPOINT]
|
Timber.d("[DEBUG][ENTRYPOINT][loading_credentials] Loading credentials from repository.")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// [CORE-LOGIC] Подписываемся на поток учетных данных.
|
|
||||||
credentialsRepository.getCredentials().collect { credentials ->
|
credentialsRepository.getCredentials().collect { credentials ->
|
||||||
// [ACTION] Обновляем состояние, если учетные данные существуют.
|
|
||||||
if (credentials != null) {
|
if (credentials != null) {
|
||||||
|
Timber.d("[DEBUG][ACTION][updating_state] Credentials found, updating UI state.")
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
serverUrl = credentials.serverUrl,
|
serverUrl = credentials.serverUrl,
|
||||||
@@ -68,76 +59,55 @@ class SetupViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('loadCredentials')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('onServerUrlChange')]
|
||||||
* [CONTRACT]
|
|
||||||
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
|
|
||||||
* @param newUrl Новое значение URL.
|
|
||||||
* @sideeffect Обновляет поле `serverUrl` в `_uiState`.
|
|
||||||
*/
|
|
||||||
fun onServerUrlChange(newUrl: String) {
|
fun onServerUrlChange(newUrl: String) {
|
||||||
_uiState.update { it.copy(serverUrl = newUrl) }
|
_uiState.update { it.copy(serverUrl = newUrl) }
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onServerUrlChange')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('onUsernameChange')]
|
||||||
* [CONTRACT]
|
|
||||||
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
|
|
||||||
* @param newUsername Новое значение имени пользователя.
|
|
||||||
* @sideeffect Обновляет поле `username` в `_uiState`.
|
|
||||||
*/
|
|
||||||
fun onUsernameChange(newUsername: String) {
|
fun onUsernameChange(newUsername: String) {
|
||||||
_uiState.update { it.copy(username = newUsername) }
|
_uiState.update { it.copy(username = newUsername) }
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onUsernameChange')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('onPasswordChange')]
|
||||||
* [CONTRACT]
|
|
||||||
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
|
|
||||||
* @param newPassword Новое значение пароля.
|
|
||||||
* @sideeffect Обновляет поле `password` в `_uiState`.
|
|
||||||
*/
|
|
||||||
fun onPasswordChange(newPassword: String) {
|
fun onPasswordChange(newPassword: String) {
|
||||||
_uiState.update { it.copy(password = newPassword) }
|
_uiState.update { it.copy(password = newPassword) }
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('onPasswordChange')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('connect')]
|
||||||
* [CONTRACT]
|
|
||||||
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
|
|
||||||
* Выполняет две основные операции:
|
|
||||||
* 1. Сохраняет введенные учетные данные для последующих сессий.
|
|
||||||
* 2. Выполняет вход в систему с использованием этих данных.
|
|
||||||
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
|
|
||||||
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
|
|
||||||
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
|
|
||||||
*/
|
|
||||||
fun connect() {
|
fun connect() {
|
||||||
// [ENTRYPOINT]
|
Timber.d("[DEBUG][ENTRYPOINT][connecting] Starting connection process.")
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
|
|
||||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||||
|
|
||||||
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
|
|
||||||
val credentials = Credentials(
|
val credentials = Credentials(
|
||||||
serverUrl = _uiState.value.serverUrl.trim(),
|
serverUrl = _uiState.value.serverUrl.trim(),
|
||||||
username = _uiState.value.username.trim(),
|
username = _uiState.value.username.trim(),
|
||||||
password = _uiState.value.password
|
password = _uiState.value.password
|
||||||
)
|
)
|
||||||
|
|
||||||
// [ACTION] Сохраняем учетные данные для будущего использования.
|
Timber.d("[DEBUG][ACTION][saving_credentials] Saving credentials.")
|
||||||
credentialsRepository.saveCredentials(credentials)
|
credentialsRepository.saveCredentials(credentials)
|
||||||
|
|
||||||
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
|
Timber.d("[DEBUG][ACTION][executing_login] Executing login use case.")
|
||||||
loginUseCase(credentials).fold(
|
loginUseCase(credentials).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
|
Timber.d("[DEBUG][SUCCESS][login_successful] Login successful.")
|
||||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||||
},
|
},
|
||||||
onFailure = { exception ->
|
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") }
|
_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]
|
// [END_FILE_SetupViewModel.kt]
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.theme
|
// [PACKAGE] com.homebox.lens.ui.theme
|
||||||
// [FILE] Color.kt
|
// [FILE] Color.kt
|
||||||
|
// [SEMANTICS] ui, theme, color
|
||||||
package com.homebox.lens.ui.theme
|
package com.homebox.lens.ui.theme
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
val Purple80 = Color(0xFFD0BCFF)
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.theme
|
// [PACKAGE] com.homebox.lens.ui.theme
|
||||||
// [FILE] Theme.kt
|
// [FILE] Theme.kt
|
||||||
|
// [SEMANTICS] ui, theme
|
||||||
package com.homebox.lens.ui.theme
|
package com.homebox.lens.ui.theme
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
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.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = Purple80,
|
||||||
@@ -30,10 +32,17 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
tertiary = Pink40
|
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
|
@Composable
|
||||||
fun HomeboxLensTheme(
|
fun HomeboxLensTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = true,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -61,4 +70,5 @@ fun HomeboxLensTheme(
|
|||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('HomeboxLensTheme')]
|
||||||
// [END_FILE_Theme.kt]
|
// [END_FILE_Theme.kt]
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
// [PACKAGE] com.homebox.lens.ui.theme
|
// [PACKAGE] com.homebox.lens.ui.theme
|
||||||
// [FILE] Typography.kt
|
// [FILE] Typography.kt
|
||||||
|
// [SEMANTICS] ui, theme, typography
|
||||||
package com.homebox.lens.ui.theme
|
package com.homebox.lens.ui.theme
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
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(
|
val Typography = Typography(
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
@@ -19,5 +24,6 @@ val Typography = Typography(
|
|||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataStructure('Typography')]
|
||||||
|
|
||||||
// [END_FILE_Typography.kt]
|
// [END_FILE_Typography.kt]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// [FILE] Dependencies.kt
|
// [FILE] Dependencies.kt
|
||||||
// [PURPOSE] Centralized dependency management for the entire project.
|
// [SEMANTICS] build, dependencies
|
||||||
|
|
||||||
|
// [ENTITY: Object('Versions')]
|
||||||
object Versions {
|
object Versions {
|
||||||
// Build
|
// Build
|
||||||
const val compileSdk = 34
|
const val compileSdk = 34
|
||||||
@@ -45,7 +46,9 @@ object Versions {
|
|||||||
const val extJunit = "1.1.5"
|
const val extJunit = "1.1.5"
|
||||||
const val espresso = "3.5.1"
|
const val espresso = "3.5.1"
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Object('Versions')]
|
||||||
|
|
||||||
|
// [ENTITY: Object('Libs')]
|
||||||
object Libs {
|
object Libs {
|
||||||
// Kotlin
|
// Kotlin
|
||||||
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
|
const val coroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
|
||||||
@@ -96,5 +99,6 @@ object Libs {
|
|||||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
|
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Object('Libs')]
|
||||||
|
|
||||||
// [END_FILE_Dependencies.kt]
|
// [END_FILE_Dependencies.kt]
|
||||||
@@ -62,6 +62,9 @@ dependencies {
|
|||||||
implementation(Libs.hiltAndroid)
|
implementation(Libs.hiltAndroid)
|
||||||
kapt(Libs.hiltCompiler)
|
kapt(Libs.hiltCompiler)
|
||||||
|
|
||||||
|
// [DEPENDENCY] Logging
|
||||||
|
implementation(Libs.timber)
|
||||||
|
|
||||||
// [DEPENDENCY] Testing
|
// [DEPENDENCY] Testing
|
||||||
testImplementation(Libs.junit)
|
testImplementation(Libs.junit)
|
||||||
androidTestImplementation(Libs.extJunit)
|
androidTestImplementation(Libs.extJunit)
|
||||||
|
|||||||
@@ -1,74 +1,74 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api
|
// [PACKAGE] com.homebox.lens.data.api
|
||||||
// [FILE] HomeboxApiService.kt
|
// [FILE] HomeboxApiService.kt
|
||||||
|
// [SEMANTICS] data, api, retrofit
|
||||||
package com.homebox.lens.data.api
|
package com.homebox.lens.data.api
|
||||||
|
|
||||||
import com.homebox.lens.data.api.dto.GroupStatisticsDto
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.api.dto.ItemCreateDto
|
import com.homebox.lens.data.api.dto.*
|
||||||
import com.homebox.lens.data.api.dto.ItemOutDto
|
|
||||||
import com.homebox.lens.data.api.dto.ItemSummaryDto
|
|
||||||
import com.homebox.lens.data.api.dto.ItemUpdateDto
|
|
||||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
|
||||||
import com.homebox.lens.data.api.dto.LabelOutDto
|
|
||||||
import com.homebox.lens.data.api.dto.LabelSummaryDto
|
|
||||||
import com.homebox.lens.data.api.dto.LocationOutCountDto
|
|
||||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
|
||||||
import com.homebox.lens.data.api.dto.PaginationResultDto
|
|
||||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.*
|
||||||
import retrofit2.http.DELETE
|
// [END_IMPORTS]
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Headers
|
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.PUT
|
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Interface('HomeboxApiService')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Interface('HomeboxApiService')]
|
* @summary Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
|
||||||
* [PURPOSE] Определяет эндпоинты для взаимодействия с Homebox API, используя DTO.
|
|
||||||
*/
|
*/
|
||||||
interface HomeboxApiService {
|
interface HomeboxApiService {
|
||||||
|
|
||||||
// [ENDPOINT] Auth
|
// [ENTITY: ApiEndpoint('login')]
|
||||||
@Headers("Content-Type: application/json")
|
@Headers("Content-Type: application/json")
|
||||||
@POST("v1/users/login")
|
@POST("v1/users/login")
|
||||||
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||||
|
// [END_ENTITY: ApiEndpoint('login')]
|
||||||
|
|
||||||
// [ENDPOINT] Items
|
// [ENTITY: ApiEndpoint('getItems')]
|
||||||
@GET("v1/items")
|
@GET("v1/items")
|
||||||
suspend fun getItems(
|
suspend fun getItems(
|
||||||
@Query("q") query: String? = null,
|
@Query("q") query: String? = null,
|
||||||
@Query("page") page: Int? = null,
|
@Query("page") page: Int? = null,
|
||||||
@Query("pageSize") pageSize: Int? = null
|
@Query("pageSize") pageSize: Int? = null
|
||||||
): PaginationResultDto<ItemSummaryDto>
|
): PaginationResultDto<ItemSummaryDto>
|
||||||
|
// [END_ENTITY: ApiEndpoint('getItems')]
|
||||||
|
|
||||||
|
// [ENTITY: ApiEndpoint('createItem')]
|
||||||
@POST("v1/items")
|
@POST("v1/items")
|
||||||
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
|
suspend fun createItem(@Body item: ItemCreateDto): ItemSummaryDto
|
||||||
|
// [END_ENTITY: ApiEndpoint('createItem')]
|
||||||
|
|
||||||
|
// [ENTITY: ApiEndpoint('getItem')]
|
||||||
@GET("v1/items/{id}")
|
@GET("v1/items/{id}")
|
||||||
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
|
suspend fun getItem(@Path("id") itemId: String): ItemOutDto
|
||||||
|
// [END_ENTITY: ApiEndpoint('getItem')]
|
||||||
|
|
||||||
|
// [ENTITY: ApiEndpoint('updateItem')]
|
||||||
@PUT("v1/items/{id}")
|
@PUT("v1/items/{id}")
|
||||||
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
|
suspend fun updateItem(@Path("id") itemId: String, @Body item: ItemUpdateDto): ItemOutDto
|
||||||
|
// [END_ENTITY: ApiEndpoint('updateItem')]
|
||||||
|
|
||||||
|
// [ENTITY: ApiEndpoint('deleteItem')]
|
||||||
@DELETE("v1/items/{id}")
|
@DELETE("v1/items/{id}")
|
||||||
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
|
suspend fun deleteItem(@Path("id") itemId: String): Response<Unit>
|
||||||
|
// [END_ENTITY: ApiEndpoint('deleteItem')]
|
||||||
|
|
||||||
// [ENDPOINT] Locations
|
// [ENTITY: ApiEndpoint('getLocations')]
|
||||||
@GET("v1/locations")
|
@GET("v1/locations")
|
||||||
suspend fun getLocations(): List<LocationOutCountDto>
|
suspend fun getLocations(): List<LocationOutCountDto>
|
||||||
|
// [END_ENTITY: ApiEndpoint('getLocations')]
|
||||||
|
|
||||||
// [ENDPOINT] Labels
|
// [ENTITY: ApiEndpoint('getLabels')]
|
||||||
@GET("v1/labels")
|
@GET("v1/labels")
|
||||||
suspend fun getLabels(): List<LabelOutDto>
|
suspend fun getLabels(): List<LabelOutDto>
|
||||||
|
// [END_ENTITY: ApiEndpoint('getLabels')]
|
||||||
|
|
||||||
|
// [ENTITY: ApiEndpoint('createLabel')]
|
||||||
@POST("v1/labels")
|
@POST("v1/labels")
|
||||||
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
|
suspend fun createLabel(@Body newLabel: LabelCreateDto): LabelSummaryDto
|
||||||
|
// [END_ENTITY: ApiEndpoint('createLabel')]
|
||||||
|
|
||||||
// [ENDPOINT] Statistics
|
// [ENTITY: ApiEndpoint('getStatistics')]
|
||||||
@GET("v1/groups/statistics")
|
@GET("v1/groups/statistics")
|
||||||
suspend fun getStatistics(): GroupStatisticsDto
|
suspend fun getStatistics(): GroupStatisticsDto
|
||||||
|
// [END_ENTITY: ApiEndpoint('getStatistics')]
|
||||||
}
|
}
|
||||||
// [END_FILE_HomeboxApiService.kt]
|
// [END_ENTITY: Interface('HomeboxApiService')]
|
||||||
|
// [END_FILE_HomeboxApiService.kt]
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.CustomField
|
import com.homebox.lens.domain.model.CustomField
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('CustomFieldDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для кастомного поля.
|
||||||
* DTO для кастомного поля.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CustomFieldDto(
|
data class CustomFieldDto(
|
||||||
@@ -20,10 +20,12 @@ data class CustomFieldDto(
|
|||||||
@Json(name = "value") val value: String,
|
@Json(name = "value") val value: String,
|
||||||
@Json(name = "type") val type: String
|
@Json(name = "type") val type: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('CustomFieldDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('CustomField')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из CustomFieldDto в доменную модель CustomField.
|
||||||
* Маппер из CustomFieldDto в доменную модель CustomField.
|
|
||||||
*/
|
*/
|
||||||
fun CustomFieldDto.toDomain(): CustomField {
|
fun CustomFieldDto.toDomain(): CustomField {
|
||||||
return CustomField(
|
return CustomField(
|
||||||
@@ -32,3 +34,4 @@ fun CustomFieldDto.toDomain(): CustomField {
|
|||||||
type = this.type
|
type = this.type
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -8,14 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.GroupStatistics
|
import com.homebox.lens.domain.model.GroupStatistics
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('GroupStatisticsDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для статистики.
|
||||||
* DTO для статистики.
|
|
||||||
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
|
|
||||||
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
|
|
||||||
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class GroupStatisticsDto(
|
data class GroupStatisticsDto(
|
||||||
@@ -23,19 +20,17 @@ data class GroupStatisticsDto(
|
|||||||
@Json(name = "totalLabels") val totalLabels: Int,
|
@Json(name = "totalLabels") val totalLabels: Int,
|
||||||
@Json(name = "totalLocations") val totalLocations: Int,
|
@Json(name = "totalLocations") val totalLocations: Int,
|
||||||
@Json(name = "totalItemPrice") val totalItemPrice: Double,
|
@Json(name = "totalItemPrice") val totalItemPrice: Double,
|
||||||
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
|
|
||||||
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
|
|
||||||
@Json(name = "totalUsers") val totalUsers: Int? = null,
|
@Json(name = "totalUsers") val totalUsers: Int? = null,
|
||||||
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
|
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('GroupStatisticsDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('GroupStatistics')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
||||||
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
|
||||||
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
|
|
||||||
*/
|
*/
|
||||||
fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
||||||
// [ACTION] Маппим данные из DTO в доменную модель.
|
|
||||||
return GroupStatistics(
|
return GroupStatistics(
|
||||||
items = this.totalItems,
|
items = this.totalItems,
|
||||||
labels = this.totalLabels,
|
labels = this.totalLabels,
|
||||||
@@ -43,4 +38,5 @@ fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
|||||||
totalValue = this.totalItemPrice
|
totalValue = this.totalItemPrice
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_FILE_GroupStatisticsDto.kt]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
// [END_FILE_GroupStatisticsDto.kt]
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.Image
|
import com.homebox.lens.domain.model.Image
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('ImageDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для изображения.
|
||||||
* DTO для изображения.
|
* @param id Уникальный идентификатор.
|
||||||
* @property id Уникальный идентификатор.
|
* @param path Путь к файлу.
|
||||||
* @property path Путь к файлу.
|
* @param isPrimary Является ли основным.
|
||||||
* @property isPrimary Является ли основным.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ImageDto(
|
data class ImageDto(
|
||||||
@@ -23,10 +23,12 @@ data class ImageDto(
|
|||||||
@Json(name = "path") val path: String,
|
@Json(name = "path") val path: String,
|
||||||
@Json(name = "isPrimary") val isPrimary: Boolean
|
@Json(name = "isPrimary") val isPrimary: Boolean
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ImageDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('Image')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из ImageDto в доменную модель Image.
|
||||||
* Маппер из ImageDto в доменную модель Image.
|
|
||||||
*/
|
*/
|
||||||
fun ImageDto.toDomain(): Image {
|
fun ImageDto.toDomain(): Image {
|
||||||
return Image(
|
return Image(
|
||||||
@@ -35,3 +37,4 @@ fun ImageDto.toDomain(): Image {
|
|||||||
isPrimary = this.isPrimary
|
isPrimary = this.isPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.ItemAttachment
|
import com.homebox.lens.domain.model.ItemAttachment
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('ItemAttachmentDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для вложения.
|
||||||
* DTO для вложения.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemAttachmentDto(
|
data class ItemAttachmentDto(
|
||||||
@@ -23,10 +23,12 @@ data class ItemAttachmentDto(
|
|||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemAttachmentDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemAttachment')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
|
||||||
* Маппер из ItemAttachmentDto в доменную модель ItemAttachment.
|
|
||||||
*/
|
*/
|
||||||
fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
||||||
return ItemAttachment(
|
return ItemAttachment(
|
||||||
@@ -38,3 +40,4 @@ fun ItemAttachmentDto.toDomain(): ItemAttachment {
|
|||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.ItemCreate
|
import com.homebox.lens.domain.model.ItemCreate
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('ItemCreateDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для создания вещи.
|
||||||
* DTO для создания вещи.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemCreateDto(
|
data class ItemCreateDto(
|
||||||
@@ -30,10 +30,12 @@ data class ItemCreateDto(
|
|||||||
@Json(name = "parentId") val parentId: String?,
|
@Json(name = "parentId") val parentId: String?,
|
||||||
@Json(name = "labelIds") val labelIds: List<String>?
|
@Json(name = "labelIds") val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemCreateDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDto')]
|
||||||
|
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemCreateDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из доменной модели ItemCreate в ItemCreateDto.
|
||||||
* Маппер из доменной модели ItemCreate в ItemCreateDto.
|
|
||||||
*/
|
*/
|
||||||
fun ItemCreate.toDto(): ItemCreateDto {
|
fun ItemCreate.toDto(): ItemCreateDto {
|
||||||
return ItemCreateDto(
|
return ItemCreateDto(
|
||||||
@@ -52,3 +54,4 @@ fun ItemCreate.toDto(): ItemCreateDto {
|
|||||||
labelIds = this.labelIds
|
labelIds = this.labelIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDto')]
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||||
// [FILE] ItemDto.kt
|
// [FILE] ItemDto.kt
|
||||||
|
// [SEMANTICS] data, dto, api
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('ItemOut')]
|
||||||
|
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
|
||||||
|
// [RELATION: DataClass('ItemOut')] -> [DEPENDS_ON] -> [DataClass('LabelOutDto')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('ItemOut')]
|
* @summary DTO для полной информации о вещи (GET /v1/items/{id}).
|
||||||
* [PURPOSE] DTO для полной информации о вещи (GET /v1/items/{id}).
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemOut(
|
data class ItemOut(
|
||||||
@@ -23,10 +26,12 @@ data class ItemOut(
|
|||||||
@Json(name = "value") val value: BigDecimal?,
|
@Json(name = "value") val value: BigDecimal?,
|
||||||
@Json(name = "createdAt") val createdAt: String?
|
@Json(name = "createdAt") val createdAt: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemOut')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('ItemSummary')]
|
||||||
|
// [RELATION: DataClass('ItemSummary')] -> [DEPENDS_ON] -> [DataClass('LocationOut')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('ItemSummary')]
|
* @summary DTO для краткой информации о вещи в списках (GET /v1/items).
|
||||||
* [PURPOSE] DTO для краткой информации о вещи в списках (GET /v1/items).
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemSummary(
|
data class ItemSummary(
|
||||||
@@ -36,10 +41,11 @@ data class ItemSummary(
|
|||||||
@Json(name = "location") val location: LocationOut?,
|
@Json(name = "location") val location: LocationOut?,
|
||||||
@Json(name = "createdAt") val createdAt: String?
|
@Json(name = "createdAt") val createdAt: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemSummary')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('ItemCreate')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('ItemCreate')]
|
* @summary DTO для создания новой вещи (POST /v1/items).
|
||||||
* [PURPOSE] DTO для создания новой вещи (POST /v1/items).
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemCreate(
|
data class ItemCreate(
|
||||||
@@ -49,10 +55,11 @@ data class ItemCreate(
|
|||||||
@Json(name = "labelIds") val labelIds: List<String>?,
|
@Json(name = "labelIds") val labelIds: List<String>?,
|
||||||
@Json(name = "value") val value: BigDecimal?
|
@Json(name = "value") val value: BigDecimal?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemCreate')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('ItemUpdate')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('ItemUpdate')]
|
* @summary DTO для обновления вещи (PUT /v1/items/{id}).
|
||||||
* [PURPOSE] DTO для обновления вещи (PUT /v1/items/{id}).
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemUpdate(
|
data class ItemUpdate(
|
||||||
@@ -62,5 +69,6 @@ data class ItemUpdate(
|
|||||||
@Json(name = "labelIds") val labelIds: List<String>?,
|
@Json(name = "labelIds") val labelIds: List<String>?,
|
||||||
@Json(name = "value") val value: BigDecimal?
|
@Json(name = "value") val value: BigDecimal?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemUpdate')]
|
||||||
|
|
||||||
// [END_FILE_ItemDto.kt]
|
// [END_FILE_ItemDto.kt]
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.ItemOut
|
import com.homebox.lens.domain.model.ItemOut
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('ItemOutDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для полной модели вещи.
|
||||||
* DTO для полной модели вещи.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemOutDto(
|
data class ItemOutDto(
|
||||||
@@ -39,10 +39,12 @@ data class ItemOutDto(
|
|||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemOutDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из ItemOutDto в доменную модель ItemOut.
|
||||||
* Маппер из ItemOutDto в доменную модель ItemOut.
|
|
||||||
*/
|
*/
|
||||||
fun ItemOutDto.toDomain(): ItemOut {
|
fun ItemOutDto.toDomain(): ItemOut {
|
||||||
return ItemOut(
|
return ItemOut(
|
||||||
@@ -70,3 +72,4 @@ fun ItemOutDto.toDomain(): ItemOut {
|
|||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.ItemSummary
|
import com.homebox.lens.domain.model.ItemSummary
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('ItemSummaryDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для сокращенной модели вещи.
|
||||||
* DTO для сокращенной модели вещи.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemSummaryDto(
|
data class ItemSummaryDto(
|
||||||
@@ -27,10 +27,12 @@ data class ItemSummaryDto(
|
|||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemSummaryDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
||||||
* Маппер из ItemSummaryDto в доменную модель ItemSummary.
|
|
||||||
*/
|
*/
|
||||||
fun ItemSummaryDto.toDomain(): ItemSummary {
|
fun ItemSummaryDto.toDomain(): ItemSummary {
|
||||||
return ItemSummary(
|
return ItemSummary(
|
||||||
@@ -46,3 +48,4 @@ fun ItemSummaryDto.toDomain(): ItemSummary {
|
|||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.ItemUpdate
|
import com.homebox.lens.domain.model.ItemUpdate
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('ItemUpdateDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для обновления вещи.
|
||||||
* DTO для обновления вещи.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class ItemUpdateDto(
|
data class ItemUpdateDto(
|
||||||
@@ -31,10 +31,12 @@ data class ItemUpdateDto(
|
|||||||
@Json(name = "parentId") val parentId: String?,
|
@Json(name = "parentId") val parentId: String?,
|
||||||
@Json(name = "labelIds") val labelIds: List<String>?
|
@Json(name = "labelIds") val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemUpdateDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDto')]
|
||||||
|
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('ItemUpdateDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
||||||
* Маппер из доменной модели ItemUpdate в ItemUpdateDto.
|
|
||||||
*/
|
*/
|
||||||
fun ItemUpdate.toDto(): ItemUpdateDto {
|
fun ItemUpdate.toDto(): ItemUpdateDto {
|
||||||
return ItemUpdateDto(
|
return ItemUpdateDto(
|
||||||
@@ -54,3 +56,4 @@ fun ItemUpdate.toDto(): ItemUpdateDto {
|
|||||||
labelIds = this.labelIds
|
labelIds = this.labelIds
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDto')]
|
||||||
@@ -3,21 +3,23 @@
|
|||||||
// [SEMANTICS] data_transfer_object, label, create, api
|
// [SEMANTICS] data_transfer_object, label, create, api
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('LabelCreateDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для тела запроса на создание метки (POST /v1/labels).
|
||||||
* DTO для тела запроса на создание метки (POST /v1/labels).
|
* @param name Название метки.
|
||||||
* @property name Название метки.
|
* @param color Цвет метки в формате HEX (например, "#FF0000").
|
||||||
* @property color Цвет метки в формате HEX (например, "#FF0000").
|
* @param description Описание метки.
|
||||||
* @property description Описание метки.
|
|
||||||
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelCreate` из OpenAPI.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LabelCreateDto(
|
data class LabelCreateDto(
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "color") val color: String?,
|
@Json(name = "color") val color: String?,
|
||||||
@Json(name = "description") val description: String? = null // Описание не используется в приложении, но может быть в API
|
@Json(name = "description") val description: String? = null // [AI_NOTE]: Описание не используется в приложении, но может быть в API
|
||||||
)
|
)
|
||||||
// [END_FILE_LabelCreateDto.kt]
|
// [END_ENTITY: DataClass('LabelCreateDto')]
|
||||||
|
// [END_FILE_LabelCreateDto.kt]
|
||||||
|
|||||||
@@ -8,44 +8,38 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.LabelOut
|
import com.homebox.lens.domain.model.LabelOut
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('LabelOutDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для метки.
|
||||||
* DTO для метки.
|
|
||||||
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
|
|
||||||
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
|
||||||
* `JsonDataException: Required value 'isArchived' missing`.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LabelOutDto(
|
data class LabelOutDto(
|
||||||
@Json(name = "id") val id: String,
|
@Json(name = "id") val id: String,
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
// [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
|
|
||||||
@Json(name = "color") val color: String?,
|
@Json(name = "color") val color: String?,
|
||||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
|
||||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String,
|
@Json(name = "updatedAt") val updatedAt: String,
|
||||||
// [COHERENCE_NOTE] Поле `description` также может отсутствовать, добавляем его как nullable.
|
|
||||||
@Json(name = "description") val description: String?
|
@Json(name = "description") val description: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LabelOutDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из LabelOutDto в доменную модель LabelOut.
|
||||||
* Маппер из LabelOutDto в доменную модель LabelOut.
|
|
||||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
|
||||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
|
||||||
*/
|
*/
|
||||||
fun LabelOutDto.toDomain(): LabelOut {
|
fun LabelOutDto.toDomain(): LabelOut {
|
||||||
return LabelOut(
|
return LabelOut(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию.
|
color = this.color ?: "",
|
||||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
isArchived = this.isArchived ?: false,
|
||||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
|
||||||
createdAt = this.createdAt,
|
createdAt = this.createdAt,
|
||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_FILE_LabelOutDto.kt]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
// [END_FILE_LabelOutDto.kt]
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
|
// [SEMANTICS] data_transfer_object, label, summary, api, mapper
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.LabelSummary
|
import com.homebox.lens.domain.model.LabelSummary
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('LabelSummaryDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для ответа от API при создании метки.
|
||||||
* DTO для ответа от API при создании метки.
|
|
||||||
* @coherence_note Структура этого класса точно соответствует схеме `repo.LabelSummary` из OpenAPI.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LabelSummaryDto(
|
data class LabelSummaryDto(
|
||||||
@@ -21,9 +22,11 @@ data class LabelSummaryDto(
|
|||||||
@Json(name = "createdAt") val createdAt: String?,
|
@Json(name = "createdAt") val createdAt: String?,
|
||||||
@Json(name = "updatedAt") val updatedAt: String?
|
@Json(name = "updatedAt") val updatedAt: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LabelSummaryDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelSummary')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* @summary Маппер из DTO в доменную модель.
|
* @summary Маппер из DTO в доменную модель.
|
||||||
* @return Объект доменной модели [LabelSummary].
|
* @return Объект доменной модели [LabelSummary].
|
||||||
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
|
* @sideeffect Отбрасывает поля, ненужные доменному слою (`color`, `description`, etc.),
|
||||||
@@ -35,4 +38,5 @@ fun LabelSummaryDto.toDomain(): LabelSummary {
|
|||||||
name = this.name
|
name = this.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_FILE_LabelSummaryDto.kt]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
// [END_FILE_LabelSummaryDto.kt]
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||||
// [FILE] LocationDto.kt
|
// [FILE] LocationDto.kt
|
||||||
|
// [SEMANTICS] data, dto, api, location
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('LocationOut')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('LocationOut')]
|
* @summary DTO для информации о местоположении.
|
||||||
* [PURPOSE] DTO для информации о местоположении.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LocationOut(
|
data class LocationOut(
|
||||||
@Json(name = "id") val id: String,
|
@Json(name = "id") val id: String,
|
||||||
@Json(name = "name") val name: String
|
@Json(name = "name") val name: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LocationOut')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('LocationOutCount')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('LocationOutCount')]
|
* @summary DTO для информации о местоположении со счетчиком вещей.
|
||||||
* [PURPOSE] DTO для информации о местоположении со счетчиком вещей.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LocationOutCount(
|
data class LocationOutCount(
|
||||||
@@ -27,5 +29,6 @@ data class LocationOutCount(
|
|||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
@Json(name = "itemCount") val itemCount: Int
|
@Json(name = "itemCount") val itemCount: Int
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LocationOutCount')]
|
||||||
|
|
||||||
// [END_FILE_LocationDto.kt]
|
// [END_FILE_LocationDto.kt]
|
||||||
@@ -8,47 +8,40 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.LocationOutCount
|
import com.homebox.lens.domain.model.LocationOutCount
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('LocationOutCountDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для местоположения со счетчиком.
|
||||||
* DTO для местоположения со счетчиком.
|
|
||||||
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
|
|
||||||
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
|
||||||
* `JsonDataException: Required value '...' missing`.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LocationOutCountDto(
|
data class LocationOutCountDto(
|
||||||
@Json(name = "id") val id: String,
|
@Json(name = "id") val id: String,
|
||||||
@Json(name = "name") val name: String,
|
@Json(name = "name") val name: String,
|
||||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
|
||||||
@Json(name = "color") val color: String?,
|
@Json(name = "color") val color: String?,
|
||||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
|
||||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||||
@Json(name = "itemCount") val itemCount: Int,
|
@Json(name = "itemCount") val itemCount: Int,
|
||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String,
|
@Json(name = "updatedAt") val updatedAt: String,
|
||||||
// [COHERENCE_NOTE] Поле `description` также может отсутствовать или быть null,
|
|
||||||
// поэтому его тоже безопасно сделать nullable.
|
|
||||||
@Json(name = "description") val description: String?
|
@Json(name = "description") val description: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LocationOutCountDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOutCount')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
||||||
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
|
||||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
|
||||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
|
||||||
*/
|
*/
|
||||||
fun LocationOutCountDto.toDomain(): LocationOutCount {
|
fun LocationOutCountDto.toDomain(): LocationOutCount {
|
||||||
return LocationOutCount(
|
return LocationOutCount(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null.
|
color = this.color ?: "",
|
||||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
isArchived = this.isArchived ?: false,
|
||||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
|
||||||
itemCount = this.itemCount,
|
itemCount = this.itemCount,
|
||||||
createdAt = this.createdAt,
|
createdAt = this.createdAt,
|
||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// [END_FILE_LocationOutCountDto.kt]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
// [END_FILE_LocationOutCountDto.kt]
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.LocationOut
|
import com.homebox.lens.domain.model.LocationOut
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('LocationOutDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для местоположения.
|
||||||
* DTO для местоположения.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LocationOutDto(
|
data class LocationOutDto(
|
||||||
@@ -23,10 +23,12 @@ data class LocationOutDto(
|
|||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LocationOutDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LocationOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из LocationOutDto в доменную модель LocationOut.
|
||||||
* Маппер из LocationOutDto в доменную модель LocationOut.
|
|
||||||
*/
|
*/
|
||||||
fun LocationOutDto.toDomain(): LocationOut {
|
fun LocationOutDto.toDomain(): LocationOut {
|
||||||
return LocationOut(
|
return LocationOut(
|
||||||
@@ -38,3 +40,4 @@ fun LocationOutDto.toDomain(): LocationOut {
|
|||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||||
// [FILE] LoginFormDto.kt
|
// [FILE] LoginFormDto.kt
|
||||||
|
// [SEMANTICS] data, dto, api, login
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('LoginFormDto')]
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LoginFormDto(
|
data class LoginFormDto(
|
||||||
@Json(name = "username") val username: String,
|
@Json(name = "username") val username: String,
|
||||||
@Json(name = "password") val password: String,
|
@Json(name = "password") val password: String,
|
||||||
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
|
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
|
||||||
)
|
)
|
||||||
// [END_FILE_LoginFormDto.kt]
|
// [END_ENTITY: DataClass('LoginFormDto')]
|
||||||
|
// [END_FILE_LoginFormDto.kt]
|
||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.MaintenanceEntry
|
import com.homebox.lens.domain.model.MaintenanceEntry
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('MaintenanceEntryDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для записи об обслуживании.
|
||||||
* DTO для записи об обслуживании.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class MaintenanceEntryDto(
|
data class MaintenanceEntryDto(
|
||||||
@@ -25,10 +25,12 @@ data class MaintenanceEntryDto(
|
|||||||
@Json(name = "createdAt") val createdAt: String,
|
@Json(name = "createdAt") val createdAt: String,
|
||||||
@Json(name = "updatedAt") val updatedAt: String
|
@Json(name = "updatedAt") val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('MaintenanceEntryDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('MaintenanceEntry')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
|
||||||
* Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry.
|
|
||||||
*/
|
*/
|
||||||
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
||||||
return MaintenanceEntry(
|
return MaintenanceEntry(
|
||||||
@@ -42,3 +44,4 @@ fun MaintenanceEntryDto.toDomain(): MaintenanceEntry {
|
|||||||
updatedAt = this.updatedAt
|
updatedAt = this.updatedAt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||||
// [FILE] PaginationDto.kt
|
// [FILE] PaginationDto.kt
|
||||||
|
// [SEMANTICS] data, dto, api, pagination
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('PaginationResult')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('PaginationResult')]
|
* @summary DTO для пагинированных результатов от API.
|
||||||
* [PURPOSE] DTO для пагинированных результатов от API.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class PaginationResult<T>(
|
data class PaginationResult<T>(
|
||||||
@@ -19,5 +20,6 @@ data class PaginationResult<T>(
|
|||||||
@Json(name = "total") val total: Int,
|
@Json(name = "total") val total: Int,
|
||||||
@Json(name = "pageSize") val pageSize: Int
|
@Json(name = "pageSize") val pageSize: Int
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('PaginationResult')]
|
||||||
|
|
||||||
// [END_FILE_PaginationDto.kt]
|
// [END_FILE_PaginationDto.kt]
|
||||||
@@ -8,11 +8,11 @@ package com.homebox.lens.data.api.dto
|
|||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import com.homebox.lens.domain.model.PaginationResult
|
import com.homebox.lens.domain.model.PaginationResult
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('PaginationResultDto')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary DTO для постраничных результатов.
|
||||||
* DTO для постраничных результатов.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class PaginationResultDto<T>(
|
data class PaginationResultDto<T>(
|
||||||
@@ -21,10 +21,12 @@ data class PaginationResultDto<T>(
|
|||||||
@Json(name = "pageSize") val pageSize: Int,
|
@Json(name = "pageSize") val pageSize: Int,
|
||||||
@Json(name = "total") val total: Int
|
@Json(name = "total") val total: Int
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('PaginationResultDto')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('PaginationResult')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Маппер из PaginationResultDto в доменную модель PaginationResult.
|
||||||
* Маппер из PaginationResultDto в доменную модель PaginationResult.
|
|
||||||
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
|
* @param transform Функция для преобразования каждого элемента из DTO в доменную модель.
|
||||||
*/
|
*/
|
||||||
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
|
fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResult<R> {
|
||||||
@@ -35,3 +37,4 @@ fun <T, R> PaginationResultDto<T>.toDomain(transform: (T) -> R): PaginationResul
|
|||||||
total = this.total
|
total = this.total
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||||
// [FILE] StatisticsDto.kt
|
// [FILE] StatisticsDto.kt
|
||||||
|
// [SEMANTICS] data, dto, api, statistics
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('GroupStatistics')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('GroupStatistics')]
|
* @summary DTO для статистической информации.
|
||||||
* [PURPOSE] DTO для статистической информации.
|
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class GroupStatistics(
|
data class GroupStatistics(
|
||||||
@@ -19,5 +20,6 @@ data class GroupStatistics(
|
|||||||
@Json(name = "locations") val locations: Int,
|
@Json(name = "locations") val locations: Int,
|
||||||
@Json(name = "labels") val labels: Int
|
@Json(name = "labels") val labels: Int
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('GroupStatistics')]
|
||||||
|
|
||||||
// [END_FILE_StatisticsDto.kt]
|
// [END_FILE_StatisticsDto.kt]
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||||
// [FILE] TokenResponseDto.kt
|
// [FILE] TokenResponseDto.kt
|
||||||
|
// [SEMANTICS] data, dto, api, token
|
||||||
package com.homebox.lens.data.api.dto
|
package com.homebox.lens.data.api.dto
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('TokenResponseDto')]
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class TokenResponseDto(
|
data class TokenResponseDto(
|
||||||
@Json(name = "token") val token: String,
|
@Json(name = "token") val token: String,
|
||||||
@Json(name = "attachmentToken") val attachmentToken: String,
|
@Json(name = "attachmentToken") val attachmentToken: String,
|
||||||
@Json(name = "expiresAt") val expiresAt: String
|
@Json(name = "expiresAt") val expiresAt: String
|
||||||
)
|
)
|
||||||
// [END_FILE_TokenResponseDto.kt]
|
// [END_ENTITY: DataClass('TokenResponseDto')]
|
||||||
|
// [END_FILE_TokenResponseDto.kt]
|
||||||
@@ -4,26 +4,27 @@
|
|||||||
|
|
||||||
package com.homebox.lens.data.api.mapper
|
package com.homebox.lens.data.api.mapper
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||||
import com.homebox.lens.domain.model.TokenResponse
|
import com.homebox.lens.domain.model.TokenResponse
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('TokenResponse')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Преобразует DTO-объект токена в доменную модель.
|
||||||
* [HELPER] Преобразует DTO-объект токена в доменную модель.
|
|
||||||
* @receiver [TokenResponseDto] объект из слоя данных.
|
* @receiver [TokenResponseDto] объект из слоя данных.
|
||||||
* @return [TokenResponse] объект для доменного слоя.
|
* @return [TokenResponse] объект для доменного слоя.
|
||||||
* @throws IllegalArgumentException если токен в DTO пустой.
|
* @throws IllegalArgumentException если токен в DTO пустой.
|
||||||
*/
|
*/
|
||||||
fun TokenResponseDto.toDomain(): TokenResponse {
|
fun TokenResponseDto.toDomain(): TokenResponse {
|
||||||
// [PRECONDITION] DTO должен содержать валидные данные для маппинга.
|
require(this.token.isNotBlank()) { "DTO token is blank, cannot map to domain model." }
|
||||||
require(this.token.isNotBlank()) { "[PRECONDITION_FAILED] DTO token is blank, cannot map to domain model." }
|
|
||||||
|
|
||||||
// [ACTION]
|
|
||||||
val domainModel = TokenResponse(token = this.token)
|
val domainModel = TokenResponse(token = this.token)
|
||||||
|
|
||||||
// [POSTCONDITION] Проверяем, что инвариант доменной модели соблюден.
|
check(domainModel.token.isNotBlank()) { "Domain model token is blank after mapping." }
|
||||||
check(domainModel.token.isNotBlank()) { "[POSTCONDITION_FAILED] Domain model token is blank after mapping." }
|
|
||||||
|
|
||||||
return domainModel
|
return domainModel
|
||||||
}
|
}
|
||||||
// [END_FILE_TokenMapper.kt]
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
// [END_FILE_TokenMapper.kt]
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db
|
// [PACKAGE] com.homebox.lens.data.db
|
||||||
// [FILE] Converters.kt
|
// [FILE] Converters.kt
|
||||||
|
// [SEMANTICS] data, database, room, converter
|
||||||
package com.homebox.lens.data.db
|
package com.homebox.lens.data.db
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Class('Converters')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Class('Converters')]
|
* @summary Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
|
||||||
* [PURPOSE] Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию.
|
|
||||||
*/
|
*/
|
||||||
class Converters {
|
class Converters {
|
||||||
|
// [ENTITY: Function('fromString')]
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun fromString(value: String?): BigDecimal? {
|
fun fromString(value: String?): BigDecimal? {
|
||||||
return value?.let { BigDecimal(it) }
|
return value?.let { BigDecimal(it) }
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('fromString')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('bigDecimalToString')]
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
|
fun bigDecimalToString(bigDecimal: BigDecimal?): String? {
|
||||||
return bigDecimal?.toPlainString()
|
return bigDecimal?.toPlainString()
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('bigDecimalToString')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Class('Converters')]
|
||||||
|
|
||||||
// [END_FILE_Converters.kt]
|
// [END_FILE_Converters.kt]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db
|
// [PACKAGE] com.homebox.lens.data.db
|
||||||
// [FILE] HomeboxDatabase.kt
|
// [FILE] HomeboxDatabase.kt
|
||||||
|
// [SEMANTICS] data, database, room
|
||||||
package com.homebox.lens.data.db
|
package com.homebox.lens.data.db
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
@@ -10,11 +11,11 @@ import com.homebox.lens.data.db.dao.ItemDao
|
|||||||
import com.homebox.lens.data.db.dao.LabelDao
|
import com.homebox.lens.data.db.dao.LabelDao
|
||||||
import com.homebox.lens.data.db.dao.LocationDao
|
import com.homebox.lens.data.db.dao.LocationDao
|
||||||
import com.homebox.lens.data.db.entity.*
|
import com.homebox.lens.data.db.entity.*
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Database('HomeboxDatabase')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomDatabase('HomeboxDatabase')]
|
* @summary Основной класс для работы с локальной базой данных Room.
|
||||||
* [PURPOSE] Основной класс для работы с локальной базой данных Room.
|
|
||||||
*/
|
*/
|
||||||
@Database(
|
@Database(
|
||||||
entities = [
|
entities = [
|
||||||
@@ -37,5 +38,6 @@ abstract class HomeboxDatabase : RoomDatabase() {
|
|||||||
const val DATABASE_NAME = "homebox_lens_db"
|
const val DATABASE_NAME = "homebox_lens_db"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Database('HomeboxDatabase')]
|
||||||
|
|
||||||
// [END_FILE_HomeboxDatabase.kt]
|
// [END_FILE_HomeboxDatabase.kt]
|
||||||
@@ -1,45 +1,61 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||||
// [FILE] ItemDao.kt
|
// [FILE] ItemDao.kt
|
||||||
|
// [SEMANTICS] data, database, dao, item
|
||||||
package com.homebox.lens.data.db.dao
|
package com.homebox.lens.data.db.dao
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import com.homebox.lens.data.db.entity.ItemEntity
|
import com.homebox.lens.data.db.entity.ItemEntity
|
||||||
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
|
import com.homebox.lens.data.db.entity.ItemLabelCrossRef
|
||||||
import com.homebox.lens.data.db.entity.ItemWithLabels
|
import com.homebox.lens.data.db.entity.ItemWithLabels
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Interface('ItemDao')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomDao('ItemDao')]
|
* @summary Предоставляет методы для работы с 'items' в локальной БД.
|
||||||
* [PURPOSE] Предоставляет методы для работы с 'items' в локальной БД.
|
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface ItemDao {
|
interface ItemDao {
|
||||||
|
|
||||||
|
// [ENTITY: Function('getRecentlyAddedItems')]
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM items ORDER BY createdAt DESC LIMIT :limit")
|
@Query("SELECT * FROM items ORDER BY createdAt DESC LIMIT :limit")
|
||||||
fun getRecentlyAddedItems(limit: Int): Flow<List<ItemWithLabels>>
|
fun getRecentlyAddedItems(limit: Int): Flow<List<ItemWithLabels>>
|
||||||
|
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getItems')]
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM items")
|
@Query("SELECT * FROM items")
|
||||||
suspend fun getItems(): List<ItemWithLabels>
|
suspend fun getItems(): List<ItemWithLabels>
|
||||||
|
// [END_ENTITY: Function('getItems')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getItem')]
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM items WHERE id = :itemId")
|
@Query("SELECT * FROM items WHERE id = :itemId")
|
||||||
suspend fun getItem(itemId: String): ItemWithLabels?
|
suspend fun getItem(itemId: String): ItemWithLabels?
|
||||||
|
// [END_ENTITY: Function('getItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('insertItems')]
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertItems(items: List<ItemEntity>)
|
suspend fun insertItems(items: List<ItemEntity>)
|
||||||
|
// [END_ENTITY: Function('insertItems')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('insertItem')]
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertItem(item: ItemEntity)
|
suspend fun insertItem(item: ItemEntity)
|
||||||
|
// [END_ENTITY: Function('insertItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('deleteItem')]
|
||||||
@Query("DELETE FROM items WHERE id = :itemId")
|
@Query("DELETE FROM items WHERE id = :itemId")
|
||||||
suspend fun deleteItem(itemId: String)
|
suspend fun deleteItem(itemId: String)
|
||||||
|
// [END_ENTITY: Function('deleteItem')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('insertItemLabelCrossRefs')]
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
|
suspend fun insertItemLabelCrossRefs(crossRefs: List<ItemLabelCrossRef>)
|
||||||
|
// [END_ENTITY: Function('insertItemLabelCrossRefs')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Interface('ItemDao')]
|
||||||
|
|
||||||
// [END_FILE_ItemDao.kt]
|
// [END_FILE_ItemDao.kt]
|
||||||
@@ -1,27 +1,33 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||||
// [FILE] LabelDao.kt
|
// [FILE] LabelDao.kt
|
||||||
|
// [SEMANTICS] data, database, dao, label
|
||||||
package com.homebox.lens.data.db.dao
|
package com.homebox.lens.data.db.dao
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.homebox.lens.data.db.entity.LabelEntity
|
import com.homebox.lens.data.db.entity.LabelEntity
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Interface('LabelDao')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomDao('LabelDao')]
|
* @summary Предоставляет методы для работы с 'labels' в локальной БД.
|
||||||
* [PURPOSE] Предоставляет методы для работы с 'labels' в локальной БД.
|
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface LabelDao {
|
interface LabelDao {
|
||||||
|
|
||||||
|
// [ENTITY: Function('getLabels')]
|
||||||
@Query("SELECT * FROM labels")
|
@Query("SELECT * FROM labels")
|
||||||
suspend fun getLabels(): List<LabelEntity>
|
suspend fun getLabels(): List<LabelEntity>
|
||||||
|
// [END_ENTITY: Function('getLabels')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('insertLabels')]
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertLabels(labels: List<LabelEntity>)
|
suspend fun insertLabels(labels: List<LabelEntity>)
|
||||||
|
// [END_ENTITY: Function('insertLabels')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Interface('LabelDao')]
|
||||||
|
|
||||||
// [END_FILE_LabelDao.kt]
|
// [END_FILE_LabelDao.kt]
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.dao
|
// [PACKAGE] com.homebox.lens.data.db.dao
|
||||||
// [FILE] LocationDao.kt
|
// [FILE] LocationDao.kt
|
||||||
|
// [SEMANTICS] data, database, dao, location
|
||||||
package com.homebox.lens.data.db.dao
|
package com.homebox.lens.data.db.dao
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import com.homebox.lens.data.db.entity.LocationEntity
|
import com.homebox.lens.data.db.entity.LocationEntity
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Interface('LocationDao')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomDao('LocationDao')]
|
* @summary Предоставляет методы для работы с 'locations' в локальной БД.
|
||||||
* [PURPOSE] Предоставляет методы для работы с 'locations' в локальной БД.
|
|
||||||
*/
|
*/
|
||||||
@Dao
|
@Dao
|
||||||
interface LocationDao {
|
interface LocationDao {
|
||||||
|
|
||||||
|
// [ENTITY: Function('getLocations')]
|
||||||
@Query("SELECT * FROM locations")
|
@Query("SELECT * FROM locations")
|
||||||
suspend fun getLocations(): List<LocationEntity>
|
suspend fun getLocations(): List<LocationEntity>
|
||||||
|
// [END_ENTITY: Function('getLocations')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('insertLocations')]
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insertLocations(locations: List<LocationEntity>)
|
suspend fun insertLocations(locations: List<LocationEntity>)
|
||||||
|
// [END_ENTITY: Function('insertLocations')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Interface('LocationDao')]
|
||||||
|
|
||||||
// [END_FILE_LocationDao.kt]
|
// [END_FILE_LocationDao.kt]
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||||
// [FILE] ItemEntity.kt
|
// [FILE] ItemEntity.kt
|
||||||
|
// [SEMANTICS] data, database, entity, item
|
||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DatabaseTable('ItemEntity')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomEntity('ItemEntity')]
|
* @summary Представляет собой строку в таблице 'items' в локальной БД.
|
||||||
* [PURPOSE] Представляет собой строку в таблице 'items' в локальной БД.
|
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "items")
|
@Entity(tableName = "items")
|
||||||
data class ItemEntity(
|
data class ItemEntity(
|
||||||
@@ -22,5 +23,6 @@ data class ItemEntity(
|
|||||||
val value: BigDecimal?,
|
val value: BigDecimal?,
|
||||||
val createdAt: String?
|
val createdAt: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DatabaseTable('ItemEntity')]
|
||||||
|
|
||||||
// [END_FILE_ItemEntity.kt]
|
// [END_FILE_ItemEntity.kt]
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||||
// [FILE] ItemLabelCrossRef.kt
|
// [FILE] ItemLabelCrossRef.kt
|
||||||
|
// [SEMANTICS] data, database, entity, relation
|
||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DatabaseTable('ItemLabelCrossRef')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomEntity('ItemLabelCrossRef')]
|
* @summary Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
|
||||||
* [PURPOSE] Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity.
|
|
||||||
*/
|
*/
|
||||||
@Entity(
|
@Entity(
|
||||||
primaryKeys = ["itemId", "labelId"],
|
primaryKeys = ["itemId", "labelId"],
|
||||||
@@ -19,5 +20,6 @@ data class ItemLabelCrossRef(
|
|||||||
val itemId: String,
|
val itemId: String,
|
||||||
val labelId: String
|
val labelId: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DatabaseTable('ItemLabelCrossRef')]
|
||||||
|
|
||||||
// [END_FILE_ItemLabelCrossRef.kt]
|
// [END_FILE_ItemLabelCrossRef.kt]
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||||
// [FILE] ItemWithLabels.kt
|
// [FILE] ItemWithLabels.kt
|
||||||
|
// [SEMANTICS] data, database, entity, relation
|
||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Junction
|
import androidx.room.Junction
|
||||||
import androidx.room.Relation
|
import androidx.room.Relation
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('ItemWithLabels')]
|
||||||
|
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('ItemEntity')]
|
||||||
|
// [RELATION: DataClass('ItemWithLabels')] -> [DEPENDS_ON] -> [DatabaseTable('LabelEntity')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Pojo('ItemWithLabels')]
|
* @summary POJO для получения ItemEntity вместе со связанными LabelEntity.
|
||||||
* [PURPOSE] POJO для получения ItemEntity вместе со связанными LabelEntity.
|
|
||||||
*/
|
*/
|
||||||
data class ItemWithLabels(
|
data class ItemWithLabels(
|
||||||
@Embedded val item: ItemEntity,
|
@Embedded val item: ItemEntity,
|
||||||
@@ -25,5 +28,6 @@ data class ItemWithLabels(
|
|||||||
)
|
)
|
||||||
val labels: List<LabelEntity>
|
val labels: List<LabelEntity>
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemWithLabels')]
|
||||||
|
|
||||||
// [END_FILE_ItemWithLabels.kt]
|
// [END_FILE_ItemWithLabels.kt]
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||||
// [FILE] LabelEntity.kt
|
// [FILE] LabelEntity.kt
|
||||||
|
// [SEMANTICS] data, database, entity, label
|
||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DatabaseTable('LabelEntity')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomEntity('LabelEntity')]
|
* @summary Представляет собой строку в таблице 'labels' в локальной БД.
|
||||||
* [PURPOSE] Представляет собой строку в таблице 'labels' в локальной БД.
|
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "labels")
|
@Entity(tableName = "labels")
|
||||||
data class LabelEntity(
|
data class LabelEntity(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DatabaseTable('LabelEntity')]
|
||||||
|
|
||||||
// [END_FILE_LabelEntity.kt]
|
// [END_FILE_LabelEntity.kt]
|
||||||
@@ -1,20 +1,22 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||||
// [FILE] LocationEntity.kt
|
// [FILE] LocationEntity.kt
|
||||||
|
// [SEMANTICS] data, database, entity, location
|
||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DatabaseTable('LocationEntity')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: RoomEntity('LocationEntity')]
|
* @summary Представляет собой строку в таблице 'locations' в локальной БД.
|
||||||
* [PURPOSE] Представляет собой строку в таблице 'locations' в локальной БД.
|
|
||||||
*/
|
*/
|
||||||
@Entity(tableName = "locations")
|
@Entity(tableName = "locations")
|
||||||
data class LocationEntity(
|
data class LocationEntity(
|
||||||
@PrimaryKey val id: String,
|
@PrimaryKey val id: String,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DatabaseTable('LocationEntity')]
|
||||||
|
|
||||||
// [END_FILE_LocationEntity.kt]
|
// [END_FILE_LocationEntity.kt]
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.db.entity
|
// [PACKAGE] com.homebox.lens.data.db.entity
|
||||||
// [FILE] Mapper.kt
|
// [FILE] Mapper.kt
|
||||||
|
// [SEMANTICS] data, database, mapper
|
||||||
package com.homebox.lens.data.db.entity
|
package com.homebox.lens.data.db.entity
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.Image
|
import com.homebox.lens.domain.model.Image
|
||||||
import com.homebox.lens.domain.model.ItemSummary
|
import com.homebox.lens.domain.model.ItemSummary
|
||||||
import com.homebox.lens.domain.model.LabelOut
|
import com.homebox.lens.domain.model.LabelOut
|
||||||
import com.homebox.lens.domain.model.LocationOut
|
import com.homebox.lens.domain.model.LocationOut
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
||||||
* Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель).
|
|
||||||
*
|
|
||||||
* [COHERENCE_NOTE] Так как сущности БД содержат только подмножество полей доменной модели,
|
|
||||||
* недостающие поля заполняются значениями по умолчанию (false, 0.0, пустые строки) или null.
|
|
||||||
* Это компромисс для обеспечения компиляции и базовой функциональности.
|
|
||||||
*/
|
*/
|
||||||
fun ItemWithLabels.toDomain(): ItemSummary {
|
fun ItemWithLabels.toDomain(): ItemSummary {
|
||||||
return ItemSummary(
|
return ItemSummary(
|
||||||
id = this.item.id,
|
id = this.item.id,
|
||||||
name = this.item.name,
|
name = this.item.name,
|
||||||
// Предполагаем, что `image` в БД - это URL. Создаем объект Image или null.
|
|
||||||
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
|
image = this.item.image?.let { Image(id = "", path = it, isPrimary = true) },
|
||||||
// `location` в ItemEntity - это только ID. Создаем базовый LocationOut.
|
|
||||||
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
|
location = this.item.locationId?.let { LocationOut(id = it, name = "", color = "", isArchived = false, createdAt = "", updatedAt = "") },
|
||||||
labels = this.labels.map { it.toDomain() },
|
labels = this.labels.map { it.toDomain() },
|
||||||
// Заполняем недостающие поля значениями по умолчанию.
|
|
||||||
assetId = null,
|
assetId = null,
|
||||||
isArchived = false,
|
isArchived = false,
|
||||||
value = this.item.value?.toDouble() ?: 0.0,
|
value = this.item.value?.toDouble() ?: 0.0,
|
||||||
@@ -33,21 +29,21 @@ fun ItemWithLabels.toDomain(): ItemSummary {
|
|||||||
updatedAt = ""
|
updatedAt = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('toDomain')]
|
||||||
|
// [RELATION: Function('toDomain')] -> [RETURNS] -> [DataClass('LabelOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
|
||||||
* Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель).
|
|
||||||
*
|
|
||||||
* [COHERENCE_NOTE] Заполняет недостающие поля значениями по умолчанию.
|
|
||||||
*/
|
*/
|
||||||
fun LabelEntity.toDomain(): LabelOut {
|
fun LabelEntity.toDomain(): LabelOut {
|
||||||
return LabelOut(
|
return LabelOut(
|
||||||
id = this.id,
|
id = this.id,
|
||||||
name = this.name,
|
name = this.name,
|
||||||
// Заполняем недостающие поля значениями по умолчанию.
|
color = "#CCCCCC",
|
||||||
color = "#CCCCCC", // Серый цвет по умолчанию
|
|
||||||
isArchived = false,
|
isArchived = false,
|
||||||
createdAt = "",
|
createdAt = "",
|
||||||
updatedAt = ""
|
updatedAt = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDomain')]
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.di
|
// [PACKAGE] com.homebox.lens.data.di
|
||||||
// [FILE] ApiModule.kt
|
// [FILE] ApiModule.kt
|
||||||
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService.
|
// [SEMANTICS] di, hilt, networking
|
||||||
package com.homebox.lens.data.di
|
package com.homebox.lens.data.di
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.api.HomeboxApiService
|
import com.homebox.lens.data.api.HomeboxApiService
|
||||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||||
@@ -17,41 +18,34 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Module('ApiModule')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Module('ApiModule')]
|
* @summary Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
||||||
* [CONTRACT]
|
|
||||||
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
|
||||||
* необходимых для сетевого взаимодействия.
|
* необходимых для сетевого взаимодействия.
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object ApiModule {
|
object ApiModule {
|
||||||
|
|
||||||
// [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
|
|
||||||
private const val BASE_URL = "https://homebox.bebesh.ru/api/"
|
private const val BASE_URL = "https://homebox.bebesh.ru/api/"
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('provideOkHttpClient')]
|
||||||
* [PROVIDER]
|
// [RELATION: Function('provideOkHttpClient')] -> [PROVIDES] -> [Framework('OkHttpClient')]
|
||||||
* [CONTRACT]
|
|
||||||
* Предоставляет сконфигурированный OkHttpClient.
|
|
||||||
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
|
|
||||||
* Используется Provider<T> для предотвращения циклов зависимостей.
|
|
||||||
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
|
|
||||||
*/
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideOkHttpClient(
|
fun provideOkHttpClient(
|
||||||
credentialsRepositoryProvider: Provider<CredentialsRepository>
|
credentialsRepositoryProvider: Provider<CredentialsRepository>
|
||||||
): OkHttpClient {
|
): OkHttpClient {
|
||||||
// [ACTION] Создаем перехватчик для логирования.
|
Timber.d("[DEBUG][PROVIDER][providing_okhttp_client] Providing OkHttpClient.")
|
||||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
level = HttpLoggingInterceptor.Level.BODY
|
||||||
}
|
}
|
||||||
|
|
||||||
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
|
|
||||||
val acceptHeaderInterceptor = Interceptor { chain ->
|
val acceptHeaderInterceptor = Interceptor { chain ->
|
||||||
val request = chain.request().newBuilder()
|
val request = chain.request().newBuilder()
|
||||||
.header("Accept", "application/json")
|
.header("Accept", "application/json")
|
||||||
@@ -59,77 +53,71 @@ object ApiModule {
|
|||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
|
|
||||||
val authInterceptor = Interceptor { chain ->
|
val authInterceptor = Interceptor { chain ->
|
||||||
// [HELPER] Получаем токен из репозитория.
|
|
||||||
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
|
|
||||||
// а интерфейс Interceptor'а является синхронным.
|
|
||||||
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
|
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
|
||||||
|
|
||||||
val requestBuilder = chain.request().newBuilder()
|
val requestBuilder = chain.request().newBuilder()
|
||||||
|
|
||||||
// [ACTION] Если токен существует, добавляем его в заголовок.
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
// Сервер ожидает заголовок "Authorization: Bearer <token>"
|
|
||||||
// Предполагается, что `token` уже содержит префикс "Bearer ".
|
|
||||||
requestBuilder.addHeader("Authorization", token)
|
requestBuilder.addHeader("Authorization", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.proceed(requestBuilder.build())
|
chain.proceed(requestBuilder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
|
|
||||||
return OkHttpClient.Builder()
|
return OkHttpClient.Builder()
|
||||||
.addInterceptor(acceptHeaderInterceptor)
|
.addInterceptor(acceptHeaderInterceptor)
|
||||||
.addInterceptor(authInterceptor) // Добавляем перехватчик для токена
|
.addInterceptor(authInterceptor)
|
||||||
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос
|
.addInterceptor(loggingInterceptor)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideOkHttpClient')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('provideMoshi')]
|
||||||
* [PROVIDER]
|
// [RELATION: Function('provideMoshi')] -> [PROVIDES] -> [Framework('Moshi')]
|
||||||
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
|
|
||||||
*/
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideMoshi(): Moshi {
|
fun provideMoshi(): Moshi {
|
||||||
|
Timber.d("[DEBUG][PROVIDER][providing_moshi] Providing Moshi.")
|
||||||
return Moshi.Builder()
|
return Moshi.Builder()
|
||||||
.add(KotlinJsonAdapterFactory())
|
.add(KotlinJsonAdapterFactory())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideMoshi')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('provideMoshiConverterFactory')]
|
||||||
* [PROVIDER]
|
// [RELATION: Function('provideMoshiConverterFactory')] -> [PROVIDES] -> [Framework('MoshiConverterFactory')]
|
||||||
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
|
|
||||||
*/
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
|
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
|
||||||
|
Timber.d("[DEBUG][PROVIDER][providing_moshi_converter] Providing MoshiConverterFactory.")
|
||||||
return MoshiConverterFactory.create(moshi)
|
return MoshiConverterFactory.create(moshi)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideMoshiConverterFactory')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('provideRetrofit')]
|
||||||
* [PROVIDER]
|
// [RELATION: Function('provideRetrofit')] -> [PROVIDES] -> [Framework('Retrofit')]
|
||||||
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
|
|
||||||
*/
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
|
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
|
||||||
|
Timber.d("[DEBUG][PROVIDER][providing_retrofit] Providing Retrofit.")
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(BASE_URL)
|
.baseUrl(BASE_URL)
|
||||||
.client(okHttpClient)
|
.client(okHttpClient)
|
||||||
.addConverterFactory(moshiConverterFactory)
|
.addConverterFactory(moshiConverterFactory)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideRetrofit')]
|
||||||
|
|
||||||
/**
|
// [ENTITY: Function('provideHomeboxApiService')]
|
||||||
* [PROVIDER]
|
// [RELATION: Function('provideHomeboxApiService')] -> [PROVIDES] -> [Interface('HomeboxApiService')]
|
||||||
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
|
|
||||||
*/
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
|
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
|
||||||
|
Timber.d("[DEBUG][PROVIDER][providing_api_service] Providing HomeboxApiService.")
|
||||||
return retrofit.create(HomeboxApiService::class.java)
|
return retrofit.create(HomeboxApiService::class.java)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideHomeboxApiService')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Module('ApiModule')]
|
||||||
// [END_FILE_ApiModule.kt]
|
// [END_FILE_ApiModule.kt]
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.di
|
// [PACKAGE] com.homebox.lens.data.di
|
||||||
// [FILE] DatabaseModule.kt
|
// [FILE] DatabaseModule.kt
|
||||||
|
// [SEMANTICS] di, hilt, database
|
||||||
package com.homebox.lens.data.di
|
package com.homebox.lens.data.di
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import com.homebox.lens.data.db.HomeboxDatabase
|
import com.homebox.lens.data.db.HomeboxDatabase
|
||||||
@@ -11,40 +12,50 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: Module('DatabaseModule')]
|
||||||
/**
|
/**
|
||||||
* [MODULE: DaggerHilt('DatabaseModule')]
|
* @summary Предоставляет зависимости для работы с базой данных Room.
|
||||||
* [PURPOSE] Предоставляет зависимости для работы с базой данных Room.
|
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DatabaseModule {
|
object DatabaseModule {
|
||||||
|
|
||||||
// [PROVIDER]
|
// [ENTITY: Function('provideHomeboxDatabase')]
|
||||||
|
// [RELATION: Function('provideHomeboxDatabase')] -> [PROVIDES] -> [Database('HomeboxDatabase')]
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
|
fun provideHomeboxDatabase(@ApplicationContext context: Context): HomeboxDatabase {
|
||||||
// [ACTION] Build Room database instance
|
Timber.d("[DEBUG][PROVIDER][providing_database] Providing HomeboxDatabase.")
|
||||||
return Room.databaseBuilder(
|
return Room.databaseBuilder(
|
||||||
context,
|
context,
|
||||||
HomeboxDatabase::class.java,
|
HomeboxDatabase::class.java,
|
||||||
HomeboxDatabase.DATABASE_NAME
|
HomeboxDatabase.DATABASE_NAME
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideHomeboxDatabase')]
|
||||||
|
|
||||||
// [PROVIDER]
|
// [ENTITY: Function('provideItemDao')]
|
||||||
|
// [RELATION: Function('provideItemDao')] -> [PROVIDES] -> [Interface('ItemDao')]
|
||||||
@Provides
|
@Provides
|
||||||
fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
|
fun provideItemDao(database: HomeboxDatabase) = database.itemDao()
|
||||||
|
// [END_ENTITY: Function('provideItemDao')]
|
||||||
|
|
||||||
// [PROVIDER]
|
// [ENTITY: Function('provideLabelDao')]
|
||||||
|
// [RELATION: Function('provideLabelDao')] -> [PROVIDES] -> [Interface('LabelDao')]
|
||||||
@Provides
|
@Provides
|
||||||
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
|
fun provideLabelDao(database: HomeboxDatabase) = database.labelDao()
|
||||||
|
// [END_ENTITY: Function('provideLabelDao')]
|
||||||
|
|
||||||
// [PROVIDER]
|
// [ENTITY: Function('provideLocationDao')]
|
||||||
|
// [RELATION: Function('provideLocationDao')] -> [PROVIDES] -> [Interface('LocationDao')]
|
||||||
@Provides
|
@Provides
|
||||||
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
|
fun provideLocationDao(database: HomeboxDatabase) = database.locationDao()
|
||||||
|
// [END_ENTITY: Function('provideLocationDao')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Module('DatabaseModule')]
|
||||||
|
|
||||||
// [END_FILE_DatabaseModule.kt]
|
// [END_FILE_DatabaseModule.kt]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package com.homebox.lens.data.di
|
package com.homebox.lens.data.di
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
||||||
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
|
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
|
||||||
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
||||||
@@ -15,47 +16,52 @@ import dagger.Module
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Module('RepositoryModule')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Module('RepositoryModule')]
|
* @summary Hilt-модуль для предоставления реализаций репозиториев.
|
||||||
* [CONTRACT]
|
* @description Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
||||||
* Hilt-модуль для предоставления реализаций репозиториев.
|
|
||||||
* Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
|
||||||
*/
|
*/
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
abstract class RepositoryModule {
|
abstract class RepositoryModule {
|
||||||
|
|
||||||
|
// [ENTITY: Function('bindItemRepository')]
|
||||||
|
// [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Связывает интерфейс ItemRepository с его реализацией.
|
||||||
* Связывает интерфейс ItemRepository с его реализацией.
|
|
||||||
*/
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindItemRepository(
|
abstract fun bindItemRepository(
|
||||||
itemRepositoryImpl: ItemRepositoryImpl
|
itemRepositoryImpl: ItemRepositoryImpl
|
||||||
): ItemRepository
|
): ItemRepository
|
||||||
|
// [END_ENTITY: Function('bindItemRepository')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('bindCredentialsRepository')]
|
||||||
|
// [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Связывает интерфейс CredentialsRepository с его реализацией.
|
||||||
* Связывает интерфейс CredentialsRepository с его реализацией.
|
|
||||||
*/
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindCredentialsRepository(
|
abstract fun bindCredentialsRepository(
|
||||||
credentialsRepositoryImpl: CredentialsRepositoryImpl
|
credentialsRepositoryImpl: CredentialsRepositoryImpl
|
||||||
): CredentialsRepository
|
): CredentialsRepository
|
||||||
|
// [END_ENTITY: Function('bindCredentialsRepository')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('bindAuthRepository')]
|
||||||
|
// [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Связывает интерфейс AuthRepository с его реализацией.
|
||||||
* [FIX] Связывает интерфейс AuthRepository с его реализацией.
|
|
||||||
* Это исправляет ошибку "could not be resolved", так как теперь Hilt знает,
|
|
||||||
* какую конкретную реализацию предоставить, когда запрашивается AuthRepository.
|
|
||||||
*/
|
*/
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
abstract fun bindAuthRepository(
|
abstract fun bindAuthRepository(
|
||||||
authRepositoryImpl: AuthRepositoryImpl
|
authRepositoryImpl: AuthRepositoryImpl
|
||||||
): AuthRepository
|
): AuthRepository
|
||||||
|
// [END_ENTITY: Function('bindAuthRepository')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Module('RepositoryModule')]
|
||||||
// [END_FILE_RepositoryModule.kt]
|
// [END_FILE_RepositoryModule.kt]
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.di
|
// [PACKAGE] com.homebox.lens.data.di
|
||||||
// [FILE] StorageModule.kt
|
// [FILE] StorageModule.kt
|
||||||
|
// [SEMANTICS] di, hilt, storage
|
||||||
package com.homebox.lens.data.di
|
package com.homebox.lens.data.di
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
|
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
|
||||||
@@ -12,30 +13,39 @@ import dagger.Provides
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Module('StorageModule')]
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object StorageModule {
|
object StorageModule {
|
||||||
|
|
||||||
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs" // No longer secret
|
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs"
|
||||||
|
|
||||||
// [ACTION] Provide a standard, unencrypted SharedPreferences instance.
|
// [ENTITY: Function('provideSharedPreferences')]
|
||||||
|
// [RELATION: Function('provideSharedPreferences')] -> [PROVIDES] -> [Framework('SharedPreferences')]
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||||
|
Timber.d("[DEBUG][PROVIDER][providing_shared_preferences] Providing SharedPreferences.")
|
||||||
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideSharedPreferences')]
|
||||||
|
|
||||||
// [ACTION] Provide our new EncryptedPreferencesWrapper as the main entry point for secure storage.
|
// [ENTITY: Function('provideEncryptedPreferencesWrapper')]
|
||||||
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor.
|
// [RELATION: Function('provideEncryptedPreferencesWrapper')] -> [PROVIDES] -> [Class('EncryptedPreferencesWrapper')]
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideEncryptedPreferencesWrapper(
|
fun provideEncryptedPreferencesWrapper(
|
||||||
sharedPreferences: SharedPreferences,
|
sharedPreferences: SharedPreferences,
|
||||||
cryptoManager: CryptoManager
|
cryptoManager: CryptoManager
|
||||||
): EncryptedPreferencesWrapper {
|
): EncryptedPreferencesWrapper {
|
||||||
|
Timber.d("[DEBUG][PROVIDER][providing_encrypted_prefs_wrapper] Providing EncryptedPreferencesWrapper.")
|
||||||
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
|
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('provideEncryptedPreferencesWrapper')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Module('StorageModule')]
|
||||||
// [END_FILE_StorageModule.kt]
|
// [END_FILE_StorageModule.kt]
|
||||||
@@ -20,17 +20,20 @@ import kotlinx.coroutines.withContext
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Class('AuthRepositoryImpl')]
|
||||||
|
// [RELATION: Class('AuthRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('AuthRepository')]
|
||||||
|
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||||
|
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('OkHttpClient')]
|
||||||
|
// [RELATION: Class('AuthRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('MoshiConverterFactory')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Class('AuthRepositoryImpl')]
|
* @summary Реализация репозитория для управления аутентификацией.
|
||||||
* [CONTRACT]
|
|
||||||
* Реализация репозитория для управления аутентификацией.
|
|
||||||
* @param encryptedPrefs Защищенное хранилище для токена.
|
* @param encryptedPrefs Защищенное хранилище для токена.
|
||||||
* @param okHttpClient Общий OkHttp клиент для переиспользования.
|
* @param okHttpClient Общий OkHttp клиент для переиспользования.
|
||||||
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
|
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
|
||||||
* [COHERENCE_NOTE] Реализация метода login теперь включает логику создания временного Retrofit-клиента
|
|
||||||
* "на лету", используя URL сервера из credentials. Эта логика была перенесена из ItemRepositoryImpl.
|
|
||||||
*/
|
*/
|
||||||
class AuthRepositoryImpl @Inject constructor(
|
class AuthRepositoryImpl @Inject constructor(
|
||||||
private val encryptedPrefs: SharedPreferences,
|
private val encryptedPrefs: SharedPreferences,
|
||||||
@@ -42,47 +45,53 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [ENTITY: Function('login')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
||||||
* Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
|
||||||
* на указанный пользователем URL сервера.
|
* на указанный пользователем URL сервера.
|
||||||
* @param credentials Учетные данные пользователя, включая URL сервера.
|
* @param credentials Учетные данные пользователя, включая URL сервера.
|
||||||
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
|
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
|
||||||
*/
|
*/
|
||||||
override suspend fun login(credentials: Credentials): Result<TokenResponse> {
|
override suspend fun login(credentials: Credentials): Result<TokenResponse> {
|
||||||
// [PRECONDITION]
|
require(credentials.serverUrl.isNotBlank()) { "Server URL cannot be blank." }
|
||||||
require(credentials.serverUrl.isNotBlank()) { "[PRECONDITION_FAILED] Server URL cannot be blank." }
|
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
// [ACTION] Создаем временный Retrofit клиент с URL, указанным пользователем.
|
Timber.d("[DEBUG][ACTION][creating_retrofit_client] Creating temporary Retrofit client for URL: ${credentials.serverUrl}")
|
||||||
val tempApiService = Retrofit.Builder()
|
val tempApiService = Retrofit.Builder()
|
||||||
.baseUrl(credentials.serverUrl)
|
.baseUrl(credentials.serverUrl)
|
||||||
.client(okHttpClient) // Переиспользуем существующий OkHttp клиент
|
.client(okHttpClient)
|
||||||
.addConverterFactory(moshiConverterFactory) // и конвертер
|
.addConverterFactory(moshiConverterFactory)
|
||||||
.build()
|
.build()
|
||||||
.create(HomeboxApiService::class.java)
|
.create(HomeboxApiService::class.java)
|
||||||
|
|
||||||
// [ACTION] Создаем DTO и выполняем запрос.
|
|
||||||
val loginForm = LoginFormDto(credentials.username, credentials.password)
|
val loginForm = LoginFormDto(credentials.username, credentials.password)
|
||||||
|
Timber.d("[DEBUG][ACTION][performing_login] Performing login request.")
|
||||||
val tokenResponseDto = tempApiService.login(loginForm)
|
val tokenResponseDto = tempApiService.login(loginForm)
|
||||||
|
|
||||||
// [ACTION] Маппим результат в доменную модель.
|
Timber.d("[DEBUG][ACTION][mapping_to_domain] Mapping token response to domain model.")
|
||||||
tokenResponseDto.toDomain()
|
tokenResponseDto.toDomain()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('login')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('saveToken')]
|
||||||
override suspend fun saveToken(token: String) {
|
override suspend fun saveToken(token: String) {
|
||||||
require(token.isNotBlank()) { "[PRECONDITION_FAILED] Token cannot be blank." }
|
require(token.isNotBlank()) { "Token cannot be blank." }
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
|
||||||
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
|
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('saveToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getToken')]
|
||||||
override fun getToken(): Flow<String?> = flow {
|
override fun getToken(): Flow<String?> = flow {
|
||||||
|
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||||
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
|
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
|
||||||
}.flowOn(Dispatchers.IO)
|
}.flowOn(Dispatchers.IO)
|
||||||
|
// [END_ENTITY: Function('getToken')]
|
||||||
}
|
}
|
||||||
// [END_FILE_AuthRepositoryImpl.kt]
|
// [END_ENTITY: Class('AuthRepositoryImpl')]
|
||||||
|
// [END_FILE_AuthRepositoryImpl.kt]
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.repository
|
// [PACKAGE] com.homebox.lens.data.repository
|
||||||
// [FILE] CredentialsRepositoryImpl.kt
|
// [FILE] CredentialsRepositoryImpl.kt
|
||||||
// [PURPOSE] Имплементация репозитория для управления учетными данными и токенами доступа.
|
// [SEMANTICS] data, repository, credentials, security
|
||||||
package com.homebox.lens.data.repository
|
package com.homebox.lens.data.repository
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.homebox.lens.domain.model.Credentials
|
import com.homebox.lens.domain.model.Credentials
|
||||||
@@ -11,13 +12,16 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Class('CredentialsRepositoryImpl')]
|
||||||
|
// [RELATION: Class('CredentialsRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('CredentialsRepository')]
|
||||||
|
// [RELATION: Class('CredentialsRepositoryImpl')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: Class('CredentialsRepositoryImpl')]
|
* @summary Реализует репозиторий для управления учетными данными пользователя.
|
||||||
* [CONTRACT]
|
* @description Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
||||||
* Реализует репозиторий для управления учетными данными пользователя.
|
|
||||||
* Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
|
||||||
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
|
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
|
||||||
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
|
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +29,6 @@ class CredentialsRepositoryImpl @Inject constructor(
|
|||||||
private val encryptedPrefs: SharedPreferences
|
private val encryptedPrefs: SharedPreferences
|
||||||
) : CredentialsRepository {
|
) : CredentialsRepository {
|
||||||
|
|
||||||
// [CONSTANTS_KEYS] Ключи для хранения данных в SharedPreferences.
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_SERVER_URL = "key_server_url"
|
private const val KEY_SERVER_URL = "key_server_url"
|
||||||
private const val KEY_USERNAME = "key_username"
|
private const val KEY_USERNAME = "key_username"
|
||||||
@@ -33,15 +36,15 @@ class CredentialsRepositoryImpl @Inject constructor(
|
|||||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [ENTITY: Function('saveCredentials')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Сохраняет основные учетные данные пользователя.
|
||||||
* Сохраняет основные учетные данные пользователя.
|
|
||||||
* @param credentials Объект с учетными данными для сохранения.
|
* @param credentials Объект с учетными данными для сохранения.
|
||||||
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
|
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
|
||||||
*/
|
*/
|
||||||
override suspend fun saveCredentials(credentials: Credentials) {
|
override suspend fun saveCredentials(credentials: Credentials) {
|
||||||
// [ACTION] Выполняем запись в SharedPreferences в фоновом потоке.
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
Timber.d("[DEBUG][ACTION][saving_credentials] Saving user credentials.")
|
||||||
encryptedPrefs.edit()
|
encryptedPrefs.edit()
|
||||||
.putString(KEY_SERVER_URL, credentials.serverUrl)
|
.putString(KEY_SERVER_URL, credentials.serverUrl)
|
||||||
.putString(KEY_USERNAME, credentials.username)
|
.putString(KEY_USERNAME, credentials.username)
|
||||||
@@ -49,51 +52,57 @@ class CredentialsRepositoryImpl @Inject constructor(
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('saveCredentials')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getCredentials')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Извлекает сохраненные учетные данные пользователя в виде потока.
|
||||||
* Извлекает сохраненные учетные данные пользователя в виде потока.
|
|
||||||
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
|
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
|
||||||
*/
|
*/
|
||||||
override fun getCredentials(): Flow<Credentials?> = flow {
|
override fun getCredentials(): Flow<Credentials?> = flow {
|
||||||
// [CORE-LOGIC] Читаем данные из SharedPreferences.
|
Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.")
|
||||||
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
|
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
|
||||||
val username = encryptedPrefs.getString(KEY_USERNAME, null)
|
val username = encryptedPrefs.getString(KEY_USERNAME, null)
|
||||||
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
|
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
|
||||||
|
|
||||||
// [ACTION] Эммитим результат.
|
|
||||||
if (serverUrl != null && username != null && password != null) {
|
if (serverUrl != null && username != null && password != null) {
|
||||||
|
Timber.d("[DEBUG][SUCCESS][credentials_found] Found and emitting credentials.")
|
||||||
emit(Credentials(serverUrl, username, password))
|
emit(Credentials(serverUrl, username, password))
|
||||||
} else {
|
} else {
|
||||||
|
Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, emitting null.")
|
||||||
emit(null)
|
emit(null)
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.IO) // [ACTION] Указываем, что Flow должен выполняться в фоновом потоке.
|
}.flowOn(Dispatchers.IO)
|
||||||
|
// [END_ENTITY: Function('getCredentials')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('saveToken')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Сохраняет токен авторизации.
|
||||||
* Сохраняет токен авторизации.
|
|
||||||
* @param token Токен для сохранения.
|
* @param token Токен для сохранения.
|
||||||
* @sideeffect Перезаписывает существующий токен в SharedPreferences.
|
* @sideeffect Перезаписывает существующий токен в SharedPreferences.
|
||||||
*/
|
*/
|
||||||
override suspend fun saveToken(token: String) {
|
override suspend fun saveToken(token: String) {
|
||||||
// [ACTION] Выполняем запись токена в фоновом потоке.
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.")
|
||||||
encryptedPrefs.edit()
|
encryptedPrefs.edit()
|
||||||
.putString(KEY_AUTH_TOKEN, token)
|
.putString(KEY_AUTH_TOKEN, token)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('saveToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getToken')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Извлекает сохраненный токен авторизации.
|
||||||
* Извлекает сохраненный токен авторизации.
|
|
||||||
* @return Строка с токеном или null, если он не найден.
|
* @return Строка с токеном или null, если он не найден.
|
||||||
*/
|
*/
|
||||||
override suspend fun getToken(): String? {
|
override suspend fun getToken(): String? {
|
||||||
// [ACTION] Выполняем чтение токена в фоновом потоке.
|
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
|
Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.")
|
||||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('getToken')]
|
||||||
}
|
}
|
||||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
// [END_ENTITY: Class('CredentialsRepositoryImpl')]
|
||||||
|
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.repository
|
// [PACKAGE] com.homebox.lens.data.repository
|
||||||
// [FILE] EncryptedPreferencesWrapper.kt
|
// [FILE] EncryptedPreferencesWrapper.kt
|
||||||
// [PURPOSE] A wrapper around SharedPreferences to provide on-the-fly encryption/decryption.
|
// [SEMANTICS] data, security, preferences
|
||||||
|
|
||||||
package com.homebox.lens.data.repository
|
package com.homebox.lens.data.repository
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import com.homebox.lens.data.security.CryptoManager
|
import com.homebox.lens.data.security.CryptoManager
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Class('EncryptedPreferencesWrapper')]
|
||||||
|
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Framework('SharedPreferences')]
|
||||||
|
// [RELATION: Class('EncryptedPreferencesWrapper')] -> [DEPENDS_ON] -> [Class('CryptoManager')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
||||||
* Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
* @description It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
|
||||||
* It uses a CryptoManager to encrypt/decrypt data before writing/reading from a standard SharedPreferences instance.
|
|
||||||
* @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data.
|
* @param sharedPreferences The underlying standard SharedPreferences instance to store encrypted data.
|
||||||
* @param cryptoManager The manager responsible for all cryptographic operations.
|
* @param cryptoManager The manager responsible for all cryptographic operations.
|
||||||
*/
|
*/
|
||||||
@@ -23,44 +27,58 @@ class EncryptedPreferencesWrapper @Inject constructor(
|
|||||||
private val cryptoManager: CryptoManager
|
private val cryptoManager: CryptoManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
// [ENTITY: Function('getString')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Retrieves a decrypted string value for a given key.
|
||||||
* Retrieves a decrypted string value for a given key.
|
|
||||||
* @param key The key for the preference.
|
* @param key The key for the preference.
|
||||||
* @param defaultValue The value to return if the key is not found or decryption fails.
|
* @param defaultValue The value to return if the key is not found or decryption fails.
|
||||||
* @return The decrypted string, or the defaultValue.
|
* @return The decrypted string, or the defaultValue.
|
||||||
|
* @sideeffect Reads from SharedPreferences.
|
||||||
*/
|
*/
|
||||||
fun getString(key: String, defaultValue: String?): String? {
|
fun getString(key: String, defaultValue: String?): String? {
|
||||||
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue
|
Timber.d("[DEBUG][ENTRYPOINT][getting_string] Attempting to get string for key: %s", key)
|
||||||
|
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue.also {
|
||||||
|
Timber.d("[DEBUG][FALLBACK][no_value_found] No value for key %s, returning default.", key)
|
||||||
|
}
|
||||||
return try {
|
return try {
|
||||||
|
Timber.d("[DEBUG][ACTION][decoding_value] Decoding Base64 value.")
|
||||||
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
|
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
|
||||||
|
Timber.d("[DEBUG][ACTION][decrypting_value] Decrypting value with CryptoManager.")
|
||||||
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
|
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
|
||||||
String(decryptedBytes, Charset.defaultCharset())
|
String(decryptedBytes, Charset.defaultCharset()).also {
|
||||||
|
Timber.d("[DEBUG][SUCCESS][decryption_complete] Successfully decrypted value for key: %s", key)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Log the error, maybe clear the invalid preference
|
Timber.e(e, "[ERROR][EXCEPTION][decryption_failed] Failed to decrypt value for key: %s", key)
|
||||||
defaultValue
|
defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('getString')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('putString')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Encrypts and saves a string value for a given key.
|
||||||
* Encrypts and saves a string value for a given key.
|
|
||||||
* @param key The key for the preference.
|
* @param key The key for the preference.
|
||||||
* @param value The string value to encrypt and save.
|
* @param value The string value to encrypt and save.
|
||||||
* @sideeffect Modifies the underlying SharedPreferences file.
|
* @sideeffect Modifies the underlying SharedPreferences file.
|
||||||
*/
|
*/
|
||||||
fun putString(key: String, value: String) {
|
fun putString(key: String, value: String) {
|
||||||
|
Timber.d("[DEBUG][ENTRYPOINT][putting_string] Attempting to put string for key: %s", key)
|
||||||
try {
|
try {
|
||||||
|
Timber.d("[DEBUG][ACTION][encrypting_value] Encrypting value with CryptoManager.")
|
||||||
val outputStream = ByteArrayOutputStream()
|
val outputStream = ByteArrayOutputStream()
|
||||||
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
|
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
|
||||||
val encryptedBytes = outputStream.toByteArray()
|
val encryptedBytes = outputStream.toByteArray()
|
||||||
|
Timber.d("[DEBUG][ACTION][encoding_value] Encoding encrypted value to Base64.")
|
||||||
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
|
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
|
||||||
|
Timber.d("[DEBUG][ACTION][writing_to_prefs] Writing encrypted value to SharedPreferences.")
|
||||||
sharedPreferences.edit().putString(key, encryptedValue).apply()
|
sharedPreferences.edit().putString(key, encryptedValue).apply()
|
||||||
|
Timber.d("[DEBUG][SUCCESS][encryption_complete] Successfully encrypted and saved value for key: %s", key)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Log the error
|
Timber.e(e, "[ERROR][EXCEPTION][encryption_failed] Failed to encrypt and save value for key: %s", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('putString')]
|
||||||
// [COHERENCE_NOTE] Add other methods like getInt, putInt etc. as needed, following the same pattern.
|
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Class('EncryptedPreferencesWrapper')]
|
||||||
// [END_FILE_EncryptedPreferencesWrapper.kt]
|
// [END_FILE_EncryptedPreferencesWrapper.kt]
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// [FILE] ItemRepositoryImpl.kt
|
// [FILE] ItemRepositoryImpl.kt
|
||||||
// [SEMANTICS] data_repository, implementation, items, labels
|
// [SEMANTICS] data_repository, implementation, items, labels
|
||||||
package com.homebox.lens.data.repository
|
package com.homebox.lens.data.repository
|
||||||
|
|
||||||
// [IMPORTS]
|
// [IMPORTS]
|
||||||
import com.homebox.lens.data.api.HomeboxApiService
|
import com.homebox.lens.data.api.HomeboxApiService
|
||||||
import com.homebox.lens.data.api.dto.LabelCreateDto
|
import com.homebox.lens.data.api.dto.LabelCreateDto
|
||||||
@@ -15,108 +16,112 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
// [CORE-LOGIC]
|
// [END_IMPORTS]
|
||||||
/**
|
|
||||||
[CONTRACT]
|
// [ENTITY: Repository('ItemRepositoryImpl')]
|
||||||
Реализация репозитория для работы с данными о вещах.
|
// [RELATION: Repository('ItemRepositoryImpl')] -> [IMPLEMENTS] -> [Interface('ItemRepository')]
|
||||||
@param apiService Сервис для взаимодействия с Homebox API.
|
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [ApiEndpoint('HomeboxApiService')]
|
||||||
@param itemDao DAO для доступа к локальной базе данных.
|
// [RELATION: Repository('ItemRepositoryImpl')] -> [DEPENDS_ON] -> [DatabaseTable('ItemDao')]
|
||||||
*/
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class ItemRepositoryImpl @Inject constructor(
|
class ItemRepositoryImpl @Inject constructor(
|
||||||
private val apiService: HomeboxApiService,
|
private val apiService: HomeboxApiService,
|
||||||
private val itemDao: ItemDao
|
private val itemDao: ItemDao
|
||||||
) : ItemRepository {
|
) : ItemRepository {
|
||||||
/**
|
|
||||||
[CONTRACT] @see ItemRepository.createItem
|
// [ENTITY: Function('createItem')]
|
||||||
*/
|
// [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')]
|
||||||
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
|
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
|
||||||
val itemDto = newItemData.toDto()
|
val itemDto = newItemData.toDto()
|
||||||
val resultDto = apiService.createItem(itemDto)
|
val resultDto = apiService.createItem(itemDto)
|
||||||
return resultDto.toDomain()
|
return resultDto.toDomain()
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('createItem')]
|
||||||
[CONTRACT] @see ItemRepository.getItemDetails
|
|
||||||
*/
|
// [ENTITY: Function('getItemDetails')]
|
||||||
|
// [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||||
override suspend fun getItemDetails(itemId: String): ItemOut {
|
override suspend fun getItemDetails(itemId: String): ItemOut {
|
||||||
val resultDto = apiService.getItem(itemId)
|
val resultDto = apiService.getItem(itemId)
|
||||||
return resultDto.toDomain()
|
return resultDto.toDomain()
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('getItemDetails')]
|
||||||
[CONTRACT] @see ItemRepository.updateItem
|
|
||||||
*/
|
// [ENTITY: Function('updateItem')]
|
||||||
|
// [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')]
|
||||||
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
|
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
|
||||||
val itemDto = item.toDto()
|
val itemDto = item.toDto()
|
||||||
val resultDto = apiService.updateItem(itemId, itemDto)
|
val resultDto = apiService.updateItem(itemId, itemDto)
|
||||||
return resultDto.toDomain()
|
return resultDto.toDomain()
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('updateItem')]
|
||||||
[CONTRACT] @see ItemRepository.deleteItem
|
|
||||||
*/
|
// [ENTITY: Function('deleteItem')]
|
||||||
override suspend fun deleteItem(itemId: String) {
|
override suspend fun deleteItem(itemId: String) {
|
||||||
apiService.deleteItem(itemId)
|
apiService.deleteItem(itemId)
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('deleteItem')]
|
||||||
[CONTRACT] @see ItemRepository.syncInventory
|
|
||||||
*/
|
// [ENTITY: Function('syncInventory')]
|
||||||
|
// [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
|
||||||
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
|
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
|
||||||
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
|
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
|
||||||
return resultDto.toDomain { it.toDomain() }
|
return resultDto.toDomain { it.toDomain() }
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('syncInventory')]
|
||||||
[CONTRACT] @see ItemRepository.getStatistics
|
|
||||||
*/
|
// [ENTITY: Function('getStatistics')]
|
||||||
|
// [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')]
|
||||||
override suspend fun getStatistics(): GroupStatistics {
|
override suspend fun getStatistics(): GroupStatistics {
|
||||||
val resultDto = apiService.getStatistics()
|
val resultDto = apiService.getStatistics()
|
||||||
return resultDto.toDomain()
|
return resultDto.toDomain()
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('getStatistics')]
|
||||||
[CONTRACT] @see ItemRepository.getAllLocations
|
|
||||||
*/
|
// [ENTITY: Function('getAllLocations')]
|
||||||
|
// [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')]
|
||||||
override suspend fun getAllLocations(): List<LocationOutCount> {
|
override suspend fun getAllLocations(): List<LocationOutCount> {
|
||||||
val resultDto = apiService.getLocations()
|
val resultDto = apiService.getLocations()
|
||||||
return resultDto.map { it.toDomain() }
|
return resultDto.map { it.toDomain() }
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('getAllLocations')]
|
||||||
[CONTRACT] @see ItemRepository.getAllLabels
|
|
||||||
*/
|
// [ENTITY: Function('getAllLabels')]
|
||||||
|
// [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')]
|
||||||
override suspend fun getAllLabels(): List<LabelOut> {
|
override suspend fun getAllLabels(): List<LabelOut> {
|
||||||
val resultDto = apiService.getLabels()
|
val resultDto = apiService.getLabels()
|
||||||
return resultDto.map { it.toDomain() }
|
return resultDto.map { it.toDomain() }
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('getAllLabels')]
|
||||||
[CONTRACT] @see ItemRepository.createLabel
|
|
||||||
*/
|
// [ENTITY: Function('createLabel')]
|
||||||
|
// [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')]
|
||||||
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
|
override suspend fun createLabel(newLabelData: LabelCreate): LabelSummary {
|
||||||
// [DATA-FLOW] Convert domain model to DTO for the API call.
|
|
||||||
val labelCreateDto = newLabelData.toDto()
|
val labelCreateDto = newLabelData.toDto()
|
||||||
// [ACTION] Call the API service.
|
|
||||||
val resultDto = apiService.createLabel(labelCreateDto)
|
val resultDto = apiService.createLabel(labelCreateDto)
|
||||||
// [DATA-FLOW] Convert the resulting DTO back to a domain model.
|
|
||||||
return resultDto.toDomain()
|
return resultDto.toDomain()
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('createLabel')]
|
||||||
[CONTRACT] @see ItemRepository.searchItems
|
|
||||||
*/
|
// [ENTITY: Function('searchItems')]
|
||||||
|
// [RELATION: Function('searchItems')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')]
|
||||||
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
|
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
|
||||||
val resultDto = apiService.getItems(query = query)
|
val resultDto = apiService.getItems(query = query)
|
||||||
return resultDto.toDomain { it.toDomain() }
|
return resultDto.toDomain { it.toDomain() }
|
||||||
}
|
}
|
||||||
/**
|
// [END_ENTITY: Function('searchItems')]
|
||||||
[CONTRACT] @see ItemRepository.getRecentlyAddedItems
|
|
||||||
*/
|
// [ENTITY: Function('getRecentlyAddedItems')]
|
||||||
|
// [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')]
|
||||||
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
override fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> {
|
||||||
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
return itemDao.getRecentlyAddedItems(limit).map { entities ->
|
||||||
entities.map { it.toDomain() }
|
entities.map { it.toDomain() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('getRecentlyAddedItems')]
|
||||||
}
|
}
|
||||||
// [HELPER] Mapper function for LabelCreate
|
// [END_ENTITY: Repository('ItemRepositoryImpl')]
|
||||||
/**
|
|
||||||
[CONTRACT]
|
// [ENTITY: Function('toDto')]
|
||||||
@summary Маппер из доменной модели LabelCreate в DTO LabelCreateDto.
|
// [RELATION: Function('toDto')] -> [RETURNS] -> [DataClass('LabelCreateDto')]
|
||||||
@return DTO-объект [LabelCreateDto].
|
|
||||||
*/
|
|
||||||
private fun LabelCreate.toDto(): LabelCreateDto {
|
private fun LabelCreate.toDto(): LabelCreateDto {
|
||||||
return LabelCreateDto(
|
return LabelCreateDto(
|
||||||
name = this.name,
|
name = this.name,
|
||||||
@@ -124,4 +129,6 @@ private fun LabelCreate.toDto(): LabelCreateDto {
|
|||||||
description = null // Description is not part of the domain model for creation.
|
description = null // Description is not part of the domain model for creation.
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('toDto')]
|
||||||
|
|
||||||
// [END_FILE_ItemRepositoryImpl.kt]
|
// [END_FILE_ItemRepositoryImpl.kt]
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
// [PACKAGE] com.homebox.lens.data.security
|
// [PACKAGE] com.homebox.lens.data.security
|
||||||
// [FILE] CryptoManager.kt
|
// [FILE] CryptoManager.kt
|
||||||
// [PURPOSE] Handles all cryptographic operations using AndroidKeyStore.
|
// [SEMANTICS] data, security, cryptography
|
||||||
|
|
||||||
package com.homebox.lens.data.security
|
package com.homebox.lens.data.security
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.security.keystore.KeyGenParameterSpec
|
import android.security.keystore.KeyGenParameterSpec
|
||||||
import android.security.keystore.KeyProperties
|
import android.security.keystore.KeyProperties
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.security.KeyStore
|
import java.security.KeyStore
|
||||||
@@ -17,11 +18,12 @@ import javax.crypto.SecretKey
|
|||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Class('CryptoManager')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary A manager for handling encryption and decryption using the Android Keystore system.
|
||||||
* A manager for handling encryption and decryption using the Android Keystore system.
|
* @description This class ensures that cryptographic keys are stored securely.
|
||||||
* This class ensures that cryptographic keys are stored securely.
|
|
||||||
* It is designed to be a Singleton provided by Hilt.
|
* It is designed to be a Singleton provided by Hilt.
|
||||||
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
|
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
|
||||||
*/
|
*/
|
||||||
@@ -29,7 +31,6 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class CryptoManager @Inject constructor() {
|
class CryptoManager @Inject constructor() {
|
||||||
|
|
||||||
// [ЯКОРЬ] Настройки для шифрования
|
|
||||||
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
||||||
load(null)
|
load(null)
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,6 @@ class CryptoManager @Inject constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// [CORE-LOGIC] Получение или создание ключа
|
|
||||||
private fun getKey(): SecretKey {
|
private fun getKey(): SecretKey {
|
||||||
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
|
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
|
||||||
return existingKey?.secretKey ?: createKey()
|
return existingKey?.secretKey ?: createKey()
|
||||||
@@ -67,8 +67,15 @@ class CryptoManager @Inject constructor() {
|
|||||||
}.generateKey()
|
}.generateKey()
|
||||||
}
|
}
|
||||||
|
|
||||||
// [ACTION] Шифрование потока данных
|
// [ENTITY: Function('encrypt')]
|
||||||
|
/**
|
||||||
|
* @summary Encrypts a byte array and writes it to an output stream.
|
||||||
|
* @param bytes The byte array to encrypt.
|
||||||
|
* @param outputStream The stream to write the encrypted data to.
|
||||||
|
* @return The encrypted byte array.
|
||||||
|
*/
|
||||||
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
|
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
|
||||||
|
Timber.d("[DEBUG][ACTION][encrypting_data] Encrypting data.")
|
||||||
val cipher = encryptCipher
|
val cipher = encryptCipher
|
||||||
val encryptedBytes = cipher.doFinal(bytes)
|
val encryptedBytes = cipher.doFinal(bytes)
|
||||||
outputStream.use {
|
outputStream.use {
|
||||||
@@ -79,9 +86,16 @@ class CryptoManager @Inject constructor() {
|
|||||||
}
|
}
|
||||||
return encryptedBytes
|
return encryptedBytes
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('encrypt')]
|
||||||
|
|
||||||
// [ACTION] Дешифрование потока данных
|
// [ENTITY: Function('decrypt')]
|
||||||
|
/**
|
||||||
|
* @summary Decrypts a byte array from an input stream.
|
||||||
|
* @param inputStream The stream to read the encrypted data from.
|
||||||
|
* @return The decrypted byte array.
|
||||||
|
*/
|
||||||
fun decrypt(inputStream: InputStream): ByteArray {
|
fun decrypt(inputStream: InputStream): ByteArray {
|
||||||
|
Timber.d("[DEBUG][ACTION][decrypting_data] Decrypting data.")
|
||||||
return inputStream.use {
|
return inputStream.use {
|
||||||
val ivSize = it.read()
|
val ivSize = it.read()
|
||||||
val iv = ByteArray(ivSize)
|
val iv = ByteArray(ivSize)
|
||||||
@@ -94,6 +108,7 @@ class CryptoManager @Inject constructor() {
|
|||||||
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
|
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: Function('decrypt')]
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||||
@@ -103,4 +118,5 @@ class CryptoManager @Inject constructor() {
|
|||||||
private const val ALIAS = "homebox_lens_secret_key"
|
private const val ALIAS = "homebox_lens_secret_key"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_FILE_CryptoManager.kt]
|
// [END_ENTITY: Class('CryptoManager')]
|
||||||
|
// [END_FILE_CryptoManager.kt]
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.model
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
// [FILE] Credentials.kt
|
// [FILE] Credentials.kt
|
||||||
|
// [SEMANTICS] domain, model, credentials
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('Credentials')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Data class to hold server credentials.
|
||||||
* Data class to hold server credentials.
|
* @param serverUrl The URL of the Homebox server.
|
||||||
* @property serverUrl The URL of the Homebox server.
|
* @param username The username for authentication.
|
||||||
* @property username The username for authentication.
|
* @param password The password for authentication.
|
||||||
* @property password The password for authentication.
|
|
||||||
*/
|
*/
|
||||||
data class Credentials(
|
data class Credentials(
|
||||||
val serverUrl: String,
|
val serverUrl: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String
|
val password: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('Credentials')]
|
||||||
// [END_FILE_Credentials.kt]
|
// [END_FILE_Credentials.kt]
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
// [FILE] CustomField.kt
|
// [FILE] CustomField.kt
|
||||||
// [SEMANTICS] data_structure, entity, custom_field
|
// [SEMANTICS] data_structure, entity, custom_field
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('CustomField')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления кастомного поля.
|
||||||
* Модель данных для представления кастомного поля.
|
* @param name Имя поля.
|
||||||
* @property name Имя поля.
|
* @param value Значение поля.
|
||||||
* @property value Значение поля.
|
* @param type Тип поля (например, "text", "number").
|
||||||
* @property type Тип поля (например, "text", "number").
|
|
||||||
*/
|
*/
|
||||||
data class CustomField(
|
data class CustomField(
|
||||||
val name: String,
|
val name: String,
|
||||||
val value: String,
|
val value: String,
|
||||||
val type: String
|
val type: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('CustomField')]
|
||||||
// [END_FILE_CustomField.kt]
|
// [END_FILE_CustomField.kt]
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
// [FILE] GroupStatistics.kt
|
// [FILE] GroupStatistics.kt
|
||||||
// [SEMANTICS] data_structure, statistics
|
// [SEMANTICS] data_structure, statistics
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('GroupStatistics')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления агрегированной статистики.
|
||||||
* Модель данных для представления агрегированной статистики.
|
* @param items Общее количество вещей.
|
||||||
* @property items Общее количество вещей.
|
* @param labels Общее количество меток.
|
||||||
* @property labels Общее количество меток.
|
* @param locations Общее количество местоположений.
|
||||||
* @property locations Общее количество местоположений.
|
* @param totalValue Общая стоимость всех вещей.
|
||||||
* @property totalValue Общая стоимость всех вещей.
|
|
||||||
*/
|
*/
|
||||||
data class GroupStatistics(
|
data class GroupStatistics(
|
||||||
val items: Int,
|
val items: Int,
|
||||||
@@ -17,4 +17,5 @@ data class GroupStatistics(
|
|||||||
val locations: Int,
|
val locations: Int,
|
||||||
val totalValue: Double
|
val totalValue: Double
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('GroupStatistics')]
|
||||||
// [END_FILE_GroupStatistics.kt]
|
// [END_FILE_GroupStatistics.kt]
|
||||||
|
|||||||
@@ -2,17 +2,18 @@
|
|||||||
// [FILE] Image.kt
|
// [FILE] Image.kt
|
||||||
// [SEMANTICS] data_structure, entity, image
|
// [SEMANTICS] data_structure, entity, image
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('Image')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления изображения, привязанного к вещи.
|
||||||
* Модель данных для представления изображения, привязанного к вещи.
|
* @param id Уникальный идентификатор изображения.
|
||||||
* @property id Уникальный идентификатор изображения.
|
* @param path Путь к файлу изображения.
|
||||||
* @property path Путь к файлу изображения.
|
* @param isPrimary Является ли это изображение основным для вещи.
|
||||||
* @property isPrimary Является ли это изображение основным для вещи.
|
|
||||||
*/
|
*/
|
||||||
data class Image(
|
data class Image(
|
||||||
val id: String,
|
val id: String,
|
||||||
val path: String,
|
val path: String,
|
||||||
val isPrimary: Boolean
|
val isPrimary: Boolean
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('Image')]
|
||||||
// [END_FILE_Image.kt]
|
// [END_FILE_Image.kt]
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.model
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
// [FILE] Item.kt
|
// [FILE] Item.kt
|
||||||
|
// [SEMANTICS] domain, model
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('Item')]
|
||||||
|
// [RELATION: DataClass('Item')] -> [DEPENDS_ON] -> [DataClass('Location')]
|
||||||
|
// [RELATION: DataClass('Item')] -> [DEPENDS_ON] -> [DataClass('Label')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('Item')]
|
* @summary Представляет собой вещь в инвентаре.
|
||||||
* [PURPOSE] Представляет собой вещь в инвентаре.
|
* @param id Уникальный идентификатор вещи.
|
||||||
* @property id Уникальный идентификатор вещи.
|
* @param name Название вещи.
|
||||||
* @property name Название вещи.
|
* @param description Описание вещи.
|
||||||
* @property description Описание вещи.
|
* @param image Url изображения.
|
||||||
* @property image Url изображения.
|
* @param location Местоположение вещи.
|
||||||
* @property location Местоположение вещи.
|
* @param labels Список меток, присвоенных вещи.
|
||||||
* @property labels Список меток, присвоенных вещи.
|
* @param value Стоимость вещи.
|
||||||
* @property value Стоимость вещи.
|
* @param createdAt Дата создания.
|
||||||
* @property createdAt Дата создания.
|
|
||||||
*/
|
*/
|
||||||
data class Item(
|
data class Item(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -28,5 +31,6 @@ data class Item(
|
|||||||
val value: BigDecimal?,
|
val value: BigDecimal?,
|
||||||
val createdAt: String?
|
val createdAt: String?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('Item')]
|
||||||
|
|
||||||
// [END_FILE_Item.kt]
|
// [END_FILE_Item.kt]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
// [FILE] ItemAttachment.kt
|
// [FILE] ItemAttachment.kt
|
||||||
// [SEMANTICS] data_structure, entity, attachment
|
// [SEMANTICS] data_structure, entity, attachment
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('ItemAttachment')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления вложения (файла), привязанного к вещи.
|
||||||
* Модель данных для представления вложения (файла), привязанного к вещи.
|
* @param id Уникальный идентификатор вложения.
|
||||||
* @property id Уникальный идентификатор вложения.
|
* @param name Имя файла.
|
||||||
* @property name Имя файла.
|
* @param path Путь к файлу.
|
||||||
* @property path Путь к файлу.
|
* @param type MIME-тип файла.
|
||||||
* @property type MIME-тип файла.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class ItemAttachment(
|
data class ItemAttachment(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -21,4 +21,5 @@ data class ItemAttachment(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemAttachment')]
|
||||||
// [END_FILE_ItemAttachment.kt]
|
// [END_FILE_ItemAttachment.kt]
|
||||||
|
|||||||
@@ -2,23 +2,23 @@
|
|||||||
// [FILE] ItemCreate.kt
|
// [FILE] ItemCreate.kt
|
||||||
// [SEMANTICS] data_structure, entity, input, create
|
// [SEMANTICS] data_structure, entity, input, create
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('ItemCreate')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для создания новой "Вещи".
|
||||||
* Модель данных для создания новой "Вещи".
|
* @param name Название вещи (обязательно).
|
||||||
* @property name Название вещи (обязательно).
|
* @param assetId Идентификатор актива.
|
||||||
* @property assetId Идентификатор актива.
|
* @param description Описание.
|
||||||
* @property description Описание.
|
* @param notes Заметки.
|
||||||
* @property notes Заметки.
|
* @param serialNumber Серийный номер.
|
||||||
* @property serialNumber Серийный номер.
|
* @param quantity Количество.
|
||||||
* @property quantity Количество.
|
* @param value Стоимость.
|
||||||
* @property value Стоимость.
|
* @param purchasePrice Цена покупки.
|
||||||
* @property purchasePrice Цена покупки.
|
* @param purchaseDate Дата покупки.
|
||||||
* @property purchaseDate Дата покупки.
|
* @param warrantyUntil Гарантия до.
|
||||||
* @property warrantyUntil Гарантия до.
|
* @param locationId ID местоположения.
|
||||||
* @property locationId ID местоположения.
|
* @param parentId ID родительской вещи.
|
||||||
* @property parentId ID родительской вещи.
|
* @param labelIds Список ID меток.
|
||||||
* @property labelIds Список ID меток.
|
|
||||||
*/
|
*/
|
||||||
data class ItemCreate(
|
data class ItemCreate(
|
||||||
val name: String,
|
val name: String,
|
||||||
@@ -35,4 +35,5 @@ data class ItemCreate(
|
|||||||
val parentId: String?,
|
val parentId: String?,
|
||||||
val labelIds: List<String>?
|
val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemCreate')]
|
||||||
// [END_FILE_ItemCreate.kt]
|
// [END_FILE_ItemCreate.kt]
|
||||||
|
|||||||
@@ -2,32 +2,32 @@
|
|||||||
// [FILE] ItemOut.kt
|
// [FILE] ItemOut.kt
|
||||||
// [SEMANTICS] data_structure, entity, detailed
|
// [SEMANTICS] data_structure, entity, detailed
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('ItemOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Полная модель данных для представления "Вещи" со всеми полями.
|
||||||
* Полная модель данных для представления "Вещи" со всеми полями.
|
* @param id Уникальный идентификатор.
|
||||||
* @property id Уникальный идентификатор.
|
* @param name Название.
|
||||||
* @property name Название.
|
* @param assetId Идентификатор актива.
|
||||||
* @property assetId Идентификатор актива.
|
* @param description Описание.
|
||||||
* @property description Описание.
|
* @param notes Заметки.
|
||||||
* @property notes Заметки.
|
* @param serialNumber Серийный номер.
|
||||||
* @property serialNumber Серийный номер.
|
* @param quantity Количество.
|
||||||
* @property quantity Количество.
|
* @param isArchived Флаг архивации.
|
||||||
* @property isArchived Флаг архивации.
|
* @param value Стоимость.
|
||||||
* @property value Стоимость.
|
* @param purchasePrice Цена покупки.
|
||||||
* @property purchasePrice Цена покупки.
|
* @param purchaseDate Дата покупки.
|
||||||
* @property purchaseDate Дата покупки.
|
* @param warrantyUntil Гарантия до.
|
||||||
* @property warrantyUntil Гарантия до.
|
* @param location Местоположение.
|
||||||
* @property location Местоположение.
|
* @param parent Родительская вещь (если есть).
|
||||||
* @property parent Родительская вещь (если есть).
|
* @param children Дочерние вещи.
|
||||||
* @property children Дочерние вещи.
|
* @param labels Список меток.
|
||||||
* @property labels Список меток.
|
* @param attachments Список вложений.
|
||||||
* @property attachments Список вложений.
|
* @param images Список изображений.
|
||||||
* @property images Список изображений.
|
* @param fields Список кастомных полей.
|
||||||
* @property fields Список кастомных полей.
|
* @param maintenance Список записей об обслуживании.
|
||||||
* @property maintenance Список записей об обслуживании.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class ItemOut(
|
data class ItemOut(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -53,4 +53,5 @@ data class ItemOut(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemOut')]
|
||||||
// [END_FILE_ItemOut.kt]
|
// [END_FILE_ItemOut.kt]
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
// [FILE] ItemSummary.kt
|
// [FILE] ItemSummary.kt
|
||||||
// [SEMANTICS] data_structure, entity, summary
|
// [SEMANTICS] data_structure, entity, summary
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('ItemSummary')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Сокращенная модель данных для представления "Вещи" в списках.
|
||||||
* Сокращенная модель данных для представления "Вещи" в списках.
|
* @param id Уникальный идентификатор вещи.
|
||||||
* @property id Уникальный идентификатор вещи.
|
* @param name Название вещи.
|
||||||
* @property name Название вещи.
|
* @param assetId Идентификатор актива.
|
||||||
* @property assetId Идентификатор актива.
|
* @param image Основное изображение. Может быть null.
|
||||||
* @property image Основное изображение. Может быть null.
|
* @param isArchived Флаг архивации.
|
||||||
* @property isArchived Флаг архивации.
|
* @param labels Список меток.
|
||||||
* @property labels Список меток.
|
* @param location Местоположение. Может быть null.
|
||||||
* @property location Местоположение. Может быть null.
|
* @param value Стоимость.
|
||||||
* @property value Стоимость.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class ItemSummary(
|
data class ItemSummary(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -29,4 +29,5 @@ data class ItemSummary(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemSummary')]
|
||||||
// [END_FILE_ItemSummary.kt]
|
// [END_FILE_ItemSummary.kt]
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
// [FILE] ItemUpdate.kt
|
// [FILE] ItemUpdate.kt
|
||||||
// [SEMANTICS] data_structure, entity, input, update
|
// [SEMANTICS] data_structure, entity, input, update
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('ItemUpdate')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для обновления существующей "Вещи".
|
||||||
* Модель данных для обновления существующей "Вещи".
|
* @param name Название вещи.
|
||||||
* @property name Название вещи.
|
* @param assetId Идентификатор актива.
|
||||||
* @property assetId Идентификатор актива.
|
* @param description Описание.
|
||||||
* @property description Описание.
|
* @param notes Заметки.
|
||||||
* @property notes Заметки.
|
* @param serialNumber Серийный номер.
|
||||||
* @property serialNumber Серийный номер.
|
* @param quantity Количество.
|
||||||
* @property quantity Количество.
|
* @param isArchived Флаг архивации.
|
||||||
* @property isArchived Флаг архивации.
|
* @param value Стоимость.
|
||||||
* @property value Стоимость.
|
* @param purchasePrice Цена покупки.
|
||||||
* @property purchasePrice Цена покупки.
|
* @param purchaseDate Дата покупки.
|
||||||
* @property purchaseDate Дата покупки.
|
* @param warrantyUntil Гарантия до.
|
||||||
* @property warrantyUntil Гарантия до.
|
* @param locationId ID местоположения.
|
||||||
* @property locationId ID местоположения.
|
* @param parentId ID родительской вещи.
|
||||||
* @property parentId ID родительской вещи.
|
* @param labelIds Список ID меток для полной замены.
|
||||||
* @property labelIds Список ID меток для полной замены.
|
|
||||||
*/
|
*/
|
||||||
data class ItemUpdate(
|
data class ItemUpdate(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
@@ -37,4 +37,5 @@ data class ItemUpdate(
|
|||||||
val parentId: String?,
|
val parentId: String?,
|
||||||
val labelIds: List<String>?
|
val labelIds: List<String>?
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('ItemUpdate')]
|
||||||
// [END_FILE_ItemUpdate.kt]
|
// [END_FILE_ItemUpdate.kt]
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.model
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
// [FILE] Label.kt
|
// [FILE] Label.kt
|
||||||
|
// [SEMANTICS] domain, model
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('Label')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('Label')]
|
* @summary Представляет собой метку (тег), которую можно присвоить вещи.
|
||||||
* [PURPOSE] Представляет собой метку (тег), которую можно присвоить вещи.
|
* @param id Уникальный идентификатор метки.
|
||||||
* @property id Уникальный идентификатор метки.
|
* @param name Название метки.
|
||||||
* @property name Название метки.
|
|
||||||
*/
|
*/
|
||||||
data class Label(
|
data class Label(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('Label')]
|
||||||
|
|
||||||
// [END_FILE_Label.kt]
|
// [END_FILE_Label.kt]
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
// [FILE] LabelCreate.kt
|
// [FILE] LabelCreate.kt
|
||||||
// [SEMANTICS] data_structure, contract, label, create
|
// [SEMANTICS] data_structure, contract, label, create
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('LabelCreate')]
|
||||||
/**
|
/**
|
||||||
[CONTRACT]
|
* @summary Модель с данными, необходимыми для создания новой метки.
|
||||||
[ENTITY: DataClass('LabelCreate')]
|
* @param name Название новой метки. Обязательное поле.
|
||||||
@summary Модель с данными, необходимыми для создания новой метки.
|
* @param color Цвет метки в формате HEX. Необязательное поле.
|
||||||
@property name Название новой метки. Обязательное поле.
|
* @invariant name не может быть пустым.
|
||||||
@property color Цвет метки в формате HEX. Необязательное поле.
|
|
||||||
@invariant name не может быть пустым.
|
|
||||||
*/
|
*/
|
||||||
data class LabelCreate(
|
data class LabelCreate(
|
||||||
val name: String,
|
val name: String,
|
||||||
val color: String?
|
val color: String?
|
||||||
)
|
)
|
||||||
// [END_FILE_LabelCreate.kt]
|
// [END_ENTITY: DataClass('LabelCreate')]
|
||||||
|
// [END_FILE_LabelCreate.kt]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
// [FILE] LabelOut.kt
|
// [FILE] LabelOut.kt
|
||||||
// [SEMANTICS] data_structure, entity, label
|
// [SEMANTICS] data_structure, entity, label
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('LabelOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления метки (тега).
|
||||||
* Модель данных для представления метки (тега).
|
* @param id Уникальный идентификатор.
|
||||||
* @property id Уникальный идентификатор.
|
* @param name Название метки.
|
||||||
* @property name Название метки.
|
* @param color Цвет метки в формате HEX (например, "#FF0000").
|
||||||
* @property color Цвет метки в формате HEX (например, "#FF0000").
|
* @param isArchived Флаг, указывающий, заархивирована ли метка.
|
||||||
* @property isArchived Флаг, указывающий, заархивирована ли метка.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class LabelOut(
|
data class LabelOut(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -21,4 +21,5 @@ data class LabelOut(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LabelOut')]
|
||||||
// [END_FILE_LabelOut.kt]
|
// [END_FILE_LabelOut.kt]
|
||||||
|
|||||||
@@ -3,17 +3,15 @@
|
|||||||
// [SEMANTICS] data_structure, entity, label, summary
|
// [SEMANTICS] data_structure, entity, label, summary
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
// [CORE-LOGIC]
|
// [ENTITY: DataClass('LabelSummary')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
|
||||||
* [ENTITY: DataClass('LabelSummary')]
|
|
||||||
* @summary Представляет краткую информацию о метке, обычно возвращаемую после создания.
|
* @summary Представляет краткую информацию о метке, обычно возвращаемую после создания.
|
||||||
* @property id Уникальный идентификатор метки.
|
* @param id Уникальный идентификатор метки.
|
||||||
* @property name Название метки.
|
* @param name Название метки.
|
||||||
* @coherence_note Эта модель соответствует схеме `repo.LabelSummary` из спецификации API.
|
|
||||||
*/
|
*/
|
||||||
data class LabelSummary(
|
data class LabelSummary(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
// [END_FILE_LabelSummary.kt]
|
// [END_ENTITY: DataClass('LabelSummary')]
|
||||||
|
// [END_FILE_LabelSummary.kt]
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.model
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
// [FILE] Location.kt
|
// [FILE] Location.kt
|
||||||
|
// [SEMANTICS] domain, model
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('Location')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('Location')]
|
* @summary Представляет собой местоположение, где может находиться вещь.
|
||||||
* [PURPOSE] Представляет собой местоположение, где может находиться вещь.
|
* @param id Уникальный идентификатор местоположения.
|
||||||
* @property id Уникальный идентификатор местоположения.
|
* @param name Название местоположения.
|
||||||
* @property name Название местоположения.
|
|
||||||
*/
|
*/
|
||||||
data class Location(
|
data class Location(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String
|
val name: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('Location')]
|
||||||
|
|
||||||
// [END_FILE_Location.kt]
|
// [END_FILE_Location.kt]
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
// [FILE] LocationOut.kt
|
// [FILE] LocationOut.kt
|
||||||
// [SEMANTICS] data_structure, entity, location
|
// [SEMANTICS] data_structure, entity, location
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('LocationOut')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления местоположения (без счетчика).
|
||||||
* Модель данных для представления местоположения (без счетчика).
|
* @param id Уникальный идентификатор.
|
||||||
* @property id Уникальный идентификатор.
|
* @param name Название местоположения.
|
||||||
* @property name Название местоположения.
|
* @param color Цвет в формате HEX.
|
||||||
* @property color Цвет в формате HEX.
|
* @param isArchived Флаг архивации.
|
||||||
* @property isArchived Флаг архивации.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class LocationOut(
|
data class LocationOut(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -21,4 +21,5 @@ data class LocationOut(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LocationOut')]
|
||||||
// [END_FILE_LocationOut.kt]
|
// [END_FILE_LocationOut.kt]
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
// [FILE] LocationOutCount.kt
|
// [FILE] LocationOutCount.kt
|
||||||
// [SEMANTICS] data_structure, entity, location
|
// [SEMANTICS] data_structure, entity, location
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('LocationOutCount')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для представления местоположения со счетчиком вещей.
|
||||||
* Модель данных для представления местоположения со счетчиком вещей.
|
* @param id Уникальный идентификатор.
|
||||||
* @property id Уникальный идентификатор.
|
* @param name Название местоположения.
|
||||||
* @property name Название местоположения.
|
* @param color Цвет в формате HEX.
|
||||||
* @property color Цвет в формате HEX.
|
* @param isArchived Флаг архивации.
|
||||||
* @property isArchived Флаг архивации.
|
* @param itemCount Количество вещей в данном местоположении.
|
||||||
* @property itemCount Количество вещей в данном местоположении.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class LocationOutCount(
|
data class LocationOutCount(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -23,4 +23,5 @@ data class LocationOutCount(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('LocationOutCount')]
|
||||||
// [END_FILE_LocationOutCount.kt]
|
// [END_FILE_LocationOutCount.kt]
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
// [FILE] MaintenanceEntry.kt
|
// [FILE] MaintenanceEntry.kt
|
||||||
// [SEMANTICS] data_structure, entity, maintenance
|
// [SEMANTICS] data_structure, entity, maintenance
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('MaintenanceEntry')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Модель данных для записи о техническом обслуживании.
|
||||||
* Модель данных для записи о техническом обслуживании.
|
* @param id Уникальный идентификатор записи.
|
||||||
* @property id Уникальный идентификатор записи.
|
* @param itemId ID связанной вещи.
|
||||||
* @property itemId ID связанной вещи.
|
* @param title Заголовок.
|
||||||
* @property title Заголовок.
|
* @param details Детальное описание.
|
||||||
* @property details Детальное описание.
|
* @param dueAt Дата, до которой нужно выполнить.
|
||||||
* @property dueAt Дата, до которой нужно выполнить.
|
* @param completedAt Дата выполнения.
|
||||||
* @property completedAt Дата выполнения.
|
* @param createdAt Дата и время создания.
|
||||||
* @property createdAt Дата и время создания.
|
* @param updatedAt Дата и время последнего обновления.
|
||||||
* @property updatedAt Дата и время последнего обновления.
|
|
||||||
*/
|
*/
|
||||||
data class MaintenanceEntry(
|
data class MaintenanceEntry(
|
||||||
val id: String,
|
val id: String,
|
||||||
@@ -25,4 +25,5 @@ data class MaintenanceEntry(
|
|||||||
val createdAt: String,
|
val createdAt: String,
|
||||||
val updatedAt: String
|
val updatedAt: String
|
||||||
)
|
)
|
||||||
// [END_FILE_MaintenanceEntry.kt]
|
// [END_ENTITY: DataClass('MaintenanceEntry')]
|
||||||
|
// [END_FILE_MaintenanceEntry.kt]
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
// [FILE] PaginationResult.kt
|
// [FILE] PaginationResult.kt
|
||||||
// [SEMANTICS] data_structure, generic, pagination
|
// [SEMANTICS] data_structure, generic, pagination
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
// [CORE-LOGIC]
|
|
||||||
|
// [ENTITY: DataClass('PaginationResult')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Генерик-класс для представления постраничных результатов от API.
|
||||||
* Генерик-класс для представления постраничных результатов от API.
|
|
||||||
* @param T Тип элементов в списке.
|
* @param T Тип элементов в списке.
|
||||||
* @property items Список элементов на текущей странице.
|
* @param items Список элементов на текущей странице.
|
||||||
* @property page Номер текущей страницы.
|
* @param page Номер текущей страницы.
|
||||||
* @property pageSize Количество элементов на странице.
|
* @param pageSize Количество элементов на странице.
|
||||||
* @property total Общее количество элементов.
|
* @param total Общее количество элементов.
|
||||||
*/
|
*/
|
||||||
data class PaginationResult<T>(
|
data class PaginationResult<T>(
|
||||||
val items: List<T>,
|
val items: List<T>,
|
||||||
@@ -18,4 +18,5 @@ data class PaginationResult<T>(
|
|||||||
val pageSize: Int,
|
val pageSize: Int,
|
||||||
val total: Int
|
val total: Int
|
||||||
)
|
)
|
||||||
// [END_FILE_PaginationResult.kt]
|
// [END_ENTITY: DataClass('PaginationResult')]
|
||||||
|
// [END_FILE_PaginationResult.kt]
|
||||||
@@ -1,28 +1,29 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.model
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
// [FILE] Result.kt
|
// [FILE] Result.kt
|
||||||
|
// [SEMANTICS] domain, model, result
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: SealedClass('Result')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: SealedClass('Result')]
|
* @summary Представляет собой результат операции, который может быть либо успешным, либо неуспешным.
|
||||||
* [PURPOSE] Представляет собой результат операции, который может быть либо успешным, либо неуспешным.
|
|
||||||
* @param T Тип данных в случае успеха.
|
* @param T Тип данных в случае успеха.
|
||||||
*/
|
*/
|
||||||
sealed class Result<out T> {
|
sealed class Result<out T> {
|
||||||
|
// [ENTITY: DataClass('Success')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('Success')]
|
* @summary Представляет собой успешный результат операции.
|
||||||
* [PURPOSE] Представляет собой успешный результат операции.
|
|
||||||
* @param data Данные, полученные в результате операции.
|
* @param data Данные, полученные в результате операции.
|
||||||
*/
|
*/
|
||||||
data class Success<out T>(val data: T) : Result<T>()
|
data class Success<out T>(val data: T) : Result<T>()
|
||||||
|
// [END_ENTITY: DataClass('Success')]
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('Error')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('Error')]
|
* @summary Представляет собой неуспешный результат операции.
|
||||||
* [PURPOSE] Представляет собой неуспешный результат операции.
|
|
||||||
* @param exception Исключение, которое произошло во время операции.
|
* @param exception Исключение, которое произошло во время операции.
|
||||||
*/
|
*/
|
||||||
data class Error(val exception: Exception) : Result<Nothing>()
|
data class Error(val exception: Exception) : Result<Nothing>()
|
||||||
|
// [END_ENTITY: DataClass('Error')]
|
||||||
}
|
}
|
||||||
|
// [END_ENTITY: SealedClass('Result')]
|
||||||
// [END_FILE_Result.kt]
|
// [END_FILE_Result.kt]
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.model
|
// [PACKAGE] com.homebox.lens.domain.model
|
||||||
// [FILE] Statistics.kt
|
// [FILE] Statistics.kt
|
||||||
|
// [SEMANTICS] domain, model
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
// [CONTRACT]
|
// [ENTITY: DataClass('Statistics')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('Statistics')]
|
* @summary Представляет собой статистику по инвентарю.
|
||||||
* [PURPOSE] Представляет собой статистику по инвентарю.
|
* @param totalValue Общая стоимость всех вещей.
|
||||||
* @property totalValue Общая стоимость всех вещей.
|
* @param totalItems Общее количество вещей.
|
||||||
* @property totalItems Общее количество вещей.
|
* @param locations Общее количество местоположений.
|
||||||
* @property locations Общее количество местоположений.
|
* @param labels Общее количество меток.
|
||||||
* @property labels Общее количество меток.
|
|
||||||
*/
|
*/
|
||||||
data class Statistics(
|
data class Statistics(
|
||||||
val totalValue: BigDecimal,
|
val totalValue: BigDecimal,
|
||||||
@@ -20,5 +21,6 @@ data class Statistics(
|
|||||||
val locations: Int,
|
val locations: Int,
|
||||||
val labels: Int
|
val labels: Int
|
||||||
)
|
)
|
||||||
|
// [END_ENTITY: DataClass('Statistics')]
|
||||||
|
|
||||||
// [END_FILE_Statistics.kt]
|
// [END_FILE_Statistics.kt]
|
||||||
|
|||||||
@@ -4,17 +4,16 @@
|
|||||||
|
|
||||||
package com.homebox.lens.domain.model
|
package com.homebox.lens.domain.model
|
||||||
|
|
||||||
|
// [ENTITY: DataClass('TokenResponse')]
|
||||||
/**
|
/**
|
||||||
* [ENTITY: DataClass('TokenResponse')]
|
* @summary Модель данных, представляющая ответ от сервера с токеном аутентификации.
|
||||||
* [CONTRACT]
|
* @param token Строка, содержащая JWT или другой токен доступа.
|
||||||
* Модель данных, представляющая ответ от сервера с токеном аутентификации.
|
|
||||||
* @property token Строка, содержащая JWT или другой токен доступа.
|
|
||||||
* @invariant `token` не должен быть пустым.
|
* @invariant `token` не должен быть пустым.
|
||||||
*/
|
*/
|
||||||
data class TokenResponse(val token: String) {
|
data class TokenResponse(val token: String) {
|
||||||
init {
|
init {
|
||||||
// [INVARIANT_CHECK]
|
require(token.isNotBlank()) { "Token cannot be blank." }
|
||||||
require(token.isNotBlank()) { "[INVARIANT_FAILED] Token cannot be blank." }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// [END_FILE_TokenResponse.kt]
|
// [END_ENTITY: DataClass('TokenResponse')]
|
||||||
|
// [END_FILE_TokenResponse.kt]
|
||||||
|
|||||||
@@ -8,35 +8,39 @@ package com.homebox.lens.domain.repository
|
|||||||
import com.homebox.lens.domain.model.Credentials
|
import com.homebox.lens.domain.model.Credentials
|
||||||
import com.homebox.lens.domain.model.TokenResponse
|
import com.homebox.lens.domain.model.TokenResponse
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Interface('AuthRepository')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Репозиторий для управления аутентификацией.
|
||||||
* Репозиторий для управления аутентификацией.
|
|
||||||
* [COHERENCE_NOTE] Добавлен метод `login` для инкапсуляции логики входа.
|
|
||||||
*/
|
*/
|
||||||
interface AuthRepository {
|
interface AuthRepository {
|
||||||
|
// [ENTITY: Function('login')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Выполняет вход в систему, используя предоставленные учетные данные.
|
||||||
* Выполняет вход в систему, используя предоставленные учетные данные.
|
|
||||||
* @param credentials Учетные данные пользователя (URL сервера, логин, пароль).
|
* @param credentials Учетные данные пользователя (URL сервера, логин, пароль).
|
||||||
* @return [Result] с [TokenResponse] в случае успеха, или с [Exception] в случае ошибки.
|
* @return [Result] с [TokenResponse] в случае успеха, или с [Exception] в случае ошибки.
|
||||||
* @throws IllegalArgumentException если `credentials` невалидны (предусловие).
|
* @throws IllegalArgumentException если `credentials` невалидны (предусловие).
|
||||||
*/
|
*/
|
||||||
suspend fun login(credentials: Credentials): Result<TokenResponse>
|
suspend fun login(credentials: Credentials): Result<TokenResponse>
|
||||||
|
// [END_ENTITY: Function('login')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('saveToken')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Сохраняет токен аутентификации.
|
||||||
* Сохраняет токен аутентификации.
|
|
||||||
* @param token Токен для сохранения.
|
* @param token Токен для сохранения.
|
||||||
* @throws IllegalArgumentException если `token` пустой (предусловие).
|
* @throws IllegalArgumentException если `token` пустой (предусловие).
|
||||||
*/
|
*/
|
||||||
suspend fun saveToken(token: String)
|
suspend fun saveToken(token: String)
|
||||||
|
// [END_ENTITY: Function('saveToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getToken')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Получает токен аутентификации.
|
||||||
* Получает токен аутентификации.
|
|
||||||
* @return [Flow], который эммитит токен в виде строки, или `null`, если токен отсутствует.
|
* @return [Flow], который эммитит токен в виде строки, или `null`, если токен отсутствует.
|
||||||
*/
|
*/
|
||||||
fun getToken(): Flow<String?>
|
fun getToken(): Flow<String?>
|
||||||
|
// [END_ENTITY: Function('getToken')]
|
||||||
}
|
}
|
||||||
// [END_FILE_AuthRepository.kt]
|
// [END_ENTITY: Interface('AuthRepository')]
|
||||||
|
// [END_FILE_AuthRepository.kt]
|
||||||
|
|||||||
@@ -1,44 +1,51 @@
|
|||||||
// [PACKAGE] com.homebox.lens.domain.repository
|
// [PACKAGE] com.homebox.lens.domain.repository
|
||||||
// [FILE] CredentialsRepository.kt
|
// [FILE] CredentialsRepository.kt
|
||||||
|
// [SEMANTICS] domain, repository, credentials
|
||||||
package com.homebox.lens.domain.repository
|
package com.homebox.lens.domain.repository
|
||||||
|
|
||||||
|
// [IMPORTS]
|
||||||
import com.homebox.lens.domain.model.Credentials
|
import com.homebox.lens.domain.model.Credentials
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
// [END_IMPORTS]
|
||||||
|
|
||||||
|
// [ENTITY: Interface('CredentialsRepository')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Repository for managing user credentials and session tokens.
|
||||||
* Repository for managing user credentials and session tokens.
|
|
||||||
*/
|
*/
|
||||||
interface CredentialsRepository {
|
interface CredentialsRepository {
|
||||||
|
// [ENTITY: Function('saveCredentials')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Saves the user's base credentials (URL, username, password) securely.
|
||||||
* Saves the user's base credentials (URL, username, password) securely.
|
|
||||||
* @param credentials The credentials to save.
|
* @param credentials The credentials to save.
|
||||||
* @sideeffect Overwrites any existing saved credentials.
|
* @sideeffect Overwrites any existing saved credentials.
|
||||||
*/
|
*/
|
||||||
suspend fun saveCredentials(credentials: Credentials)
|
suspend fun saveCredentials(credentials: Credentials)
|
||||||
|
// [END_ENTITY: Function('saveCredentials')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getCredentials')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Retrieves the saved user credentials.
|
||||||
* Retrieves the saved user credentials.
|
|
||||||
* @return A Flow emitting the saved [Credentials], or null if none are saved.
|
* @return A Flow emitting the saved [Credentials], or null if none are saved.
|
||||||
*/
|
*/
|
||||||
fun getCredentials(): Flow<Credentials?>
|
fun getCredentials(): Flow<Credentials?>
|
||||||
|
// [END_ENTITY: Function('getCredentials')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('saveToken')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Saves the authorization token received after a successful login.
|
||||||
* [ACTION] Saves the authorization token received after a successful login.
|
|
||||||
* @param token The authorization token (including "Bearer " prefix if provided by the server).
|
* @param token The authorization token (including "Bearer " prefix if provided by the server).
|
||||||
* @sideeffect Overwrites any existing saved token.
|
* @sideeffect Overwrites any existing saved token.
|
||||||
*/
|
*/
|
||||||
suspend fun saveToken(token: String)
|
suspend fun saveToken(token: String)
|
||||||
|
// [END_ENTITY: Function('saveToken')]
|
||||||
|
|
||||||
|
// [ENTITY: Function('getToken')]
|
||||||
/**
|
/**
|
||||||
* [CONTRACT]
|
* @summary Retrieves the saved authorization token.
|
||||||
* [ACTION] Retrieves the saved authorization token.
|
|
||||||
* @return The saved token as a String, or null if no token is saved.
|
* @return The saved token as a String, or null if no token is saved.
|
||||||
*/
|
*/
|
||||||
suspend fun getToken(): String?
|
suspend fun getToken(): String?
|
||||||
|
// [END_ENTITY: Function('getToken')]
|
||||||
}
|
}
|
||||||
// [END_FILE_CredentialsRepository.kt]
|
// [END_ENTITY: Interface('CredentialsRepository')]
|
||||||
|
// [END_FILE_CredentialsRepository.kt]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user