Homebox Lens Android-клиент для системы управления инвентарем Homebox. Позволяет пользователям управлять своим инвентарем, взаимодействуя с экземпляром сервера Homebox. UI Framework Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных. Асинхронные операции Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока. Сетевое взаимодействие Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON. Локальное хранилище Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных. Библиотека логирования В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования. Интернационализация (Мультиязычность) Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории. - Все строки, видимые пользователю, вынесены в `app/src/main/res/values/strings.xml`. - Язык по умолчанию - русский (ru). - В коде для доступа к строкам используются ссылки на ресурсы (например, `R.string.app_name`). Внедрение зависимостей (Dependency Injection) Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях. Модуль Hilt для зависимостей уровня приложения AppModule.kt предоставляет зависимости на уровне приложения, такие как контекст приложения и другие синглтоны. Навигация Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation. Навигационный граф NavGraph.kt определяет структуру навигации приложения, связывая экраны и их маршруты. Определение маршрутов экранов Screen.kt определяет все возможные маршруты (экраны) в приложении в виде запечатанного класса для типобезопасной навигации. Спецификация безопасности проекта. Все сетевые взаимодействия должны быть защищены HTTPS. Аутентификация пользователя хранится в EncryptedSharedPreferences. Обработка ошибок аутентификации должна включать logout и редирект на экран логина. Использовать JWT или API-ключ для авторизации запросов. При истечении токена автоматически обновлять. Локальные данные (credentials) шифровать с помощью Android KeyStore. Спецификация обработки ошибок. Все потенциальные ошибки (сеть, БД, валидация) должны быть обработаны с использованием sealed classes для ошибок (e.g., NetworkError, ValidationError) и отображаться пользователю через Snackbar или Dialog. При сетевых ошибках показывать сообщение "No internet connection" и предлагать retry. Для HTTP 4xx/5xx отображать user-friendly сообщение на основе response body. Использовать require/check для контрактов, логировать и показывать toast. Руководство по использованию иконок Этот раздел определяет стандартный набор иконок 'androidx.compose.material.icons.Icons.Filled' для использования в приложении. Для некоторых иконок указаны их AutoMirrored версии для корректного отображения в RTL-языках. build, dependencies build, dependencies Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation. navigation, compose, nav_host Запечатанный класс для определения маршрутов навигации в приложении. Обеспечивает типобезопасность при навигации. @param route Строковый идентификатор маршрута. / sealed class Screen(val route: String) { // [ENTITY: Object('Setup')] data object Setup : Screen("setup_screen") // [END_ENTITY: Object('Setup')] // [ENTITY: Object('Dashboard')] data object Dashboard : Screen("dashboard_screen") // [END_ENTITY: Object('Dashboard')] // [ENTITY: Object('InventoryList')] data object InventoryList : Screen("inventory_list_screen") { // [ENTITY: Function('withFilter')] /** @summary Создает маршрут для экрана списка инвентаря с параметром фильтра. @param key Ключ фильтра (например, "label" или "location"). @param value Значение фильтра (например, ID метки или местоположения). @return Строку полного маршрута с query-параметром. @throws IllegalArgumentException если ключ или значение пустые. / fun withFilter(key: String, value: String): String { require(key.isNotBlank()) { "Filter key cannot be blank." } require(value.isNotBlank()) { "Filter value cannot be blank." } val constructedRoute = "inventory_list_screen?$key=$value" check(constructedRoute.contains("?$key=$value")) { "Route must contain the filter query." } return constructedRoute } // [END_ENTITY: Function('withFilter')] } // [END_ENTITY: Object('InventoryList')] // [ENTITY: Object('ItemDetails')] data object ItemDetails : Screen("item_details_screen/{itemId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана деталей элемента с указанным ID. @param itemId ID элемента для отображения. @return Строку полного маршрута. @throws IllegalArgumentException если itemId пустой. / fun createRoute(itemId: String): String { require(itemId.isNotBlank()) { "itemId не может быть пустым." } val route = "item_details_screen/$itemId" check(route.endsWith(itemId)) { "Маршрут должен заканчиваться на itemId." } return route } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('ItemDetails')] // [ENTITY: Object('ItemEdit')] data object ItemEdit : Screen("item_edit_screen?itemId={itemId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана редактирования элемента с указанным ID. @param itemId ID элемента для редактирования. Null, если создается новый элемент. @return Строку полного маршрута. / fun createRoute(itemId: String? = null): String { return itemId?.let { "item_edit_screen?itemId=$it" } ?: "item_edit_screen" } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('ItemEdit')] // [ENTITY: Object('LabelsList')] data object LabelsList : Screen("labels_list_screen") // [END_ENTITY: Object('LabelsList')] // [ENTITY: Object('LabelEdit')] data object LabelEdit : Screen("label_edit_screen?labelId={labelId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана редактирования метки с указанным ID. @param labelId ID метки для редактирования. Null, если создается новая метка. @return Строку полного маршрута. / fun createRoute(labelId: String? = null): String { return labelId?.let { "label_edit_screen?labelId=$it" } ?: "label_edit_screen" } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('LabelEdit')] // [ENTITY: Object('LocationsList')] data object LocationsList : Screen("locations_list_screen") // [END_ENTITY: Object('LocationsList')] // [ENTITY: Object('LocationEdit')] data object LocationEdit : Screen("location_edit_screen/{locationId}") { // [ENTITY: Function('createRoute')] /** @summary Создает маршрут для экрана редактирования местоположения с указанным ID. @param locationId ID местоположения для редактирования. @return Строку полного маршрута. @throws IllegalArgumentException если locationId пустой. / fun createRoute(locationId: String): String { require(locationId.isNotBlank()) { "locationId не может быть пустым." } val route = "location_edit_screen/$locationId" check(route.endsWith(locationId)) { "Маршрут должен заканчиваться на locationId." } return route } // [END_ENTITY: Function('createRoute')] } // [END_ENTITY: Object('LocationEdit')] // [ENTITY: Object('Search')] data object Search : Screen("search_screen") // [END_ENTITY: Object('Search')] } navigation, routes, sealed_class Класс-обертка над NavHostController для предоставления типизированных навигационных действий. navigation, controller, actions Точка входа в приложение. Инициализирует Hilt и Timber. application, hilt, timber Контент для бокового навигационного меню (Drawer). ui, common, navigation_drawer Общая обертка для экранов, включающая Scaffold и Navigation Drawer. ui, common, scaffold, navigation_drawer The main theme for the Homebox Lens application. ui, theme Defines the typography for the application. ui, theme, typography Определяет все возможные состояния для экрана "Дэшборд". ui, state, dashboard Главная Composable-функция для экрана "Панель управления". ui, screen, dashboard, compose, navigation Отображает основной контент экрана в зависимости от uiState. ui, screen, dashboard, compose, navigation Секция для отображения общей статистики. ui, screen, dashboard, compose, navigation Карточка для отображения одного статистического показателя. ui, screen, dashboard, compose, navigation Секция для отображения недавно добавленных элементов. ui, screen, dashboard, compose, navigation Карточка для отображения краткой информации об элементе. ui, screen, dashboard, compose, navigation Секция для отображения местоположений в виде чипсов. ui, screen, dashboard, compose, navigation Секция для отображения меток в виде чипсов. ui, screen, dashboard, compose, navigation ui, screen, dashboard, compose, navigation ui, screen, dashboard, compose, navigation ui, screen, dashboard, compose, navigation ViewModel для главного экрана (Dashboard). Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки. @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`. / @HiltViewModel class DashboardViewModel @Inject constructor( private val getStatisticsUseCase: GetStatisticsUseCase, private val getAllLocationsUseCase: GetAllLocationsUseCase, private val getAllLabelsUseCase: GetAllLabelsUseCase, private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading) val uiState = _uiState.asStateFlow() init { loadDashboardData() } // [ENTITY: Function('loadDashboardData')] /** @summary Загружает все необходимые данные для экрана Dashboard. @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`. @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`. / fun loadDashboardData() { viewModelScope.launch { _uiState.value = DashboardUiState.Loading Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.") val statsFlow = flow { emit(getStatisticsUseCase()) } val locationsFlow = flow { emit(getAllLocationsUseCase()) } val labelsFlow = flow { emit(getAllLabelsUseCase()) } val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10) combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems -> DashboardUiState.Success( statistics = stats, locations = locations, labels = labels, recentlyAddedItems = recentItems ) }.catch { exception -> Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.") _uiState.value = DashboardUiState.Error( message = exception.message ?: "Could not load dashboard data." ) }.collect { successState -> Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.") _uiState.value = successState } } } // [END_ENTITY: Function('loadDashboardData')] } ui_logic, dashboard, state_management, sealed_state, parallel_data_loading, timber_logging Composable-функция для экрана "Редактирование метки". ui, screen, label, edit ViewModel для экрана редактирования/создания метки. Управляет состоянием и логикой экрана, включая загрузку, создание и обновление метки. ui, viewmodel, label_management, hilt Состояние UI для экрана редактирования метки. ui, viewmodel, label_management Composable-функция для экрана "Список инвентаря". ui, screen, inventory, list ViewModel for the inventory list screen. ui, viewmodel, inventory_list Неизменяемая модель данных, представляющая полное состояние экрана настройки (Setup Screen). Использование `data class` предоставляет метод `copy()` для легкого создания новых состояний. @param serverUrl URL-адрес сервера Homebox. @param username Имя пользователя для входа. @param password Пароль пользователя. @param isLoading Флаг, указывающий, выполняется ли в данный момент сетевой запрос. @param error Сообщение об ошибке для отображения пользователю, или `null` при отсутствии ошибки. @param 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 ) ui_state, data_model, immutable Главная Composable-функция для экрана настройки соединения с сервером. ui, screen, setup, compose Отображает контент экрана настройки: поля ввода и кнопку. ui, screen, setup, compose ui, screen, setup, compose ViewModel для экрана первоначальной настройки (Setup). ui_logic, viewmodel, state_management, user_setup, authentication_flow Отображает экран со списком всех меток. ui, labels_list, state_management, compose, dialog Composable-функция для отображения списка меток. ui, labels_list, state_management, compose, dialog Composable-функция для отображения одного элемента в списке меток. ui, labels_list, state_management, compose, dialog ViewModel для экрана со списком меток. Управляет состоянием экрана, загружает список меток, обрабатывает ошибки и управляет диалогом создания новой метки. @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`. / @HiltViewModel class LabelsListViewModel @Inject constructor( private val getAllLabelsUseCase: GetAllLabelsUseCase ) : ViewModel() { private val _uiState = MutableStateFlow<LabelsListUiState>(LabelsListUiState.Loading) val uiState = _uiState.asStateFlow() init { loadLabels() } // [ENTITY: Function('loadLabels')] /** @summary Загружает список меток. @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его между состояниями `Loading`, `Success` и `Error`. @sideeffect Асинхронно обновляет `_uiState`. / fun loadLabels() { viewModelScope.launch { _uiState.value = LabelsListUiState.Loading Timber.i("[INFO][ENTRYPOINT][loading_labels] Starting labels list load. State -> Loading.") val result = runCatching { getAllLabelsUseCase() } result.fold( onSuccess = { labelOuts -> Timber.i("[INFO][SUCCESS][labels_loaded] Labels loaded successfully. Count: ${labelOuts.size}. State -> Success.") val labels = labelOuts.map { labelOut -> Label( id = labelOut.id, name = labelOut.name ) } _uiState.value = LabelsListUiState.Success(labels, isShowingCreateDialog = false) }, onFailure = { exception -> Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load labels. State -> Error.") _uiState.value = LabelsListUiState.Error( message = exception.message ?: "Could not load labels." ) } ) } } // [END_ENTITY: Function('loadLabels')] // [ENTITY: Function('onShowCreateDialog')] /** @summary Инициирует отображение диалога для создания метки. @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `true`. @sideeffect Обновляет `_uiState`. / fun onShowCreateDialog() { Timber.i("[INFO][ACTION][show_create_dialog] Show create label dialog requested.") if (_uiState.value is LabelsListUiState.Success) { _uiState.update { (it as LabelsListUiState.Success).copy(isShowingCreateDialog = true) } } } // [END_ENTITY: Function('onShowCreateDialog')] // [ENTITY: Function('onDismissCreateDialog')] /** @summary Скрывает диалог создания метки. @description Обновляет состояние `uiState`, устанавливая `isShowingCreateDialog` в `false`. @sideeffect Обновляет `_uiState`. / fun onDismissCreateDialog() { Timber.i("[INFO][ACTION][dismiss_create_dialog] Dismiss create label dialog requested.") if (_uiState.value is LabelsListUiState.Success) { _uiState.update { (it as LabelsListUiState.Success).copy(isShowingCreateDialog = false) } } } // [END_ENTITY: Function('onDismissCreateDialog')] // [ENTITY: Function('createLabel')] /** @summary Создает новую метку. [MVP_SCOPE] ЗАГЛУШКА. @description В текущей реализации (План Б, Этап 1), эта функция только логирует действие и скрывает диалог. Реальная логика сохранения будет добавлена на следующем этапе. @param name Название новой метки. @precondition `name` не должен быть пустым. @sideeffect Логирует действие, обновляет `_uiState`, чтобы скрыть диалог. / fun createLabel(name: String) { require(name.isNotBlank()) { "[CONTRACT_VIOLATION] Label name cannot be blank." } Timber.i("[INFO][ACTION][create_label] Create label called with name: '$name'. [STUBBED]") // [AI_NOTE]: Здесь будет вызов CreateLabelUseCase. onDismissCreateDialog() } // [END_ENTITY: Function('createLabel')] } ui_logic, labels_list, state_management, dialog_management Определяет все возможные состояния для UI экрана со списком меток. Использование sealed-интерфейса позволяет исчерпывающе обрабатывать все состояния в Composable-функциях. / sealed interface LabelsListUiState { // [ENTITY: DataClass('Success')] // [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('Label')] /** @summary Состояние успеха, содержит список меток и состояние диалога. @param labels Список меток для отображения. @param isShowingCreateDialog Флаг, показывающий, должен ли быть отображен диалог создания метки. @invariant labels не может быть null. / data class Success( val labels: List<Label>, val isShowingCreateDialog: Boolean = false ) : LabelsListUiState // [END_ENTITY: DataClass('Success')] // [ENTITY: DataClass('Error')] /** @summary Состояние ошибки. @param message Текст ошибки для отображения пользователю. @invariant message не может быть пустой. / data class Error(val message: String) : LabelsListUiState // [END_ENTITY: DataClass('Error')] // [ENTITY: Object('Loading')] /** @summary Состояние загрузки данных. @description Указывает, что идет процесс загрузки меток. / data object Loading : LabelsListUiState // [END_ENTITY: Object('Loading')] } ui_state, sealed_interface, contract Экран для сканирования QR-кодов и штрих-кодов. ui, screen, scan, camera, qrcode, barcode ViewModel для экрана сканирования. ui_logic, viewmodel, scan, camera Определяет все возможные состояния для UI экрана сканирования. ui_state, sealed_interface, contract Анализатор изображений для обнаружения штрих-кодов. data, service, barcode_scanning Composable-функция для экрана "Редактирование элемента". ui, screen, item, edit UI state for the item edit screen. ui, viewmodel, item_edit ViewModel for the item edit screen. ui, viewmodel, item_edit Composable-функция для экрана "Поиск". ui, screen, search ViewModel for the search screen. ui, viewmodel, search Composable-функция для экрана "Редактирование местоположения". ui, screen, location, edit ViewModel for the item details screen. ui, viewmodel, item_details Composable-функция для экрана "Детали элемента". ui, screen, item, details Composable-функция для экрана "Список местоположений". ui, screen, locations, list Отображает основной контент экрана в зависимости от `uiState`. ui, screen, locations, list Карточка для отображения одного местоположения. ui, screen, locations, list ui, screen, locations, list ui, screen, locations, list ui, screen, locations, list ui, screen, locations, list ViewModel для экрана списка местоположений. ui, viewmodel, locations, hilt Определяет возможные состояния UI для экрана списка местоположений. ui, state, locations Компонент для выбора цвета. ui, component, color_selection Полноэкранный оверлей с индикатором загрузки. ui, component, loading Главная и единственная Activity в приложении. ui, activity, entrypoint ui, activity, entrypoint ui, activity, entrypoint Unit tests for [UpdateItemUseCase]. testing, usecase, unit_test Сценарий использования для обновления метки. business_logic, use_case, label, update Сценарий использования для обновления местоположения. business_logic, use_case, location, update Use case для удаления вещи. business_logic, use_case, item_deletion Получает список всех локаций. domain, usecase Сценарий использования для получения списка недавно добавленных товаров. domain, usecase Use case для получения детальной информации о вещи. business_logic, use_case, item_retrieval Получает статистику инвентаря. domain, usecase Use case для выполнения входа пользователя. domain, usecase, authentication Use case для создания новой вещи. business_logic, use_case, item_creation Use case для синхронизации (получения) списка вещей. business_logic, use_case, data_sync Use case для поиска вещей по текстовому запросу. business_logic, use_case, search Сценарий использования для создания нового местоположения. business_logic, use_case, location, create Сценарий использования для удаления местоположения. business_logic, use_case, location, delete Сценарий использования для удаления метки. business_logic, use_case, label, delete Получает детальную информацию о метке по ее ID. business_logic, use_case, label_retrieval Use case для обновления существующей вещи. business_logic, use_case, item_management Сценарий использования для создания новой метки. business_logic, use_case, label, create Получает список всех меток. domain, usecase Модель с данными, необходимыми для создания новой метки. data_structure, contract, label, create Модель данных для представления агрегированной статистики. data_structure, statistics Модель данных для создания новой "Вещи". data_structure, entity, input, create Представляет собой метку (тег), которую можно присвоить вещи. domain, model Модель с данными, необходимыми для обновления местоположения. data_structure, contract, location, update Модель данных, представляющая ответ от сервера с токеном аутентификации. data_transfer_object, authentication, model Представляет собой результат операции, который может быть либо успешным, либо неуспешным. domain, model, result Модель данных для представления кастомного поля. data_structure, entity, custom_field Представляет краткую информацию о метке, обычно возвращаемую после создания. data_structure, entity, label, summary Модель данных для представления местоположения (без счетчика). data_structure, entity, location Модель с данными, необходимыми для обновления метки. data_structure, contract, label, update Модель данных для представления изображения, привязанного к вещи. data_structure, entity, image Модель с данными, необходимыми для создания нового местоположения. data_structure, contract, location, create Data class to hold server credentials. domain, model, credentials Полная модель данных для представления "Вещи" со всеми полями. data_structure, entity, detailed Модель данных для обновления существующей "Вещи". data_structure, entity, input, update Модель данных для представления метки (тега). data_structure, entity, label Модель данных для представления вложения (файла), привязанного к вещи. data_structure, entity, attachment Сокращенная модель данных для представления "Вещи" в списках. data_structure, entity, summary Представляет собой статистику по инвентарю. domain, model Модель данных для представления местоположения со счетчиком вещей. data_structure, entity, location Представляет собой местоположение, где может находиться вещь. domain, model Генерик-класс для представления постраничных результатов от API. data_structure, generic, pagination Модель данных для записи о техническом обслуживании. data_structure, entity, maintenance Представляет собой вещь в инвентаре. domain, model Репозиторий для управления аутентификацией. authentication, data_access, repository Абстракция репозитория для работы с "Вещами". Определяет контракт, которому должен следовать слой данных. / interface ItemRepository { // [ENTITY: Function('createItem')] // [RELATION: Function('createItem')] -> [RETURNS] -> [DataClass('ItemSummary')] /** @summary Создает новый элемент. @param newItemData Данные для создания нового элемента. @return Сводка по созданному элементу. / suspend fun createItem(newItemData: ItemCreate): ItemSummary // [END_ENTITY: Function('createItem')] // [ENTITY: Function('getItemDetails')] // [RELATION: Function('getItemDetails')] -> [RETURNS] -> [DataClass('ItemOut')] /** @summary Получает детальную информацию об элементе. @param itemId ID элемента. @return Детальная информация об элементе. / suspend fun getItemDetails(itemId: String): ItemOut // [END_ENTITY: Function('getItemDetails')] // [ENTITY: Function('updateItem')] // [RELATION: Function('updateItem')] -> [RETURNS] -> [DataClass('ItemOut')] /** @summary Обновляет элемент. @param itemId ID элемента для обновления. @param item Данные для обновления элемента. @return Обновленная детальная информация об элементе. / suspend fun updateItem(itemId: String, item: ItemUpdate): ItemOut // [END_ENTITY: Function('updateItem')] // [ENTITY: Function('deleteItem')] /** @summary Удаляет элемент. @param itemId ID элемента для удаления. / suspend fun deleteItem(itemId: String) // [END_ENTITY: Function('deleteItem')] // [ENTITY: Function('syncInventory')] // [RELATION: Function('syncInventory')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')] /** @summary Синхронизирует инвентарь. @param page Номер страницы. @param pageSize Размер страницы. @return Результат пагинации со сводкой по элементам. / suspend fun syncInventory(page: Int, pageSize: Int): PaginationResult<ItemSummary> // [END_ENTITY: Function('syncInventory')] // [ENTITY: Function('getStatistics')] // [RELATION: Function('getStatistics')] -> [RETURNS] -> [DataClass('GroupStatistics')] /** @summary Получает статистику. @return Статистика по группам. / suspend fun getStatistics(): GroupStatistics // [END_ENTITY: Function('getStatistics')] // [ENTITY: Function('getAllLocations')] // [RELATION: Function('getAllLocations')] -> [RETURNS] -> [DataStructure('List<LocationOutCount>')] /** @summary Получает все местоположения. @return Список всех местоположений со счетчиками. / suspend fun getAllLocations(): List<LocationOutCount> // [END_ENTITY: Function('getAllLocations')] // [ENTITY: Function('getAllLabels')] // [RELATION: Function('getAllLabels')] -> [RETURNS] -> [DataStructure('List<LabelOut>')] /** @summary Получает все метки. @return Список всех меток. / suspend fun getAllLabels(): List<LabelOut> // [END_ENTITY: Function('getAllLabels')] // [ENTITY: Function('getLabelDetails')] // [RELATION: Function('getLabelDetails')] -> [RETURNS] -> [DataClass('LabelOut')] /** @summary Получает детальную информацию о метке. @param labelId ID метки. @return Детальная информация о метке. / suspend fun getLabelDetails(labelId: String): LabelOut // [END_ENTITY: Function('getLabelDetails')] // [ENTITY: Function('createLabel')] // [RELATION: Function('createLabel')] -> [RETURNS] -> [DataClass('LabelSummary')] /** @summary Создает новую метку. @param newLabelData Данные для создания новой метки. @return Сводка по созданной метке. / suspend fun createLabel(newLabelData: LabelCreate): LabelSummary // [END_ENTITY: Function('createLabel')] // [ENTITY: Function('updateLabel')] // [RELATION: Function('updateLabel')] -> [RETURNS] -> [DataClass('LabelOut')] /** @summary Обновляет метку. @param labelId ID метки для обновления. @param labelData Данные для обновления метки. @return Обновленная информация о метке. / suspend fun updateLabel(labelId: String, labelData: LabelUpdate): LabelOut // [END_ENTITY: Function('updateLabel')] // [ENTITY: Function('deleteLabel')] /** @summary Удаляет метку. @param labelId ID метки для удаления. / suspend fun deleteLabel(labelId: String) // [END_ENTITY: Function('deleteLabel')] // [ENTITY: Function('createLocation')] // [RELATION: Function('createLocation')] -> [RETURNS] -> [DataClass('LocationOut')] /** @summary Создает новое местоположение. @param newLocationData Данные для создания нового местоположения. @return Информация о созданном местоположении. / suspend fun createLocation(newLocationData: LocationCreate): LocationOut // [END_ENTITY: Function('createLocation')] // [ENTITY: Function('updateLocation')] // [RELATION: Function('updateLocation')] -> [RETURNS] -> [DataClass('LocationOut')] /** @summary Обновляет местоположение. @param locationId ID местоположения для обновления. @param locationData Данные для обновления местоположения. @return Обновленная информация о местоположении. / suspend fun updateLocation(locationId: String, locationData: LocationUpdate): LocationOut // [END_ENTITY: Function('updateLocation')] // [ENTITY: Function('deleteLocation')] /** @summary Удаляет местоположение. @param locationId ID местоположения для удаления. / suspend fun deleteLocation(locationId: String) // [END_ENTITY: Function('deleteLocation')] // [ENTITY: Function('searchItems')] // [RELATION: Function('searchItems')] -> [RETURNS] -> [DataClass('PaginationResult<ItemSummary>')] /** @summary Ищет элементы. @param query Поисковый запрос. @return Результат пагинации со сводкой по найденным элементам. / suspend fun searchItems(query: String): PaginationResult<ItemSummary> // [END_ENTITY: Function('searchItems')] // [ENTITY: Function('getRecentlyAddedItems')] // [RELATION: Function('getRecentlyAddedItems')] -> [RETURNS] -> [DataStructure('Flow<List<ItemSummary>>')] /** @summary Получает недавно добавленные элементы. @param limit Максимальное количество возвращаемых элементов. @return Поток со списком недавно добавленных элементов. / fun getRecentlyAddedItems(limit: Int): Flow<List<ItemSummary>> // [END_ENTITY: Function('getRecentlyAddedItems')] } data_access, abstraction, repository Repository for managing user credentials and session tokens. domain, repository, credentials Hilt-модуль для предоставления реализаций репозиториев. Использует `@Binds` для эффективного связывания интерфейсов с их реализациями. / @Module @InstallIn(SingletonComponent::class) abstract class RepositoryModule { // [ENTITY: Function('bindItemRepository')] // [RELATION: Function('bindItemRepository')] -> [PROVIDES] -> [Interface('ItemRepository')] /** @summary Связывает интерфейс ItemRepository с его реализацией. / @Binds @Singleton abstract fun bindItemRepository( itemRepositoryImpl: ItemRepositoryImpl ): ItemRepository // [END_ENTITY: Function('bindItemRepository')] // [ENTITY: Function('bindCredentialsRepository')] // [RELATION: Function('bindCredentialsRepository')] -> [PROVIDES] -> [Interface('CredentialsRepository')] /** @summary Связывает интерфейс CredentialsRepository с его реализацией. / @Binds @Singleton abstract fun bindCredentialsRepository( credentialsRepositoryImpl: CredentialsRepositoryImpl ): CredentialsRepository // [END_ENTITY: Function('bindCredentialsRepository')] // [ENTITY: Function('bindAuthRepository')] // [RELATION: Function('bindAuthRepository')] -> [PROVIDES] -> [Interface('AuthRepository')] /** @summary Связывает интерфейс AuthRepository с его реализацией. / @Binds @Singleton abstract fun bindAuthRepository( authRepositoryImpl: AuthRepositoryImpl ): AuthRepository // [END_ENTITY: Function('bindAuthRepository')] } dependency_injection, hilt, module, binding Hilt-модуль, отвечающий за создание и предоставление всех зависимостей, di, networking di, hilt, storage Предоставляет зависимости для работы с базой данных Room. di, hilt, database Определяет эндпоинты для взаимодействия с Homebox API, используя DTO. data, api, retrofit DTO для полной модели вещи. data_transfer_object, item_detailed Маппер из ItemOutDto в доменную модель ItemOut. data_transfer_object, item_detailed DTO для местоположения со счетчиком. data_transfer_object, location, count Маппер из LocationOutCountDto в доменную модель LocationOutCount. data_transfer_object, location, count DTO для создания вещи. data_transfer_object, item_creation Маппер из доменной модели ItemCreate в ItemCreateDto. data_transfer_object, item_creation data, dto, api, token DTO для полной информации о вещи (GET /v1/items/{id}). data, dto, api DTO для краткой информации о вещи в списках (GET /v1/items). data, dto, api DTO для создания новой вещи (POST /v1/items). data, dto, api DTO для обновления вещи (PUT /v1/items/{id}). data, dto, api data_transfer_object, location, output data_transfer_object, location, output DTO для постраничных результатов. data_transfer_object, pagination Маппер из PaginationResultDto в доменную модель PaginationResult. data_transfer_object, pagination DTO для пагинированных результатов от API. data, dto, api, pagination DTO для обновления вещи. data_transfer_object, item_update Маппер из доменной модели ItemUpdate в ItemUpdateDto. data_transfer_object, item_update data_transfer_object, location, update data_transfer_object, location, update DTO для изображения. data_transfer_object, image Маппер из ImageDto в доменную модель Image. data_transfer_object, image DTO для кастомного поля. data_transfer_object, custom_field Маппер из CustomFieldDto в доменную модель CustomField. data_transfer_object, custom_field DTO для статистики. data_transfer_object, statistics Маппер из GroupStatisticsDto в доменную модель GroupStatistics. data_transfer_object, statistics DTO для ответа от API при создании метки. data_transfer_object, label, summary, api, mapper Маппер из DTO в доменную модель. data_transfer_object, label, summary, api, mapper DTO для записи об обслуживании. data_transfer_object, maintenance Маппер из MaintenanceEntryDto в доменную модель MaintenanceEntry. data_transfer_object, maintenance data_transfer_object, location, create DTO для метки. data_transfer_object, label Маппер из LabelOutDto в доменную модель LabelOut. data_transfer_object, label DTO для статистической информации. data, dto, api, statistics DTO для информации о местоположении. data, dto, api, location DTO для информации о местоположении со счетчиком вещей. data, dto, api, location DTO для вложения. data_transfer_object, attachment Маппер из ItemAttachmentDto в доменную модель ItemAttachment. data_transfer_object, attachment data, dto, api, login DTO для тела запроса на создание метки (POST /v1/labels). data_transfer_object, label, create, api DTO для сокращенной модели вещи. data_transfer_object, item_summary Маппер из ItemSummaryDto в доменную модель ItemSummary. data_transfer_object, item_summary data_transfer_object, label, update data_transfer_object, label, update Преобразует DTO-объект токена в доменную модель. mapper, data_conversion, clean_architecture Основной класс для работы с локальной базой данных Room. data, database, room Представляет собой строку в таблице 'labels' в локальной БД. data, database, entity, label Таблица для связи "многие-ко-многим" между ItemEntity и LabelEntity. data, database, entity, relation Представляет собой строку в таблице 'items' в локальной БД. data, database, entity, item Представляет собой строку в таблице 'locations' в локальной БД. data, database, entity, location Преобразует [ItemWithLabels] (сущность БД) в [ItemSummary] (доменную модель). data, database, mapper Преобразует [LabelEntity] (сущность БД) в [LabelOut] (доменную модель). data, database, mapper POJO для получения ItemEntity вместе со связанными LabelEntity. data, database, entity, relation Предоставляет TypeConverters для Room для типов, не поддерживаемых по умолчанию. data, database, room, converter Предоставляет методы для работы с 'labels' в локальной БД. data, database, dao, label Предоставляет методы для работы с 'items' в локальной БД. data, database, dao, item Предоставляет методы для работы с 'locations' в локальной БД. data, database, dao, location 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)) } } 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() } // [ENTITY: Function('encrypt')] /** @summary Encrypts a byte array and writes it to an output stream. @param bytes The byte array to encrypt. @param outputStream The stream to write the encrypted data to. @return The encrypted byte array. / fun encrypt(bytes: ByteArray, outputStream: OutputStream): ByteArray { Timber.d("[DEBUG][ACTION][encrypting_data] Encrypting data.") 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 } // [END_ENTITY: Function('encrypt')] // [ENTITY: Function('decrypt')] /** @summary Decrypts a byte array from an input stream. @param inputStream The stream to read the encrypted data from. @return The decrypted byte array. / fun decrypt(inputStream: InputStream): ByteArray { Timber.d("[DEBUG][ACTION][decrypting_data] Decrypting data.") 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) } } // [END_ENTITY: Function('decrypt')] 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" } } data, security, cryptography Реализует репозиторий для управления учетными данными пользователя. Взаимодействует с зашифрованными SharedPreferences для сохранения и извлечения данных. @param encryptedPrefs Зашифрованное хранилище ключ-значение, предоставляемое Hilt. @invariant Состояние этого репозитория полностью зависит от содержимого `encryptedPrefs`. / class CredentialsRepositoryImpl @Inject constructor( private val encryptedPrefs: SharedPreferences ) : CredentialsRepository { 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" } // [ENTITY: Function('saveCredentials')] /** @summary Сохраняет основные учетные данные пользователя. @param credentials Объект с учетными данными для сохранения. @sideeffect Перезаписывает существующие учетные данные в SharedPreferences. / override suspend fun saveCredentials(credentials: Credentials) { withContext(Dispatchers.IO) { Timber.d("[DEBUG][ACTION][saving_credentials] Saving user credentials.") encryptedPrefs.edit() .putString(KEY_SERVER_URL, credentials.serverUrl) .putString(KEY_USERNAME, credentials.username) .putString(KEY_PASSWORD, credentials.password) .apply() } } // [END_ENTITY: Function('saveCredentials')] // [ENTITY: Function('getCredentials')] /** @summary Извлекает сохраненные учетные данные пользователя в виде потока. @return Flow, который эммитит объект [Credentials] или null, если данные отсутствуют. / override fun getCredentials(): Flow<Credentials?> = flow { Timber.d("[DEBUG][ACTION][getting_credentials] Getting user credentials.") val serverUrl = encryptedPrefs.getString(KEY_SERVER_URL, null) val username = encryptedPrefs.getString(KEY_USERNAME, null) val password = encryptedPrefs.getString(KEY_PASSWORD, null) if (serverUrl != null && username != null && password != null) { Timber.d("[DEBUG][SUCCESS][credentials_found] Found and emitting credentials.") emit(Credentials(serverUrl, username, password)) } else { Timber.d("[DEBUG][FALLBACK][no_credentials] No credentials found, emitting null.") emit(null) } }.flowOn(Dispatchers.IO) // [END_ENTITY: Function('getCredentials')] // [ENTITY: Function('saveToken')] /** @summary Сохраняет токен авторизации. @param token Токен для сохранения. @sideeffect Перезаписывает существующий токен в SharedPreferences. / override suspend fun saveToken(token: String) { withContext(Dispatchers.IO) { Timber.d("[DEBUG][ACTION][saving_token] Saving auth token.") encryptedPrefs.edit() .putString(KEY_AUTH_TOKEN, token) .apply() } } // [END_ENTITY: Function('saveToken')] // [ENTITY: Function('getToken')] /** @summary Извлекает сохраненный токен авторизации. @return Строка с токеном или null, если он не найден. / override suspend fun getToken(): String? { return withContext(Dispatchers.IO) { Timber.d("[DEBUG][ACTION][getting_token] Getting auth token.") encryptedPrefs.getString(KEY_AUTH_TOKEN, null) } } // [END_ENTITY: Function('getToken')] } data, repository, credentials, security data_repository, implementation, items, labels data_repository, implementation, items, labels data_repository, implementation, items, labels data_repository, implementation, items, labels Реализация репозитория для управления аутентификацией. data_implementation, authentication, repository 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 ) { // [ENTITY: Function('getString')] /** @summary 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. @sideeffect Reads from SharedPreferences. / fun getString(key: String, defaultValue: String?): String? { Timber.d("[DEBUG][ENTRYPOINT][getting_string] Attempting to get string for key: %s", key) val encryptedValue = sharedPreferences.getString(key, null) ?: return defaultValue.also { Timber.d("[DEBUG][FALLBACK][no_value_found] No value for key %s, returning default.", key) } return try { Timber.d("[DEBUG][ACTION][decoding_value] Decoding Base64 value.") val bytes = android.util.Base64.decode(encryptedValue, android.util.Base64.DEFAULT) Timber.d("[DEBUG][ACTION][decrypting_value] Decrypting value with CryptoManager.") val decryptedBytes = cryptoManager.decrypt(ByteArrayInputStream(bytes)) String(decryptedBytes, Charset.defaultCharset()).also { Timber.d("[DEBUG][SUCCESS][decryption_complete] Successfully decrypted value for key: %s", key) } } catch (e: Exception) { Timber.e(e, "[ERROR][EXCEPTION][decryption_failed] Failed to decrypt value for key: %s", key) defaultValue } } // [END_ENTITY: Function('getString')] // [ENTITY: Function('putString')] /** @summary 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) { Timber.d("[DEBUG][ENTRYPOINT][putting_string] Attempting to put string for key: %s", key) try { Timber.d("[DEBUG][ACTION][encrypting_value] Encrypting value with CryptoManager.") val outputStream = ByteArrayOutputStream() cryptoManager.encrypt(value.toByteArray(Charset.defaultCharset()), outputStream) val encryptedBytes = outputStream.toByteArray() Timber.d("[DEBUG][ACTION][encoding_value] Encoding encrypted value to Base64.") val encryptedValue = android.util.Base64.encodeToString(encryptedBytes, android.util.Base64.DEFAULT) Timber.d("[DEBUG][ACTION][writing_to_prefs] Writing encrypted value to SharedPreferences.") sharedPreferences.edit().putString(key, encryptedValue).apply() Timber.d("[DEBUG][SUCCESS][encryption_complete] Successfully encrypted and saved value for key: %s", key) } catch (e: Exception) { Timber.e(e, "[ERROR][EXCEPTION][encryption_failed] Failed to encrypt and save value for key: %s", key) } } // [END_ENTITY: Function('putString')] } data, security, preferences Маршрут для экрана настроек. data object Settings : Screen("settings_screen") navigation, route, settings Composable-функция для экрана "Настройки". Отображает UI для управления настройками приложения. ui, screen, settings, compose ViewModel для экрана "Настройки". Управляет состоянием и логикой экрана настроек. ui_logic, viewmodel, settings, state_management Состояние UI для экрана настроек. Содержит поля для URL сервера и других настроек. ui_state, data_model, immutable, settings Маршрут для экрана сканирования QR/штрих-кодов. data object Scan : Screen("scan_screen") navigation, route, scan Экран для сканирования QR-кодов и штрих-кодов. ui, screen, scan, camera, qrcode, barcode ViewModel для экрана сканирования. ui_logic, viewmodel, scan, camera Определяет все возможные состояния для UI экрана сканирования. ui_state, sealed_interface, contract