feat: Add semantic enrichment to all Kotlin files

This commit is contained in:
2025-08-24 13:46:04 +03:00
parent fbd371b725
commit a608766e06
113 changed files with 1671 additions and 1253 deletions

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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)

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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)

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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')]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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