Compare commits
5 Commits
01e9b7bb00
...
07a8d82a4d
| Author | SHA1 | Date | |
|---|---|---|---|
| 07a8d82a4d | |||
| d9fc689185 | |||
| 2874c3dd67 | |||
| 258deb93d9 | |||
| 2853b5a47e |
@@ -8,6 +8,13 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
|
||||
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
@@ -19,12 +26,36 @@ fun NavGraph() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Dashboard.route
|
||||
startDestination = Screen.Setup.route
|
||||
) {
|
||||
composable(route = Screen.Setup.route) {
|
||||
SetupScreen(onSetupComplete = {
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
popUpTo(Screen.Setup.route) { inclusive = true }
|
||||
}
|
||||
})
|
||||
}
|
||||
composable(route = Screen.Dashboard.route) {
|
||||
DashboardScreen()
|
||||
}
|
||||
// TODO: Добавить остальные экраны в граф навигации
|
||||
composable(route = Screen.InventoryList.route) {
|
||||
InventoryListScreen()
|
||||
}
|
||||
composable(route = Screen.ItemDetails.route) {
|
||||
ItemDetailsScreen()
|
||||
}
|
||||
composable(route = Screen.ItemEdit.route) {
|
||||
ItemEditScreen()
|
||||
}
|
||||
composable(route = Screen.LabelsList.route) {
|
||||
LabelsListScreen()
|
||||
}
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
LocationsListScreen()
|
||||
}
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_NavGraph.kt]
|
||||
@@ -11,14 +11,17 @@ package com.homebox.lens.navigation
|
||||
* @property route Строковый идентификатор маршрута.
|
||||
*/
|
||||
sealed class Screen(val route: String) {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Представляет экран "Дэшборд".
|
||||
*/
|
||||
data object Setup : Screen("setup_screen")
|
||||
data object Dashboard : Screen("dashboard_screen")
|
||||
|
||||
// TODO: Добавить объекты для остальных экранов:
|
||||
// data object ItemDetails : Screen("item_details_screen")
|
||||
// data object Search : Screen("search_screen")
|
||||
data object InventoryList : Screen("inventory_list_screen")
|
||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||
fun createRoute(itemId: String) = "item_details_screen/$itemId"
|
||||
}
|
||||
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
||||
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
|
||||
}
|
||||
data object LabelsList : Screen("labels_list_screen")
|
||||
data object LocationsList : Screen("locations_list_screen")
|
||||
data object Search : Screen("search_screen")
|
||||
}
|
||||
// [END_FILE_Screen.kt]
|
||||
@@ -1,25 +1,32 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt
|
||||
// [SEMANTICS] ui, viewmodel, dashboard, hilt
|
||||
|
||||
// [IMPORTS]
|
||||
// [FILE] DashboardViewModel.kt
|
||||
// [SEMANTICS] ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
|
||||
import com.homebox.lens.domain.usecase.GetStatisticsUseCase
|
||||
import com.homebox.lens.ui.screen.dashboard.DashboardUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber // [FIX] Логирование происходит здесь
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
// [CORE-LOGIC]
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('DashboardViewModel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary ViewModel для главного экрана (Dashboard).
|
||||
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
|
||||
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
|
||||
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
private val getStatisticsUseCase: GetStatisticsUseCase,
|
||||
@@ -27,51 +34,72 @@ class DashboardViewModel @Inject constructor(
|
||||
private val getAllLabelsUseCase: GetAllLabelsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
|
||||
val uiState: StateFlow<DashboardUiState> = _uiState.asStateFlow()
|
||||
// [FIX] Добавлен получатель (receiver) `_uiState` для вызова asStateFlow().
|
||||
// [REASON] `asStateFlow()` является функцией-расширением для `MutableStateFlow` и
|
||||
// должна вызываться на его экземпляре, чтобы создать публичную, неизменяемую версию потока.
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
loadDashboardData()
|
||||
}
|
||||
|
||||
private fun loadDashboardData() {
|
||||
Timber.i("[ACTION] Starting dashboard data load.")
|
||||
_uiState.value = DashboardUiState.Loading
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* @summary Загружает все необходимые данные для экрана Dashboard.
|
||||
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
|
||||
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
|
||||
*/
|
||||
fun loadDashboardData() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// Параллельно запрашиваем все данные
|
||||
val statsDeferred = async { getStatisticsUseCase() }
|
||||
val locationsDeferred = async { getAllLocationsUseCase() }
|
||||
val labelsDeferred = async { getAllLabelsUseCase() }
|
||||
_uiState.value = DashboardUiState.Loading
|
||||
// [FIX] Используем Timber для логирования.
|
||||
Timber.i("[ACTION] Starting parallel dashboard data load. State -> Loading.")
|
||||
|
||||
val stats = statsDeferred.await()
|
||||
val locations = locationsDeferred.await()
|
||||
val labels = labelsDeferred.await()
|
||||
// [CORE-LOGIC: PARALLEL_FETCH]
|
||||
val result = runCatching {
|
||||
coroutineScope {
|
||||
val statsDeferred = async { getStatisticsUseCase() }
|
||||
val locationsDeferred = async { getAllLocationsUseCase() }
|
||||
val labelsDeferred = async { getAllLabelsUseCase() }
|
||||
|
||||
// [ACTION] Логируем результат здесь, во ViewModel
|
||||
if (stats != null && locations != null && labels != null) {
|
||||
val stats = statsDeferred.await()
|
||||
val locations = locationsDeferred.await()
|
||||
val labels = labelsDeferred.await()
|
||||
|
||||
// [POSTCONDITION_CHECK]
|
||||
check(stats != null && locations != null && labels != null) {
|
||||
"[POSTCONDITION_FAILED] One or more dashboard data sources returned null."
|
||||
}
|
||||
Triple(stats, locations, labels)
|
||||
}
|
||||
}
|
||||
|
||||
// [RESULT_HANDLER]
|
||||
result.fold(
|
||||
onSuccess = { (stats, locations, labels) ->
|
||||
// [FIX] Используем Timber для логирования.
|
||||
Timber.i("[SUCCESS] Dashboard data loaded successfully. State -> Success.")
|
||||
_uiState.value = DashboardUiState.Success(
|
||||
statistics = stats,
|
||||
locations = locations,
|
||||
labels = labels
|
||||
)
|
||||
Timber.i("[COHERENCE_CHECK_PASSED] Dashboard data loaded successfully.")
|
||||
} else {
|
||||
// Одна из операций вернула null
|
||||
val errorMessage = "Failed to load dashboard data: " +
|
||||
"stats is ${if(stats==null) "null" else "ok"}, " +
|
||||
"locations is ${if(locations==null) "null" else "ok"}, " +
|
||||
"labels is ${if(labels==null) "null" else "ok"}"
|
||||
Timber.e(errorMessage)
|
||||
_uiState.value = DashboardUiState.Error("Could not load all dashboard data.")
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// [FIX] Используем Timber для логирования ошибок с передачей исключения.
|
||||
Timber.e(exception, "[ERROR] Failed to load dashboard data. State -> Error.")
|
||||
_uiState.value = DashboardUiState.Error(
|
||||
message = exception.message ?: "Could not load dashboard data."
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// [ERROR_HANDLER] Эта ошибка будет отловлена, если сама корутина `launch` упадет
|
||||
Timber.e(e, "[ERROR] Critical failure in loadDashboardData coroutine.")
|
||||
_uiState.value = DashboardUiState.Error(e.message ?: "An unknown critical error occurred")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_DashboardViewModel]
|
||||
}
|
||||
// [END_FILE_DashboardViewModel.kt]
|
||||
@@ -0,0 +1,120 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
// [FIX] Opt-in for experimental Material 3 APIs
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun SetupScreen(
|
||||
viewModel: SetupViewModel = hiltViewModel(),
|
||||
onSetupComplete: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
if (uiState.isSetupComplete) {
|
||||
onSetupComplete()
|
||||
}
|
||||
|
||||
SetupScreenContent(
|
||||
uiState = uiState,
|
||||
onServerUrlChange = viewModel::onServerUrlChange,
|
||||
onUsernameChange = viewModel::onUsernameChange,
|
||||
onPasswordChange = viewModel::onPasswordChange,
|
||||
onConnectClick = viewModel::connect
|
||||
)
|
||||
}
|
||||
|
||||
// [FIX] Opt-in for experimental Material 3 APIs
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
// [CONTENT]
|
||||
@Composable
|
||||
private fun SetupScreenContent(
|
||||
uiState: SetupUiState,
|
||||
onServerUrlChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onConnectClick: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text("Server Setup") })
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text("Server URL") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text("Password") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onConnectClick,
|
||||
enabled = !uiState.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
Text("Connect")
|
||||
}
|
||||
}
|
||||
uiState.error?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [FIX] Opt-in for experimental Material 3 APIs
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SetupScreenPreview() {
|
||||
SetupScreenContent(
|
||||
uiState = SetupUiState(error = "Failed to connect"),
|
||||
onServerUrlChange = {},
|
||||
onUsernameChange = {},
|
||||
onPasswordChange = {},
|
||||
onConnectClick = {}
|
||||
)
|
||||
}
|
||||
// [END_FILE_SetupScreen.kt]
|
||||
@@ -0,0 +1,27 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupUiState.kt
|
||||
// [SEMANTICS] ui_state, data_model, immutable
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
/**
|
||||
* [ENTITY: DataClass('SetupUiState')]
|
||||
* [CONTRACT]
|
||||
* Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen).
|
||||
* Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний.
|
||||
* @property serverUrl URL-адрес сервера Homebox.
|
||||
* @property username Имя пользователя для входа.
|
||||
* @property password Пароль пользователя.
|
||||
* @property isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос.
|
||||
* @property error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки.
|
||||
* @property isSetupComplete Флаг, указывающий на успешное завершение настройки и входа.
|
||||
*/
|
||||
data class SetupUiState(
|
||||
val serverUrl: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSetupComplete: Boolean = false
|
||||
)
|
||||
// [END_FILE_SetupUiState.kt]
|
||||
@@ -0,0 +1,143 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupViewModel.kt
|
||||
// [SEMANTICS] ui_logic, viewmodel, state_management, user_setup, authentication_flow
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
// [IMPORTS]
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.usecase.LoginUseCase
|
||||
import com.homebox.lens.ui.screen.setup.SetupUiState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
// [ENTITY: ViewModel('SetupViewModel')]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* ViewModel для экрана первоначальной настройки (Setup).
|
||||
* Отвечает за:
|
||||
* 1. Загрузку и сохранение учетных данных (URL сервера, логин, пароль).
|
||||
* 2. Управление состоянием UI экрана (`SetupUiState`).
|
||||
* 3. Инициацию процесса входа в систему через `LoginUseCase`.
|
||||
* @property credentialsRepository Репозиторий для операций с учетными данными.
|
||||
* @property loginUseCase Use case для выполнения логики входа.
|
||||
* @invariant Состояние `uiState` всегда является единственным источником истины для UI.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SetupViewModel @Inject constructor(
|
||||
private val credentialsRepository: CredentialsRepository,
|
||||
private val loginUseCase: LoginUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow(SetupUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
// [LIFECYCLE_HANDLER]
|
||||
init {
|
||||
// [ACTION] Загружаем учетные данные при создании ViewModel.
|
||||
loadCredentials()
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [HELPER] Загружает учетные данные из репозитория при инициализации.
|
||||
* @sideeffect Асинхронно обновляет `_uiState` полученными учетными данными.
|
||||
*/
|
||||
private fun loadCredentials() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
// [CORE-LOGIC] Подписываемся на поток учетных данных.
|
||||
credentialsRepository.getCredentials().collect { credentials ->
|
||||
// [ACTION] Обновляем состояние, если учетные данные существуют.
|
||||
if (credentials != null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
serverUrl = credentials.serverUrl,
|
||||
username = credentials.username,
|
||||
password = credentials.password
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет URL сервера в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newUrl Новое значение URL.
|
||||
* @sideeffect Обновляет поле `serverUrl` в `_uiState`.
|
||||
*/
|
||||
fun onServerUrlChange(newUrl: String) {
|
||||
_uiState.update { it.copy(serverUrl = newUrl) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет имя пользователя в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newUsername Новое значение имени пользователя.
|
||||
* @sideeffect Обновляет поле `username` в `_uiState`.
|
||||
*/
|
||||
fun onUsernameChange(newUsername: String) {
|
||||
_uiState.update { it.copy(username = newUsername) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Обновляет пароль в состоянии UI в ответ на ввод пользователя.
|
||||
* @param newPassword Новое значение пароля.
|
||||
* @sideeffect Обновляет поле `password` в `_uiState`.
|
||||
*/
|
||||
fun onPasswordChange(newPassword: String) {
|
||||
_uiState.update { it.copy(password = newPassword) }
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Запускает процесс подключения и входа в систему по действию пользователя.
|
||||
* Выполняет две основные операции:
|
||||
* 1. Сохраняет введенные учетные данные для последующих сессий.
|
||||
* 2. Выполняет вход в систему с использованием этих данных.
|
||||
* @sideeffect Обновляет `_uiState`, управляя флагами `isLoading`, `error` и `isSetupComplete`.
|
||||
* @sideeffect Вызывает `credentialsRepository.saveCredentials`, изменяя сохраненные данные.
|
||||
* @sideeffect Вызывает `loginUseCase`, который, в свою очередь, сохраняет токен.
|
||||
*/
|
||||
fun connect() {
|
||||
// [ENTRYPOINT]
|
||||
viewModelScope.launch {
|
||||
// [ACTION] Устанавливаем состояние загрузки и сбрасываем предыдущую ошибку.
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
|
||||
// [PREPARATION] Готовим данные для операций, очищая их от лишних пробелов.
|
||||
val credentials = Credentials(
|
||||
serverUrl = _uiState.value.serverUrl.trim(),
|
||||
username = _uiState.value.username.trim(),
|
||||
password = _uiState.value.password
|
||||
)
|
||||
|
||||
// [ACTION] Сохраняем учетные данные для будущего использования.
|
||||
credentialsRepository.saveCredentials(credentials)
|
||||
|
||||
// [CORE-LOGIC] Выполняем UseCase и обрабатываем результат.
|
||||
loginUseCase(credentials).fold(
|
||||
onSuccess = {
|
||||
// [ACTION] Обработка успеха: обновляем UI, отмечая завершение настройки.
|
||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// [ERROR_HANDLER] Обработка ошибки: обновляем UI с сообщением об ошибке.
|
||||
_uiState.update { it.copy(isLoading = false, error = exception.message ?: "Login failed") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_CLASS_SetupViewModel]
|
||||
}
|
||||
// [END_FILE_SetupViewModel.kt]
|
||||
@@ -94,6 +94,7 @@ object Libs {
|
||||
const val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4"
|
||||
const val composeUiTooling = "androidx.compose.ui:ui-tooling"
|
||||
const val composeUiTestManifest = "androidx.compose.ui:ui-test-manifest"
|
||||
|
||||
}
|
||||
|
||||
// [END_FILE_Dependencies.kt]
|
||||
|
||||
@@ -10,11 +10,14 @@ import com.homebox.lens.data.api.dto.ItemSummaryDto
|
||||
import com.homebox.lens.data.api.dto.ItemUpdateDto
|
||||
import com.homebox.lens.data.api.dto.LabelOutDto
|
||||
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.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
@@ -27,6 +30,13 @@ import retrofit2.http.Query
|
||||
*/
|
||||
interface HomeboxApiService {
|
||||
|
||||
// [ENDPOINT] Auth
|
||||
// [FIX] Явно указываем заголовок Content-Type, чтобы переопределить
|
||||
// значение по умолчанию от Moshi, которое содержит "; charset=UTF-8".
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST("v1/users/login")
|
||||
suspend fun login(@Body loginForm: LoginFormDto): TokenResponseDto
|
||||
|
||||
// [ENDPOINT] Items
|
||||
@GET("v1/items")
|
||||
suspend fun getItems(
|
||||
|
||||
@@ -13,24 +13,34 @@ import com.homebox.lens.domain.model.GroupStatistics
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для статистики.
|
||||
* [COHERENCE_NOTE] Этот DTO был исправлен, чтобы точно соответствовать JSON-ответу от сервера.
|
||||
* Поля `items`, `labels`, `locations`, `totalValue` были заменены на `totalItems`, `totalLabels`,
|
||||
* `totalLocations`, `totalItemPrice` и т.д., чтобы устранить ошибку парсинга `JsonDataException`.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GroupStatisticsDto(
|
||||
@Json(name = "items") val items: Int,
|
||||
@Json(name = "labels") val labels: Int,
|
||||
@Json(name = "locations") val locations: Int,
|
||||
@Json(name = "totalValue") val totalValue: Double
|
||||
@Json(name = "totalItems") val totalItems: Int,
|
||||
@Json(name = "totalLabels") val totalLabels: Int,
|
||||
@Json(name = "totalLocations") val totalLocations: Int,
|
||||
@Json(name = "totalItemPrice") val totalItemPrice: Double,
|
||||
// [FIX] Добавляем недостающие поля, которые присутствуют в JSON, но отсутствовали в DTO.
|
||||
// Делаем их nullable на случай, если API перестанет их присылать в будущем.
|
||||
@Json(name = "totalUsers") val totalUsers: Int? = null,
|
||||
@Json(name = "totalWithWarranty") val totalWithWarranty: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из GroupStatisticsDto в доменную модель GroupStatistics.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для использования правильных полей из исправленного DTO.
|
||||
*/
|
||||
fun GroupStatisticsDto.toDomain(): GroupStatistics {
|
||||
// [ACTION] Маппим данные из DTO в доменную модель.
|
||||
return GroupStatistics(
|
||||
items = this.items,
|
||||
labels = this.labels,
|
||||
locations = this.locations,
|
||||
totalValue = this.totalValue
|
||||
items = this.totalItems,
|
||||
labels = this.totalLabels,
|
||||
locations = this.totalLocations,
|
||||
totalValue = this.totalItemPrice
|
||||
)
|
||||
}
|
||||
// [END_FILE_GroupStatisticsDto.kt]
|
||||
@@ -13,28 +13,39 @@ import com.homebox.lens.domain.model.LabelOut
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для метки.
|
||||
* [COHERENCE_NOTE] Поле `isArchived` сделано nullable (`Boolean?`),
|
||||
* так как оно отсутствует в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
||||
* `JsonDataException: Required value 'isArchived' missing`.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LabelOutDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String,
|
||||
@Json(name = "isArchived") val isArchived: Boolean,
|
||||
// [COHERENCE_NOTE] Поле `color` может быть null или отсутствовать, делаем его nullable для безопасности.
|
||||
@Json(name = "color") val color: String?,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||
@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?
|
||||
)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LabelOutDto в доменную модель LabelOut.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
||||
*/
|
||||
fun LabelOutDto.toDomain(): LabelOut {
|
||||
return LabelOut(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
color = this.color,
|
||||
isArchived = this.isArchived,
|
||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию.
|
||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_FILE_LabelOutDto.kt]
|
||||
@@ -13,30 +13,42 @@ import com.homebox.lens.domain.model.LocationOutCount
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* DTO для местоположения со счетчиком.
|
||||
* [COHERENCE_NOTE] Поля `color` и `isArchived` сделаны nullable (`String?`, `Boolean?`),
|
||||
* так как они отсутствуют в JSON-ответе от сервера. Это исправляет ошибку парсинга
|
||||
* `JsonDataException: Required value '...' missing`.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LocationOutCountDto(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "name") val name: String,
|
||||
@Json(name = "color") val color: String,
|
||||
@Json(name = "isArchived") val isArchived: Boolean,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "color") val color: String?,
|
||||
// [FIX] Поле отсутствует в JSON, делаем nullable.
|
||||
@Json(name = "isArchived") val isArchived: Boolean?,
|
||||
@Json(name = "itemCount") val itemCount: Int,
|
||||
@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?
|
||||
)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Маппер из LocationOutCountDto в доменную модель LocationOutCount.
|
||||
* [COHERENCE_NOTE] Маппер обновлен для безопасной обработки nullable полей
|
||||
* и предоставления non-nullable значений по умолчанию для доменной модели.
|
||||
*/
|
||||
fun LocationOutCountDto.toDomain(): LocationOutCount {
|
||||
return LocationOutCount(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
color = this.color,
|
||||
isArchived = this.isArchived,
|
||||
// [FIX] Используем Elvis-оператор для предоставления значения по умолчанию, если поле null.
|
||||
color = this.color ?: "", // Пустая строка как дефолтный цвет
|
||||
isArchived = this.isArchived ?: false, // `false` как дефолтное состояние архива
|
||||
itemCount = this.itemCount,
|
||||
createdAt = this.createdAt,
|
||||
updatedAt = this.updatedAt
|
||||
)
|
||||
}
|
||||
// [END_FILE_LocationOutCountDto.kt]
|
||||
@@ -0,0 +1,15 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] LoginFormDto.kt
|
||||
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LoginFormDto(
|
||||
@Json(name = "username") val username: String,
|
||||
@Json(name = "password") val password: String,
|
||||
@Json(name = "stayLoggedIn") val stayLoggedIn: Boolean = true
|
||||
)
|
||||
// [END_FILE_LoginFormDto.kt]
|
||||
@@ -0,0 +1,15 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.dto
|
||||
// [FILE] TokenResponseDto.kt
|
||||
|
||||
package com.homebox.lens.data.api.dto
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TokenResponseDto(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "attachmentToken") val attachmentToken: String,
|
||||
@Json(name = "expiresAt") val expiresAt: String
|
||||
)
|
||||
// [END_FILE_TokenResponseDto.kt]
|
||||
@@ -0,0 +1,29 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.mapper
|
||||
// [FILE] TokenMapper.kt
|
||||
// [SEMANTICS] mapper, data_conversion, clean_architecture
|
||||
|
||||
package com.homebox.lens.data.api.mapper
|
||||
|
||||
import com.homebox.lens.data.api.dto.TokenResponseDto
|
||||
import com.homebox.lens.domain.model.TokenResponse
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [HELPER] Преобразует DTO-объект токена в доменную модель.
|
||||
* @receiver [TokenResponseDto] объект из слоя данных.
|
||||
* @return [TokenResponse] объект для доменного слоя.
|
||||
* @throws IllegalArgumentException если токен в DTO пустой.
|
||||
*/
|
||||
fun TokenResponseDto.toDomain(): TokenResponse {
|
||||
// [PRECONDITION] DTO должен содержать валидные данные для маппинга.
|
||||
require(this.token.isNotBlank()) { "[PRECONDITION_FAILED] DTO token is blank, cannot map to domain model." }
|
||||
|
||||
// [ACTION]
|
||||
val domainModel = TokenResponse(token = this.token)
|
||||
|
||||
// [POSTCONDITION] Проверяем, что инвариант доменной модели соблюден.
|
||||
check(domainModel.token.isNotBlank()) { "[POSTCONDITION_FAILED] Domain model token is blank after mapping." }
|
||||
|
||||
return domainModel
|
||||
}
|
||||
// [END_FILE_TokenMapper.kt]
|
||||
@@ -0,0 +1,29 @@
|
||||
// [PACKAGE] com.homebox.lens.data.api.model
|
||||
// [FILE] LoginRequest.kt
|
||||
// [SEMANTICS] dto, network, serialization, authentication
|
||||
|
||||
package com.homebox.lens.data.api.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* [ENTITY: DataClass('LoginRequest')]
|
||||
* [CONTRACT]
|
||||
* DTO (Data Transfer Object) для запроса на аутентификацию.
|
||||
* @property username Имя пользователя.
|
||||
* @property password Пароль пользователя.
|
||||
* @invariant Свойства не должны быть пустыми.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class LoginRequest(
|
||||
@Json(name = "username") val username: String,
|
||||
@Json(name = "password") val password: String
|
||||
) {
|
||||
init {
|
||||
// [INVARIANT_CHECK]
|
||||
require(username.isNotBlank()) { "[INVARIANT_FAILED] Username cannot be blank." }
|
||||
require(password.isNotBlank()) { "[INVARIANT_FAILED] Password cannot be blank." }
|
||||
}
|
||||
}
|
||||
// [END_FILE_LoginRequest.kt]
|
||||
@@ -1,77 +1,135 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] ApiModule.kt
|
||||
|
||||
// [PURPOSE] Предоставляет синглтон-зависимости для работы с сетью, включая OkHttpClient, Retrofit и ApiService.
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Singleton
|
||||
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [MODULE: DaggerHilt('ApiModule')]
|
||||
* [PURPOSE] Предоставляет зависимости для работы с сетью (Retrofit, OkHttp, Moshi).
|
||||
* [ENTITY: Module('ApiModule')]
|
||||
* [CONTRACT]
|
||||
* Hilt-модуль, отвечающий за создание и предоставление всех зависимостей,
|
||||
* необходимых для сетевого взаимодействия.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ApiModule {
|
||||
|
||||
// [HELPER]
|
||||
private const val BASE_URL = "https://api.homebox.app/"
|
||||
// [HELPER] Временный базовый URL для API. В будущем должен стать динамическим.
|
||||
private const val BASE_URL = "https://homebox.bebesh.ru/api/"
|
||||
|
||||
// [PROVIDER]
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT]
|
||||
* Предоставляет сконфигурированный OkHttpClient.
|
||||
* @param credentialsRepositoryProvider Провайдер репозитория для доступа к токену авторизации.
|
||||
* Используется Provider<T> для предотвращения циклов зависимостей.
|
||||
* @return Синглтон-экземпляр OkHttpClient с настроенными перехватчиками.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
// [ACTION] Create logging interceptor
|
||||
val logging = HttpLoggingInterceptor().apply {
|
||||
fun provideOkHttpClient(
|
||||
credentialsRepositoryProvider: Provider<CredentialsRepository>
|
||||
): OkHttpClient {
|
||||
// [ACTION] Создаем перехватчик для логирования.
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
}
|
||||
// [ACTION] Build OkHttpClient
|
||||
|
||||
// [ACTION] Создаем перехватчик для добавления заголовка 'Accept'.
|
||||
val acceptHeaderInterceptor = Interceptor { chain ->
|
||||
val request = chain.request().newBuilder()
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
}
|
||||
|
||||
// [CORE-LOGIC] Создаем перехватчик для добавления токена авторизации.
|
||||
val authInterceptor = Interceptor { chain ->
|
||||
// [HELPER] Получаем токен из репозитория.
|
||||
// runBlocking здесь допустим, т.к. чтение из SharedPreferences - быстрая I/O операция,
|
||||
// а интерфейс Interceptor'а является синхронным.
|
||||
val token = runBlocking { credentialsRepositoryProvider.get().getToken() }
|
||||
|
||||
val requestBuilder = chain.request().newBuilder()
|
||||
|
||||
// [ACTION] Если токен существует, добавляем его в заголовок.
|
||||
if (token != null) {
|
||||
// Сервер ожидает заголовок "Authorization: Bearer <token>"
|
||||
// Предполагается, что `token` уже содержит префикс "Bearer ".
|
||||
requestBuilder.addHeader("Authorization", token)
|
||||
}
|
||||
|
||||
chain.proceed(requestBuilder.build())
|
||||
}
|
||||
|
||||
// [ACTION] Собираем OkHttpClient с правильным порядком перехватчиков.
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(logging)
|
||||
// [TODO] Add AuthInterceptor for Bearer token
|
||||
.addInterceptor(acceptHeaderInterceptor)
|
||||
.addInterceptor(authInterceptor) // Добавляем перехватчик для токена
|
||||
.addInterceptor(loggingInterceptor) // Логирование должно идти последним, чтобы видеть финальный запрос
|
||||
.build()
|
||||
}
|
||||
|
||||
// [PROVIDER]
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет экземпляр Moshi для парсинга JSON.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshi(): Moshi {
|
||||
// [ACTION] Build Moshi with Kotlin adapter
|
||||
return Moshi.Builder()
|
||||
.add(KotlinJsonAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
// [PROVIDER]
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет фабрику конвертеров для Retrofit.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
|
||||
// [ACTION] Build Retrofit instance
|
||||
fun provideMoshiConverterFactory(moshi: Moshi): MoshiConverterFactory {
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет сконфигурированный экземпляр Retrofit.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(okHttpClient: OkHttpClient, moshiConverterFactory: MoshiConverterFactory): Retrofit {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addConverterFactory(moshiConverterFactory)
|
||||
.build()
|
||||
}
|
||||
|
||||
// [PROVIDER]
|
||||
/**
|
||||
* [PROVIDER]
|
||||
* [CONTRACT] Предоставляет реализацию интерфейса HomeboxApiService.
|
||||
*/
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHomeboxApiService(retrofit: Retrofit): HomeboxApiService {
|
||||
// [ACTION] Create ApiService from Retrofit instance
|
||||
return retrofit.create(HomeboxApiService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
// [END_FILE_ApiModule.kt]
|
||||
// [END_FILE_ApiModule.kt]
|
||||
@@ -1,32 +1,61 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] RepositoryModule.kt
|
||||
// [SEMANTICS] dependency_injection, hilt, module, binding
|
||||
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.repository.AuthRepositoryImpl
|
||||
import com.homebox.lens.data.repository.CredentialsRepositoryImpl
|
||||
import com.homebox.lens.data.repository.ItemRepositoryImpl
|
||||
import com.homebox.lens.domain.repository.AuthRepository
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
// [CONTRACT]
|
||||
/**
|
||||
* [MODULE: DaggerHilt('RepositoryModule')]
|
||||
* [PURPOSE] Предоставляет реализацию для интерфейса ItemRepository.
|
||||
* [ENTITY: Module('RepositoryModule')]
|
||||
* [CONTRACT]
|
||||
* Hilt-модуль для предоставления реализаций репозиториев.
|
||||
* Использует `@Binds` для эффективного связывания интерфейсов с их реализациями.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RepositoryModule {
|
||||
abstract class RepositoryModule {
|
||||
|
||||
// [PROVIDER]
|
||||
@Provides
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Связывает интерфейс ItemRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
fun provideItemRepository(apiService: HomeboxApiService): ItemRepository {
|
||||
return ItemRepositoryImpl(apiService)
|
||||
}
|
||||
}
|
||||
abstract fun bindItemRepository(
|
||||
itemRepositoryImpl: ItemRepositoryImpl
|
||||
): ItemRepository
|
||||
|
||||
// [END_FILE_RepositoryModule.kt]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Связывает интерфейс CredentialsRepository с его реализацией.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindCredentialsRepository(
|
||||
credentialsRepositoryImpl: CredentialsRepositoryImpl
|
||||
): CredentialsRepository
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [FIX] Связывает интерфейс AuthRepository с его реализацией.
|
||||
* Это исправляет ошибку "could not be resolved", так как теперь Hilt знает,
|
||||
* какую конкретную реализацию предоставить, когда запрашивается AuthRepository.
|
||||
*/
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindAuthRepository(
|
||||
authRepositoryImpl: AuthRepositoryImpl
|
||||
): AuthRepository
|
||||
}
|
||||
// [END_FILE_RepositoryModule.kt]
|
||||
41
data/src/main/java/com/homebox/lens/data/di/StorageModule.kt
Normal file
41
data/src/main/java/com/homebox/lens/data/di/StorageModule.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
// [PACKAGE] com.homebox.lens.data.di
|
||||
// [FILE] StorageModule.kt
|
||||
|
||||
package com.homebox.lens.data.di
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.repository.EncryptedPreferencesWrapper
|
||||
import com.homebox.lens.data.security.CryptoManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object StorageModule {
|
||||
|
||||
private const val PREFERENCES_FILE_NAME = "homebox_lens_prefs" // No longer secret
|
||||
|
||||
// [ACTION] Provide a standard, unencrypted SharedPreferences instance.
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences {
|
||||
return context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
// [ACTION] Provide our new EncryptedPreferencesWrapper as the main entry point for secure storage.
|
||||
// Hilt will automatically provide SharedPreferences and CryptoManager to its constructor.
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideEncryptedPreferencesWrapper(
|
||||
sharedPreferences: SharedPreferences,
|
||||
cryptoManager: CryptoManager
|
||||
): EncryptedPreferencesWrapper {
|
||||
return EncryptedPreferencesWrapper(sharedPreferences, cryptoManager)
|
||||
}
|
||||
}
|
||||
// [END_FILE_StorageModule.kt]
|
||||
@@ -0,0 +1,88 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] AuthRepositoryImpl.kt
|
||||
// [SEMANTICS] data_implementation, authentication, repository
|
||||
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.api.dto.LoginFormDto
|
||||
import com.homebox.lens.data.api.mapper.toDomain
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.model.TokenResponse
|
||||
import com.homebox.lens.domain.repository.AuthRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* [ENTITY: Class('AuthRepositoryImpl')]
|
||||
* [CONTRACT]
|
||||
* Реализация репозитория для управления аутентификацией.
|
||||
* @param encryptedPrefs Защищенное хранилище для токена.
|
||||
* @param okHttpClient Общий OkHttp клиент для переиспользования.
|
||||
* @param moshiConverterFactory Общий конвертер Moshi для переиспользования.
|
||||
* [COHERENCE_NOTE] Реализация метода login теперь включает логику создания временного Retrofit-клиента
|
||||
* "на лету", используя URL сервера из credentials. Эта логика была перенесена из ItemRepositoryImpl.
|
||||
*/
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences,
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val moshiConverterFactory: MoshiConverterFactory
|
||||
) : AuthRepository {
|
||||
|
||||
companion object {
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Реализует вход пользователя. Создает временный API сервис для выполнения запроса
|
||||
* на указанный пользователем URL сервера.
|
||||
* @param credentials Учетные данные пользователя, включая URL сервера.
|
||||
* @return [Result] с доменной моделью [TokenResponse] при успехе или [Exception] при ошибке.
|
||||
*/
|
||||
override suspend fun login(credentials: Credentials): Result<TokenResponse> {
|
||||
// [PRECONDITION]
|
||||
require(credentials.serverUrl.isNotBlank()) { "[PRECONDITION_FAILED] Server URL cannot be blank." }
|
||||
|
||||
// [CORE-LOGIC]
|
||||
return withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
// [ACTION] Создаем временный Retrofit клиент с URL, указанным пользователем.
|
||||
val tempApiService = Retrofit.Builder()
|
||||
.baseUrl(credentials.serverUrl)
|
||||
.client(okHttpClient) // Переиспользуем существующий OkHttp клиент
|
||||
.addConverterFactory(moshiConverterFactory) // и конвертер
|
||||
.build()
|
||||
.create(HomeboxApiService::class.java)
|
||||
|
||||
// [ACTION] Создаем DTO и выполняем запрос.
|
||||
val loginForm = LoginFormDto(credentials.username, credentials.password)
|
||||
val tokenResponseDto = tempApiService.login(loginForm)
|
||||
|
||||
// [ACTION] Маппим результат в доменную модель.
|
||||
tokenResponseDto.toDomain()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveToken(token: String) {
|
||||
require(token.isNotBlank()) { "[PRECONDITION_FAILED] Token cannot be blank." }
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPrefs.edit().putString(KEY_AUTH_TOKEN, token).apply()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getToken(): Flow<String?> = flow {
|
||||
emit(encryptedPrefs.getString(KEY_AUTH_TOKEN, null))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
// [END_FILE_AuthRepositoryImpl.kt]
|
||||
@@ -0,0 +1,99 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] CredentialsRepositoryImpl.kt
|
||||
// [PURPOSE] Имплементация репозитория для управления учетными данными и токенами доступа.
|
||||
package com.homebox.lens.data.repository
|
||||
// [IMPORTS]
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* [ENTITY: Class('CredentialsRepositoryImpl')]
|
||||
* [CONTRACT]
|
||||
* Реализует репозиторий для управления учетными данными пользователя.
|
||||
* Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных.
|
||||
* @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt.
|
||||
* @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`.
|
||||
*/
|
||||
class CredentialsRepositoryImpl @Inject constructor(
|
||||
private val encryptedPrefs: SharedPreferences
|
||||
) : CredentialsRepository {
|
||||
|
||||
// [CONSTANTS_KEYS] Ключи для хранения данных в SharedPreferences.
|
||||
companion object {
|
||||
private const val KEY_SERVER_URL = "key_server_url"
|
||||
private const val KEY_USERNAME = "key_username"
|
||||
private const val KEY_PASSWORD = "key_password"
|
||||
private const val KEY_AUTH_TOKEN = "key_auth_token"
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет основные учетные данные пользователя.
|
||||
* @param credentials Объект с учетными данными для сохранения.
|
||||
* @sideeffect Перезаписывает существующие учетные данные в SharedPreferences.
|
||||
*/
|
||||
override suspend fun saveCredentials(credentials: Credentials) {
|
||||
// [ACTION] Выполняем запись в SharedPreferences в фоновом потоке.
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_SERVER_URL, credentials.serverUrl)
|
||||
.putString(KEY_USERNAME, credentials.username)
|
||||
.putString(KEY_PASSWORD, credentials.password)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Извлекает сохраненные учетные данные пользователя в виде потока.
|
||||
* @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют.
|
||||
*/
|
||||
override fun getCredentials(): Flow<Credentials?> = flow {
|
||||
// [CORE-LOGIC] Читаем данные из SharedPreferences.
|
||||
val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null)
|
||||
val username = encryptedPrefs.getString(KEY_USERNAME, null)
|
||||
val password = encryptedPrefs.getString(KEY_PASSWORD, null)
|
||||
|
||||
// [ACTION] Эммитим результат.
|
||||
if (serverUrl != null && username != null && password != null) {
|
||||
emit(Credentials(serverUrl, username, password))
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO) // [ACTION] Указываем, что Flow должен выполняться в фоновом потоке.
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет токен авторизации.
|
||||
* @param token Токен для сохранения.
|
||||
* @sideeffect Перезаписывает существующий токен в SharedPreferences.
|
||||
*/
|
||||
override suspend fun saveToken(token: String) {
|
||||
// [ACTION] Выполняем запись токена в фоновом потоке.
|
||||
withContext(Dispatchers.IO) {
|
||||
encryptedPrefs.edit()
|
||||
.putString(KEY_AUTH_TOKEN, token)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Извлекает сохраненный токен авторизации.
|
||||
* @return Строка с токеном или null, если он не найден.
|
||||
*/
|
||||
override suspend fun getToken(): String? {
|
||||
// [ACTION] Выполняем чтение токена в фоновом потоке.
|
||||
return withContext(Dispatchers.IO) {
|
||||
encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_CredentialsRepositoryImpl.kt]
|
||||
@@ -0,0 +1,66 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] EncryptedPreferencesWrapper.kt
|
||||
// [PURPOSE] A wrapper around SharedPreferences to provide on-the-fly encryption/decryption.
|
||||
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.homebox.lens.data.security.CryptoManager
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Provides a simplified and secure interface for storing and retrieving sensitive string data.
|
||||
* 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 cryptoManager The manager responsible for all cryptographic operations.
|
||||
*/
|
||||
class EncryptedPreferencesWrapper @Inject constructor(
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
private val cryptoManager: CryptoManager
|
||||
) {
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Retrieves a decrypted string value for a given key.
|
||||
* @param key The key for the preference.
|
||||
* @param defaultValue The value to return if the key is not found or decryption fails.
|
||||
* @return The decrypted string, or the defaultValue.
|
||||
*/
|
||||
fun getString(key: String, defaultValue: String?): String? {
|
||||
val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue
|
||||
return try {
|
||||
val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT)
|
||||
val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes))
|
||||
String(decryptedBytes, Charset.defaultCharset())
|
||||
} catch (e: Exception) {
|
||||
// Log the error, maybe clear the invalid preference
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Encrypts and saves a string value for a given key.
|
||||
* @param key The key for the preference.
|
||||
* @param value The string value to encrypt and save.
|
||||
* @sideeffect Modifies the underlying SharedPreferences file.
|
||||
*/
|
||||
fun putString(key: String, value: String) {
|
||||
try {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream)
|
||||
val encryptedBytes = outputStream.toByteArray()
|
||||
val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT)
|
||||
sharedPreferences.edit().putString(key, encryptedValue).apply()
|
||||
} catch (e: Exception) {
|
||||
// Log the error
|
||||
}
|
||||
}
|
||||
|
||||
// [COHERENCE_NOTE] Add other methods like getInt, putInt etc. as needed, following the same pattern.
|
||||
}
|
||||
// [END_FILE_EncryptedPreferencesWrapper.kt]
|
||||
@@ -1,7 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.data.repository
|
||||
// [FILE] ItemRepositoryImpl.kt
|
||||
// [SEMANTICS] data_repository, implementation, network
|
||||
// [SEMANTICS] data_repository, implementation, items
|
||||
|
||||
package com.homebox.lens.data.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.data.api.HomeboxApiService
|
||||
import com.homebox.lens.data.api.dto.toDomain
|
||||
@@ -16,17 +18,20 @@ import javax.inject.Singleton
|
||||
* [CONTRACT]
|
||||
* Реализация репозитория для работы с данными о вещах.
|
||||
* @param apiService Сервис для взаимодействия с Homebox API.
|
||||
* [COHERENCE_NOTE] Метод 'login' был полностью удален из этого класса, так как его ответственность
|
||||
* была передана в AuthRepositoryImpl. Это устраняет ошибку компиляции "'login' overrides nothing".
|
||||
*/
|
||||
@Singleton
|
||||
class ItemRepositoryImpl @Inject constructor(
|
||||
private val apiService: HomeboxApiService
|
||||
private val apiService: HomeboxApiService,
|
||||
) : ItemRepository {
|
||||
|
||||
// [DELETED] Метод login был здесь, но теперь он удален.
|
||||
|
||||
/**
|
||||
* [CONTRACT] @see ItemRepository.createItem
|
||||
*/
|
||||
override suspend fun createItem(newItemData: ItemCreate): ItemSummary {
|
||||
// [ACTION]
|
||||
val itemDto = newItemData.toDto()
|
||||
val resultDto = apiService.createItem(itemDto)
|
||||
return resultDto.toDomain()
|
||||
@@ -36,7 +41,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.getItemDetails
|
||||
*/
|
||||
override suspend fun getItemDetails(itemId: String): ItemOut {
|
||||
// [ACTION]
|
||||
val resultDto = apiService.getItem(itemId)
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
@@ -45,7 +49,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.updateItem
|
||||
*/
|
||||
override suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut {
|
||||
// [ACTION]
|
||||
val itemDto = item.toDto()
|
||||
val resultDto = apiService.updateItem(itemId, itemDto)
|
||||
return resultDto.toDomain()
|
||||
@@ -55,7 +58,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.deleteItem
|
||||
*/
|
||||
override suspend fun deleteItem(itemId: String) {
|
||||
// [ACTION]
|
||||
apiService.deleteItem(itemId)
|
||||
}
|
||||
|
||||
@@ -63,7 +65,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.syncInventory
|
||||
*/
|
||||
override suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> {
|
||||
// [ACTION]
|
||||
val resultDto = apiService.getItems(page = page, pageSize = pageSize)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
@@ -72,7 +73,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.getStatistics
|
||||
*/
|
||||
override suspend fun getStatistics(): GroupStatistics {
|
||||
// [ACTION]
|
||||
val resultDto = apiService.getStatistics()
|
||||
return resultDto.toDomain()
|
||||
}
|
||||
@@ -81,7 +81,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.getAllLocations
|
||||
*/
|
||||
override suspend fun getAllLocations(): List<LocationOutCount> {
|
||||
// [ACTION]
|
||||
val resultDto = apiService.getLocations()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
@@ -90,7 +89,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.getAllLabels
|
||||
*/
|
||||
override suspend fun getAllLabels(): List<LabelOut> {
|
||||
// [ACTION]
|
||||
val resultDto = apiService.getLabels()
|
||||
return resultDto.map { it.toDomain() }
|
||||
}
|
||||
@@ -99,7 +97,6 @@ class ItemRepositoryImpl @Inject constructor(
|
||||
* [CONTRACT] @see ItemRepository.searchItems
|
||||
*/
|
||||
override suspend fun searchItems(query: String): PaginationResult<ItemSummary> {
|
||||
// [ACTION]
|
||||
val resultDto = apiService.getItems(query = query)
|
||||
return resultDto.toDomain { it.toDomain() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// [PACKAGE] com.homebox.lens.data.security
|
||||
// [FILE] CryptoManager.kt
|
||||
// [PURPOSE] Handles all cryptographic operations using AndroidKeyStore.
|
||||
|
||||
package com.homebox.lens.data.security
|
||||
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import androidx.annotation.RequiresApi
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* A manager for handling encryption and decryption using the Android Keystore system.
|
||||
* This class ensures that cryptographic keys are stored securely.
|
||||
* It is designed to be a Singleton provided by Hilt.
|
||||
* @invariant The underlying SecretKey must be valid within the AndroidKeyStore.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
@Singleton
|
||||
class CryptoManager @Inject constructor() {
|
||||
|
||||
// [ЯКОРЬ] Настройки для шифрования
|
||||
private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
|
||||
load(null)
|
||||
}
|
||||
|
||||
private val encryptCipher
|
||||
get() = Cipher.getInstance(TRANSFORMATION).apply {
|
||||
init(Cipher.ENCRYPT_MODE, getKey())
|
||||
}
|
||||
|
||||
private fun getDecryptCipherForIv(iv: ByteArray): Cipher {
|
||||
return Cipher.getInstance(TRANSFORMATION).apply {
|
||||
init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))
|
||||
}
|
||||
}
|
||||
|
||||
// [CORE-LOGIC] Получение или создание ключа
|
||||
private fun getKey(): SecretKey {
|
||||
val existingKey = keyStore.getEntry(ALIAS, null) as? KeyStore.SecretKeyEntry
|
||||
return existingKey?.secretKey ?: createKey()
|
||||
}
|
||||
|
||||
private fun createKey(): SecretKey {
|
||||
return KeyGenerator.getInstance(ALGORITHM).apply {
|
||||
init(
|
||||
KeyGenParameterSpec.Builder(
|
||||
ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||
)
|
||||
.setBlockModes(BLOCK_MODE)
|
||||
.setEncryptionPaddings(PADDING)
|
||||
.setUserAuthenticationRequired(false)
|
||||
.setRandomizedEncryptionRequired(true)
|
||||
.build()
|
||||
)
|
||||
}.generateKey()
|
||||
}
|
||||
|
||||
// [ACTION] Шифрование потока данных
|
||||
fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray {
|
||||
val cipher = encryptCipher
|
||||
val encryptedBytes = cipher.doFinal(bytes)
|
||||
outputStream.use {
|
||||
it.write(cipher.iv.size)
|
||||
it.write(cipher.iv)
|
||||
it.write(encryptedBytes.size)
|
||||
it.write(encryptedBytes)
|
||||
}
|
||||
return encryptedBytes
|
||||
}
|
||||
|
||||
// [ACTION] Дешифрование потока данных
|
||||
fun decrypt(inputStream: InputStream): ByteArray {
|
||||
return inputStream.use {
|
||||
val ivSize = it.read()
|
||||
val iv = ByteArray(ivSize)
|
||||
it.read(iv)
|
||||
|
||||
val encryptedBytesSize = it.read()
|
||||
val encryptedBytes = ByteArray(encryptedBytesSize)
|
||||
it.read(encryptedBytes)
|
||||
|
||||
getDecryptCipherForIv(iv).doFinal(encryptedBytes)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
|
||||
private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
|
||||
private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
|
||||
private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING"
|
||||
private const val ALIAS = "homebox_lens_secret_key"
|
||||
}
|
||||
}
|
||||
// [END_FILE_CryptoManager.kt]
|
||||
@@ -0,0 +1,18 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] Credentials.kt
|
||||
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Data class to hold server credentials.
|
||||
* @property serverUrl The URL of the Homebox server.
|
||||
* @property username The username for authentication.
|
||||
* @property password The password for authentication.
|
||||
*/
|
||||
data class Credentials(
|
||||
val serverUrl: String,
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
// [END_FILE_Credentials.kt]
|
||||
@@ -0,0 +1,20 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.model
|
||||
// [FILE] TokenResponse.kt
|
||||
// [SEMANTICS] data_transfer_object, authentication, model
|
||||
|
||||
package com.homebox.lens.domain.model
|
||||
|
||||
/**
|
||||
* [ENTITY: DataClass('TokenResponse')]
|
||||
* [CONTRACT]
|
||||
* Модель данных, представляющая ответ от сервера с токеном аутентификации.
|
||||
* @property token Строка, содержащая JWT или другой токен доступа.
|
||||
* @invariant `token` не должен быть пустым.
|
||||
*/
|
||||
data class TokenResponse(val token: String) {
|
||||
init {
|
||||
// [INVARIANT_CHECK]
|
||||
require(token.isNotBlank()) { "[INVARIANT_FAILED] Token cannot be blank." }
|
||||
}
|
||||
}
|
||||
// [END_FILE_TokenResponse.kt]
|
||||
@@ -0,0 +1,42 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.repository
|
||||
// [FILE] AuthRepository.kt
|
||||
// [SEMANTICS] authentication, data_access, repository
|
||||
|
||||
package com.homebox.lens.domain.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.model.TokenResponse
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Репозиторий для управления аутентификацией.
|
||||
* [COHERENCE_NOTE] Добавлен метод `login` для инкапсуляции логики входа.
|
||||
*/
|
||||
interface AuthRepository {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Выполняет вход в систему, используя предоставленные учетные данные.
|
||||
* @param credentials Учетные данные пользователя (URL сервера, логин, пароль).
|
||||
* @return [Result] с [TokenResponse] в случае успеха, или с [Exception] в случае ошибки.
|
||||
* @throws IllegalArgumentException если `credentials` невалидны (предусловие).
|
||||
*/
|
||||
suspend fun login(credentials: Credentials): Result<TokenResponse>
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Сохраняет токен аутентификации.
|
||||
* @param token Токен для сохранения.
|
||||
* @throws IllegalArgumentException если `token` пустой (предусловие).
|
||||
*/
|
||||
suspend fun saveToken(token: String)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Получает токен аутентификации.
|
||||
* @return [Flow], который эммитит токен в виде строки, или `null`, если токен отсутствует.
|
||||
*/
|
||||
fun getToken(): Flow<String?>
|
||||
}
|
||||
// [END_FILE_AuthRepository.kt]
|
||||
@@ -0,0 +1,44 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.repository
|
||||
// [FILE] CredentialsRepository.kt
|
||||
|
||||
package com.homebox.lens.domain.repository
|
||||
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Repository for managing user credentials and session tokens.
|
||||
*/
|
||||
interface CredentialsRepository {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Saves the user's base credentials (URL, username, password) securely.
|
||||
* @param credentials The credentials to save.
|
||||
* @sideeffect Overwrites any existing saved credentials.
|
||||
*/
|
||||
suspend fun saveCredentials(credentials: Credentials)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Retrieves the saved user credentials.
|
||||
* @return A Flow emitting the saved [Credentials], or null if none are saved.
|
||||
*/
|
||||
fun getCredentials(): Flow<Credentials?>
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Saves the authorization token received after a successful login.
|
||||
* @param token The authorization token (including "Bearer " prefix if provided by the server).
|
||||
* @sideeffect Overwrites any existing saved token.
|
||||
*/
|
||||
suspend fun saveToken(token: String)
|
||||
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* [ACTION] Retrieves the saved authorization token.
|
||||
* @return The saved token as a String, or null if no token is saved.
|
||||
*/
|
||||
suspend fun getToken(): String?
|
||||
}
|
||||
// [END_FILE_CredentialsRepository.kt]
|
||||
@@ -1,7 +1,9 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.repository
|
||||
// [FILE] ItemRepository.kt
|
||||
// [SEMANTICS] data_access, abstraction, repository
|
||||
|
||||
package com.homebox.lens.domain.repository
|
||||
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.*
|
||||
|
||||
@@ -10,8 +12,10 @@ import com.homebox.lens.domain.model.*
|
||||
* [CONTRACT]
|
||||
* Абстракция репозитория для работы с "Вещами".
|
||||
* Определяет контракт, которому должен следовать слой данных.
|
||||
* [COHERENCE_NOTE] Метод `login` был удален, так как он относится к аутентификации и перенесен в `AuthRepository`.
|
||||
*/
|
||||
interface ItemRepository {
|
||||
// [DELETED] suspend fun login(credentials: Credentials): Result<Unit>
|
||||
suspend fun createItem(newItemData: ItemCreate): ItemSummary
|
||||
suspend fun getItemDetails(itemId: String): ItemOut
|
||||
suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut
|
||||
@@ -22,4 +26,4 @@ interface ItemRepository {
|
||||
suspend fun getAllLabels(): List<LabelOut>
|
||||
suspend fun searchItems(query: String): PaginationResult<ItemSummary>
|
||||
}
|
||||
// [END_FILE_ItemRepository.kt]
|
||||
// [END_FILE_ItemRepository.kt]
|
||||
@@ -1,25 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||
// [FILE] domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt
|
||||
// [SEMANTICS] domain, usecase, label, list
|
||||
// [FILE] GetAllLabelsUseCase.kt
|
||||
|
||||
// [IMPORTS]
|
||||
package com.homebox.lens.domain.usecase
|
||||
|
||||
import com.homebox.lens.domain.model.LabelOut
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
// [CORE-LOGIC]
|
||||
class GetAllLabelsUseCase @Inject constructor(
|
||||
private val itemRepository: ItemRepository
|
||||
) {
|
||||
suspend operator fun invoke(): List<LabelOut>? {
|
||||
return try {
|
||||
itemRepository.getAllLabels()
|
||||
} catch (e: Exception) {
|
||||
// [ERROR_HANDLER] Просто возвращаем null.
|
||||
null
|
||||
}
|
||||
class GetAllLabelsUseCase @Inject constructor(private val repository: ItemRepository) {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Получает список всех меток.
|
||||
* @return Список [LabelOut].
|
||||
* @throws Exception в случае ошибки сети или API.
|
||||
*/
|
||||
suspend operator fun invoke(): List<LabelOut> {
|
||||
// [FIX] Упрощено.
|
||||
return repository.getAllLabels()
|
||||
}
|
||||
}
|
||||
// [END_FILE_GetAllLabelsUseCase.kt]
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||
// [FILE] domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt
|
||||
// [SEMANTICS] domain, usecase, location, list
|
||||
// [FILE] GetAllLocationsUseCase.kt
|
||||
|
||||
// [IMPORTS]
|
||||
package com.homebox.lens.domain.usecase
|
||||
|
||||
import com.homebox.lens.domain.model.LocationOutCount
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
// [CORE-LOGIC]
|
||||
class GetAllLocationsUseCase @Inject constructor(
|
||||
private val itemRepository: ItemRepository
|
||||
) {
|
||||
suspend operator fun invoke(): List<LocationOutCount>? {
|
||||
return try {
|
||||
itemRepository.getAllLocations()
|
||||
} catch (e: Exception) {
|
||||
// [ERROR_HANDLER] Просто возвращаем null.
|
||||
null
|
||||
}
|
||||
class GetAllLocationsUseCase @Inject constructor(private val repository: ItemRepository) {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Получает список всех локаций.
|
||||
* @return Список [LocationOutCount].
|
||||
* @throws Exception в случае ошибки сети или API.
|
||||
*/
|
||||
suspend operator fun invoke(): List<LocationOutCount> {
|
||||
// [FIX] Упрощено.
|
||||
return repository.getAllLocations()
|
||||
}
|
||||
}
|
||||
// [END_FILE_GetAllLocationsUseCase.kt]
|
||||
}
|
||||
@@ -1,25 +1,22 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||
// [FILE] domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt
|
||||
// [SEMANTICS] domain, usecase, statistics
|
||||
// [FILE] GetStatisticsUseCase.kt
|
||||
|
||||
// [IMPORTS]
|
||||
package com.homebox.lens.domain.usecase
|
||||
|
||||
import com.homebox.lens.domain.model.GroupStatistics
|
||||
import com.homebox.lens.domain.repository.ItemRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
// [CORE-LOGIC]
|
||||
class GetStatisticsUseCase @Inject constructor(
|
||||
private val itemRepository: ItemRepository
|
||||
) {
|
||||
suspend operator fun invoke(): GroupStatistics? {
|
||||
return try {
|
||||
itemRepository.getStatistics()
|
||||
} catch (e: Exception) {
|
||||
// [ERROR_HANDLER] Просто возвращаем null, вызывающий слой обработает это.
|
||||
null
|
||||
}
|
||||
class GetStatisticsUseCase @Inject constructor(private val repository: ItemRepository) {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Получает статистику инвентаря.
|
||||
* @return [GroupStatistics] объект.
|
||||
* @throws Exception в случае ошибки сети или API.
|
||||
*/
|
||||
suspend operator fun invoke(): GroupStatistics {
|
||||
// [FIX] Упрощено. Просто вызываем репозиторий и возвращаем его результат.
|
||||
// Обработка ошибок делегирована вызывающей стороне (ViewModel).
|
||||
return repository.getStatistics()
|
||||
}
|
||||
}
|
||||
// [END_FILE_GetStatisticsUseCase.kt]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// [PACKAGE] com.homebox.lens.domain.usecase
|
||||
// [FILE] LoginUseCase.kt
|
||||
// [PURPOSE] Инкапсулирует бизнес-логику процесса входа пользователя в систему.
|
||||
package com.homebox.lens.domain.usecase
|
||||
// [IMPORTS]
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.model.TokenResponse
|
||||
import com.homebox.lens.domain.repository.AuthRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* [ENTITY: Class('LoginUseCase')]
|
||||
* [CONTRACT]
|
||||
* Use case для выполнения входа пользователя.
|
||||
* @param authRepository Репозиторий для выполнения сетевого запроса на вход и сохранения токена.
|
||||
* [COHERENCE_NOTE] Удалена зависимость от CredentialsRepository для сохранения токена.
|
||||
* Эту ответственность теперь несет AuthRepository.
|
||||
*/
|
||||
class LoginUseCase @Inject constructor(
|
||||
private val authRepository: AuthRepository
|
||||
) {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Выполняет процесс входа в систему.
|
||||
* @param credentials Учетные данные пользователя.
|
||||
* @return [Result] с [Unit] в случае успеха или с [Exception] в случае ошибки.
|
||||
* @sideeffect В случае успеха, сохраняет токен авторизации через `authRepository`.
|
||||
*/
|
||||
suspend operator fun invoke(credentials: Credentials): Result<Unit> {
|
||||
// [PRECONDITION]
|
||||
require(credentials.serverUrl.isNotBlank() && credentials.username.isNotBlank()) {
|
||||
"[PRECONDITION_FAILED] Server URL and username must not be blank."
|
||||
}
|
||||
|
||||
// [ACTION] Выполняем вход через authRepository.
|
||||
val loginResult: Result<TokenResponse> = authRepository.login(credentials)
|
||||
|
||||
// [CORE-LOGIC] Обрабатываем результат с помощью `fold`.
|
||||
return loginResult.fold(
|
||||
onSuccess = { tokenResponse ->
|
||||
// [ACTION] В случае успеха, сохраняем токен через тот же репозиторий.
|
||||
authRepository.saveToken(tokenResponse.token)
|
||||
Result.success(Unit)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
// [ACTION] В случае ошибки, просто пробрасываем ее дальше.
|
||||
Result.failure(exception)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// [END_FILE_LoginUseCase.kt]
|
||||
@@ -1,117 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PROJECT_STRUCTURE>
|
||||
<module name="app" type="android_app">
|
||||
<purpose_summary>Main application module, contains UI and application entry points.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/MainActivity.kt" status="implemented" ref_id="entry_point">
|
||||
<purpose_summary>The main and only Activity of the application, hosts the NavHost.</purpose_summary>
|
||||
<purpose_summary>Основной модуль приложения, содержит UI и точки входа в приложение.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/MainActivity.kt" status="реализовано" ref_id="entry_point">
|
||||
<purpose_summary>Главная и единственная Activity приложения, содержит NavHost.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/MainApplication.kt" status="implemented" ref_id="app_context">
|
||||
<purpose_summary>Application class, used for Hilt dependency injection setup.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/MainApplication.kt" status="реализовано" ref_id="app_context">
|
||||
<purpose_summary>Класс Application, используется для настройки внедрения зависимостей Hilt.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/di/AppModule.kt" status="implemented" ref_id="di_app">
|
||||
<purpose_summary>Hilt module for application-wide dependencies.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/di/AppModule.kt" status="реализовано" ref_id="di_app">
|
||||
<purpose_summary>Модуль Hilt для зависимостей уровня приложения.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt" status="implemented" ref_id="nav_graph">
|
||||
<purpose_summary>Defines the navigation graph for the entire application using Jetpack Compose Navigation.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/navigation/NavGraph.kt" status="реализовано" ref_id="nav_graph">
|
||||
<purpose_summary>Определяет навигационный граф для всего приложения с использованием Jetpack Compose Navigation.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/navigation/Screen.kt" status="implemented" ref_id="nav_screen">
|
||||
<purpose_summary>Defines the routes for all screens in the app as a sealed class.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/navigation/Screen.kt" status="реализовано" ref_id="nav_screen">
|
||||
<purpose_summary>Определяет маршруты для всех экранов в приложении в виде запечатанного класса.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" status="stub" spec_ref_id="screen_dashboard">
|
||||
<purpose_summary>UI for the Dashboard screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" status="реализовано" spec_ref_id="screen_dashboard">
|
||||
<purpose_summary>UI для экрана панели управления.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt" status="stub" spec_ref_id="screen_dashboard">
|
||||
<purpose_summary>ViewModel for the Dashboard screen, handles business logic.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardViewModel.kt" status="реализовано" spec_ref_id="screen_dashboard">
|
||||
<purpose_summary>ViewModel для экрана панели управления, обрабатывает бизнес-логику.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" status="stub" spec_ref_id="screen_inventory_list">
|
||||
<purpose_summary>UI for the Inventory List screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListScreen.kt" status="заглушка" spec_ref_id="screen_inventory_list">
|
||||
<purpose_summary>UI для экрана списка инвентаря.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt" status="stub" spec_ref_id="screen_inventory_list">
|
||||
<purpose_summary>ViewModel for the Inventory List screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/inventorylist/InventoryListViewModel.kt" status="заглушка" spec_ref_id="screen_inventory_list">
|
||||
<purpose_summary>ViewModel для экрана списка инвентаря.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" status="stub" spec_ref_id="screen_item_details">
|
||||
<purpose_summary>UI for the Item Details screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsScreen.kt" status="заглушка" spec_ref_id="screen_item_details">
|
||||
<purpose_summary>UI для экрана сведений о товаре.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt" status="stub" spec_ref_id="screen_item_details">
|
||||
<purpose_summary>ViewModel for the Item Details screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemdetails/ItemDetailsViewModel.kt" status="заглушка" spec_ref_id="screen_item_details">
|
||||
<purpose_summary>ViewModel для экрана сведений о товаре.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" status="stub" spec_ref_id="screen_item_edit">
|
||||
<purpose_summary>UI for the Item Edit screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditScreen.kt" status="заглушка" spec_ref_id="screen_item_edit">
|
||||
<purpose_summary>UI для экрана редактирования товара.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" status="stub" spec_ref_id="screen_item_edit">
|
||||
<purpose_summary>ViewModel for the Item Edit screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/itemedit/ItemEditViewModel.kt" status="заглушка" spec_ref_id="screen_item_edit">
|
||||
<purpose_summary>ViewModel для экрана редактирования товара.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" status="stub" spec_ref_id="screen_labels_list">
|
||||
<purpose_summary>UI for the Labels List screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" status="заглушка" spec_ref_id="screen_labels_list">
|
||||
<purpose_summary>UI для экрана списка меток.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt" status="stub" spec_ref_id="screen_labels_list">
|
||||
<purpose_summary>ViewModel for the Labels List screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt" status="заглушка" spec_ref_id="screen_labels_list">
|
||||
<purpose_summary>ViewModel для экрана списка меток.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" status="stub" spec_ref_id="screen_locations_list">
|
||||
<purpose_summary>UI for the Locations List screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" status="заглушка" spec_ref_id="screen_locations_list">
|
||||
<purpose_summary>UI для экрана списка местоположений.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt" status="stub" spec_ref_id="screen_locations_list">
|
||||
<purpose_summary>ViewModel for the Locations List screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt" status="заглушка" spec_ref_id="screen_locations_list">
|
||||
<purpose_summary>ViewModel для экрана списка местоположений.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" status="stub" spec_ref_id="screen_search">
|
||||
<purpose_summary>UI for the Search screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" status="заглушка" spec_ref_id="screen_search">
|
||||
<purpose_summary>UI для экрана поиска.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="stub" spec_ref_id="screen_search">
|
||||
<purpose_summary>ViewModel for the Search screen.</purpose_summary>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/search/SearchViewModel.kt" status="заглушка" spec_ref_id="screen_search">
|
||||
<purpose_summary>ViewModel для экрана поиска.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" status="реализовано" spec_ref_id="screen_setup">
|
||||
<purpose_summary>UI для экрана настройки.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupViewModel.kt" status="реализовано" spec_ref_id="screen_setup">
|
||||
<purpose_summary>ViewModel для экрана настройки.</purpose_summary>
|
||||
</file>
|
||||
<file name="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupUiState.kt" status="реализовано" spec_ref_id="screen_setup">
|
||||
<purpose_summary>Состояние UI для экрана настройки.</purpose_summary>
|
||||
</file>
|
||||
</module>
|
||||
<module name="data" type="android_library">
|
||||
<purpose_summary>Data layer, responsible for data sources (network, local DB) and repository implementations.</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/api/HomeboxApiService.kt" status="implemented" ref_id="api_service">
|
||||
<purpose_summary>Retrofit service interface for the Homebox API.</purpose_summary>
|
||||
<purpose_summary>Слой данных, отвечающий за источники данных (сеть, локальная БД) и реализации репозиториев.</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/api/HomeboxApiService.kt" status="реализовано" ref_id="api_service">
|
||||
<purpose_summary>Интерфейс сервиса Retrofit для Homebox API.</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/db/HomeboxDatabase.kt" status="implemented" ref_id="database">
|
||||
<purpose_summary>Room database definition for local caching.</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/db/HomeboxDatabase.kt" status="реализовано" ref_id="database">
|
||||
<purpose_summary>Определение базы данных Room для локального кэширования.</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt" status="implemented" ref_id="repo_impl">
|
||||
<purpose_summary>Implementation of the ItemRepository, coordinating data from API and local DB.</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/repository/ItemRepositoryImpl.kt" status="реализовано" ref_id="repo_impl">
|
||||
<purpose_summary>Реализация ItemRepository, координирующая данные из API и локальной БД.</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/ApiModule.kt" status="implemented" ref_id="di_api">
|
||||
<purpose_summary>Hilt module for providing network-related dependencies (Retrofit, OkHttp).</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/ApiModule.kt" status="реализовано" ref_id="di_api">
|
||||
<purpose_summary>Модуль Hilt для предоставления зависимостей, связанных с сетью (Retrofit, OkHttp).</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/DatabaseModule.kt" status="implemented" ref_id="di_db">
|
||||
<purpose_summary>Hilt module for providing database-related dependencies (Room DB, DAOs).</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/DatabaseModule.kt" status="реализовано" ref_id="di_db">
|
||||
<purpose_summary>Модуль Hilt для предоставления зависимостей, связанных с базой данных (Room DB, DAO).</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="implemented" ref_id="di_repo">
|
||||
<purpose_summary>Hilt module for binding repository interfaces to their implementations.</purpose_summary>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/RepositoryModule.kt" status="реализовано" ref_id="di_repo">
|
||||
<purpose_summary>Модуль Hilt для привязки интерфейсов репозиториев к их реализациям.</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/di/StorageModule.kt" status="реализовано" ref_id="di_storage">
|
||||
<purpose_summary>Модуль Hilt для предоставления зависимостей, связанных с хранилищем (EncryptedSharedPreferences).</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/repository/CredentialsRepositoryImpl.kt" status="реализовано" ref_id="repo_credentials_impl">
|
||||
<purpose_summary>Реализация CredentialsRepository.</purpose_summary>
|
||||
</file>
|
||||
<file name="data/src/main/java/com/homebox/lens/data/repository/AuthRepositoryImpl.kt" status="реализовано" ref_id="repo_auth_impl">
|
||||
<purpose_summary>Реализация AuthRepository.</purpose_summary>
|
||||
</file>
|
||||
</module>
|
||||
<module name="domain" type="kotlin_jvm_library">
|
||||
<purpose_summary>Domain layer, contains business logic, use cases, and repository interfaces. Pure Kotlin module.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="implemented" ref_id="repo_interface">
|
||||
<purpose_summary>Interface defining the contract for data operations related to items.</purpose_summary>
|
||||
<purpose_summary>Доменный слой, содержит бизнес-логику, сценарии использования и интерфейсы репозиториев. Чистый модуль Kotlin.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/model/Credentials.kt" status="реализовано" ref_id="model_credentials">
|
||||
<purpose_summary>Класс данных для хранения учетных данных пользователя.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="implemented" spec_ref_id="uc_create_item">
|
||||
<purpose_summary>Use case for creating a new item.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/repository/AuthRepository.kt" status="реализовано" ref_id="repo_auth_interface">
|
||||
<purpose_summary>Интерфейс для репозитория аутентификации.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" status="implemented" spec_ref_id="uc_delete_item">
|
||||
<purpose_summary>Use case for deleting an item.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/repository/CredentialsRepository.kt" status="реализовано" ref_id="repo_credentials_interface">
|
||||
<purpose_summary>Интерфейс для репозитория учетных данных.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" status="implemented" spec_ref_id="uc_get_all_labels">
|
||||
<purpose_summary>Use case for getting all labels.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/repository/ItemRepository.kt" status="реализовано" ref_id="repo_interface">
|
||||
<purpose_summary>Интерфейс, определяющий контракт для операций с данными, связанными с товарами.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" status="implemented" spec_ref_id="uc_get_all_locations">
|
||||
<purpose_summary>Use case for getting all locations.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" status="реализовано" spec_ref_id="uc_login">
|
||||
<purpose_summary>Сценарий использования для входа пользователя.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" status="implemented" spec_ref_id="uc_get_item_details">
|
||||
<purpose_summary>Use case for getting the details of a single item.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/CreateItemUseCase.kt" status="реализовано" spec_ref_id="uc_create_item">
|
||||
<purpose_summary>Сценарий использования для создания нового товара.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" status="implemented" spec_ref_id="uc_get_stats">
|
||||
<purpose_summary>Use case for getting inventory statistics.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" status="реализовано" spec_ref_id="uc_delete_item">
|
||||
<purpose_summary>Сценарий использования для удаления товара.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" status="implemented" spec_ref_id="uc_search_items">
|
||||
<purpose_summary>Use case for searching items.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" status="реализовано" spec_ref_id="uc_get_all_labels">
|
||||
<purpose_summary>Сценарий использования для получения всех меток.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" status="implemented" spec_ref_id="uc_sync_inventory">
|
||||
<purpose_summary>Use case for syncing the local inventory with the remote server.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" status="реализовано" spec_ref_id="uc_get_all_locations">
|
||||
<purpose_summary>Сценарий использования для получения всех местоположений.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" status="implemented" spec_ref_id="uc_update_item">
|
||||
<purpose_summary>Use case for updating an existing item.</purpose_summary>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetItemDetailsUseCase.kt" status="реализовано" spec_ref_id="uc_get_item_details">
|
||||
<purpose_summary>Сценарий использования для получения сведений о конкретном товаре.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" status="реализовано" spec_ref_id="uc_get_stats">
|
||||
<purpose_summary>Сценарий использования для получения статистики по инвентарю.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SearchItemsUseCase.kt" status="реализовано" spec_ref_id="uc_search_items">
|
||||
<purpose_summary>Сценарий использования для поиска товаров.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/SyncInventoryUseCase.kt" status="реализовано" spec_ref_id="uc_sync_inventory">
|
||||
<purpose_summary>Сценарий использования для синхронизации локального инвентаря с удаленным сервером.</purpose_summary>
|
||||
</file>
|
||||
<file name="domain/src/main/java/com/homebox/lens/domain/usecase/UpdateItemUseCase.kt" status="реализовано" spec_ref_id="uc_update_item">
|
||||
<purpose_summary>Сценарий использования для обновления существующего товара.</purpose_summary>
|
||||
</file>
|
||||
</module>
|
||||
</PROJECT_STRUCTURE>
|
||||
@@ -2,104 +2,111 @@
|
||||
<PROJECT_SPECIFICATION>
|
||||
<PROJECT_INFO>
|
||||
<name>Homebox Lens</name>
|
||||
<description>An Android client for the Homebox inventory management system. It allows users to manage their inventory by interacting with a Homebox server instance.</description>
|
||||
<description>Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox.</description>
|
||||
</PROJECT_INFO>
|
||||
|
||||
<TECHNICAL_DECISIONS>
|
||||
<DECISION id="tech_logging">
|
||||
<summary>Библиотека логирования</summary>
|
||||
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
|
||||
</DECISION>
|
||||
</TECHNICAL_DECISIONS>
|
||||
|
||||
<FEATURES>
|
||||
<FEATURE id="feat_dashboard" status="in_progress">
|
||||
<summary>Dashboard Screen</summary>
|
||||
<description>Displays a summary of the inventory, including statistics like total items, total value, and counts by location/label.</description>
|
||||
<FEATURE id="feat_dashboard" status="бэкенд_реализован">
|
||||
<summary>Экран панели управления</summary>
|
||||
<description>Отображает сводку по инвентарю, включая статистику, такую как общее количество товаров, общая стоимость и количество по местоположениям/меткам.</description>
|
||||
<UI_COMPONENT ref_id="screen_dashboard" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_get_stats" status="implemented">
|
||||
<summary>Fetch and display statistics</summary>
|
||||
<description>Retrieves overall inventory statistics from the server.</description>
|
||||
<FUNCTION id="func_get_stats" status="реализовано">
|
||||
<summary>Получение и отображение статистики</summary>
|
||||
<description>Получает общую статистику по инвентарю с сервера.</description>
|
||||
<implementation_ref id="uc_get_stats" />
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_inventory_list" status="in_progress">
|
||||
<summary>Inventory List Screen</summary>
|
||||
<description>Displays a searchable and filterable list of all inventory items.</description>
|
||||
<FEATURE id="feat_inventory_list" status="бэкенд_реализован">
|
||||
<summary>Экран списка инвентаря</summary>
|
||||
<description>Отображает список всех инвентарных позиций с возможностью поиска и фильтрации.</description>
|
||||
<UI_COMPONENT ref_id="screen_inventory_list" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_search_items" status="implemented">
|
||||
<summary>Search and filter items</summary>
|
||||
<description>Searches for items based on a query string and filters. The results are paginated.</description>
|
||||
<FUNCTION id="func_search_items" status="реализовано">
|
||||
<summary>Поиск и фильтрация товаров</summary>
|
||||
<description>Ищет товары по строке запроса и фильтрам. Результаты разбиты на страницы.</description>
|
||||
<implementation_ref id="uc_search_items" />
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_sync_inventory" status="implemented">
|
||||
<summary>Sync Inventory</summary>
|
||||
<description>Performs a full synchronization of the local inventory cache with the server.</description>
|
||||
<FUNCTION id="func_sync_inventory" status="реализовано">
|
||||
<summary>Синхронизация инвентаря</summary>
|
||||
<description>Выполняет полную синхронизацию локального кэша инвентаря с сервером.</description>
|
||||
<implementation_ref id="uc_sync_inventory" />
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_item_details" status="in_progress">
|
||||
<summary>Item Details Screen</summary>
|
||||
<description>Shows all details for a single inventory item, including its name, description, images, attachments, and custom fields.</description>
|
||||
<FEATURE id="feat_item_details" status="бэкенд_реализован">
|
||||
<summary>Экран сведений о товаре</summary>
|
||||
<description>Показывает все сведения о конкретном инвентарном товаре, включая его название, описание, изображения, вложения и настраиваемые поля.</description>
|
||||
<UI_COMPONENT ref_id="screen_item_details" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_get_item_details" status="implemented">
|
||||
<summary>Fetch Item Details</summary>
|
||||
<description>Retrieves the full details for a specific item from the repository.</description>
|
||||
<FUNCTION id="func_get_item_details" status="реализовано">
|
||||
<summary>Получение сведений о товаре</summary>
|
||||
<description>Получает полные сведения о конкретном товаре из репозитория.</description>
|
||||
<implementation_ref id="uc_get_item_details" />
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_item_management" status="in_progress">
|
||||
<summary>Create/Edit/Delete Items</summary>
|
||||
<description>Allows users to create new items, update existing ones, and delete them.</description>
|
||||
<FEATURE id="feat_item_management" status="бэкенд_реализован">
|
||||
<summary>Создание/редактирование/удаление товаров</summary>
|
||||
<description>Позволяет пользователям создавать новые товары, обновлять существующие и удалять их.</description>
|
||||
<UI_COMPONENT ref_id="screen_item_edit" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_create_item" status="implemented">
|
||||
<summary>Create Item</summary>
|
||||
<description>Creates a new inventory item on the server.</description>
|
||||
<FUNCTION id="func_create_item" status="реализовано">
|
||||
<summary>Создать товар</summary>
|
||||
<description>Создает новый инвентарный товар на сервере.</description>
|
||||
<implementation_ref id="uc_create_item" />
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_update_item" status="implemented">
|
||||
<summary>Update Item</summary>
|
||||
<description>Updates an existing inventory item on the server.</description>
|
||||
<FUNCTION id="func_update_item" status="реализовано">
|
||||
<summary>Обновить товар</summary>
|
||||
<description>Обновляет существующий инвентарный товар на сервере.</description>
|
||||
<implementation_ref id="uc_update_item" />
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_delete_item" status="implemented">
|
||||
<summary>Delete Item</summary>
|
||||
<description>Deletes an inventory item from the server.</description>
|
||||
<FUNCTION id="func_delete_item" status="реализовано">
|
||||
<summary>Удалить товар</summary>
|
||||
<description>Удаляет инвентарный товар с сервера.</description>
|
||||
<implementation_ref id="uc_delete_item" />
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_labels_locations" status="in_progress">
|
||||
<summary>Manage Labels and Locations</summary>
|
||||
<description>Allows users to view lists of all available labels and locations.</description>
|
||||
<FEATURE id="feat_labels_locations" status="бэкенд_реализован">
|
||||
<summary>Управление метками и местоположениями</summary>
|
||||
<description>Позволяет пользователям просматривать списки всех доступных меток и местоположений.</description>
|
||||
<UI_COMPONENT ref_id="screen_labels_list" />
|
||||
<UI_COMPONENT ref_id="screen_locations_list" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_get_all_labels" status="implemented">
|
||||
<summary>Get All Labels</summary>
|
||||
<description>Retrieves a list of all labels from the repository.</description>
|
||||
<FUNCTION id="func_get_all_labels" status="реализовано">
|
||||
<summary>Получить все метки</summary>
|
||||
<description>Получает список всех меток из репозитория.</description>
|
||||
<implementation_ref id="uc_get_all_labels" />
|
||||
</FUNCTION>
|
||||
<FUNCTION id="func_get_all_locations" status="implemented">
|
||||
<summary>Get All Locations</summary>
|
||||
<description>Retrieves a list of all locations from the repository.</description>
|
||||
<FUNCTION id="func_get_all_locations" status="реализовано">
|
||||
<summary>Получить все местоположения</summary>
|
||||
<description>Получает список всех местоположений из репозитория.</description>
|
||||
<implementation_ref id="uc_get_all_locations" />
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
|
||||
<FEATURE id="feat_search" status="in_progress">
|
||||
<summary>Search Screen</summary>
|
||||
<description>Provides a dedicated UI for searching items.</description>
|
||||
<FEATURE id="feat_search" status="бэкенд_реализован">
|
||||
<summary>Экран поиска</summary>
|
||||
<description>Предоставляет специальный пользовательский интерфейс для поиска товаров.</description>
|
||||
<UI_COMPONENT ref_id="screen_search" />
|
||||
<FUNCTIONALITY>
|
||||
<FUNCTION id="func_search_items_dedicated" status="implemented">
|
||||
<summary>Search from dedicated screen</summary>
|
||||
<description>Uses the same search functionality but from a dedicated screen.</description>
|
||||
<FUNCTION id="func_search_items_dedicated" status="реализовано">
|
||||
<summary>Поиск со специального экрана</summary>
|
||||
<description>Использует ту же функцию поиска, но со специального экрана.</description>
|
||||
<implementation_ref id="uc_search_items" />
|
||||
</FUNCTION>
|
||||
</FUNCTIONALITY>
|
||||
@@ -117,6 +124,7 @@
|
||||
<USE_CASE id="uc_delete_item" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/DeleteItemUseCase.kt" />
|
||||
<USE_CASE id="uc_get_all_labels" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLabelsUseCase.kt" />
|
||||
<USE_CASE id="uc_get_all_locations" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetAllLocationsUseCase.kt" />
|
||||
<USE_CASE id="uc_login" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/LoginUseCase.kt" />
|
||||
|
||||
<!-- UI Screens -->
|
||||
<UI_SCREEN id="screen_dashboard" file_ref="app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt" />
|
||||
@@ -126,5 +134,6 @@
|
||||
<UI_SCREEN id="screen_labels_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt" />
|
||||
<UI_SCREEN id="screen_locations_list" file_ref="app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt" />
|
||||
<UI_SCREEN id="screen_search" file_ref="app/src/main/java/com/homebox/lens/ui/screen/search/SearchScreen.kt" />
|
||||
<UI_SCREEN id="screen_setup" file_ref="app/src/main/java/com/homebox/lens/ui/screen/setup/SetupScreen.kt" />
|
||||
</IMPLEMENTATION_MAP>
|
||||
</PROJECT_SPECIFICATION>
|
||||
Reference in New Issue
Block a user