From c69f255fff058bc860109719426674e8f175001e Mon Sep 17 00:00:00 2001 From: busya Date: Sat, 9 Aug 2025 11:53:33 +0300 Subject: [PATCH] Labels + Location list --- .../com/homebox/lens/navigation/NavGraph.kt | 22 ++- .../ui/screen/labelslist/LabelsListScreen.kt | 168 ++++++++++++++++- .../ui/screen/labelslist/LabelsListUiState.kt | 36 ++++ .../screen/labelslist/LabelsListViewModel.kt | 65 ++++++- .../locationslist/LocationsListScreen.kt | 171 +++++++++++++++++- .../locationslist/LocationsListUiState.kt | 36 ++++ .../locationslist/LocationsListViewModel.kt | 65 ++++++- app/src/main/res/values-en/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + tech_spec/tech_spec.txt | 38 ++++ 10 files changed, 581 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListUiState.kt create mode 100644 app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListUiState.kt diff --git a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt index 665cf87..ed43c9c 100644 --- a/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt +++ b/app/src/main/java/com/homebox/lens/navigation/NavGraph.kt @@ -57,10 +57,28 @@ fun NavGraph() { ItemEditScreen() } composable(route = Screen.LabelsList.route) { - LabelsListScreen() + LabelsListScreen( + onNavigateBack = { navController.popBackStack() }, + onLabelClick = { labelId -> + // TODO: Navigate to a pre-filtered inventory list screen + navController.navigate(Screen.InventoryList.route) + }, + onAddNewLabelClick = { + // TODO: Navigate to a screen for creating a new label + } + ) } composable(route = Screen.LocationsList.route) { - LocationsListScreen() + LocationsListScreen( + onNavigateBack = { navController.popBackStack() }, + onLocationClick = { locationId -> + // TODO: Navigate to a pre-filtered inventory list screen + navController.navigate(Screen.InventoryList.route) + }, + onAddNewLocationClick = { + // TODO: Navigate to a screen for creating a new location + } + ) } composable(route = Screen.Search.route) { SearchScreen() diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt index a9f2978..a7d4f4d 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListScreen.kt @@ -1,22 +1,172 @@ // [PACKAGE] com.homebox.lens.ui.screen.labelslist // [FILE] LabelsListScreen.kt - +// [SEMANTICS] ui, screen, labels_list, compose package com.homebox.lens.ui.screen.labelslist -import androidx.compose.material3.Text +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Add +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.homebox.lens.R +import com.homebox.lens.domain.model.LabelOut +import com.homebox.lens.ui.theme.HomeboxLensTheme // [ENTRYPOINT] +/** + * [CONTRACT] + * @summary Главная Composable-функция для экрана списка меток. + * @param onNavigateBack Функция для навигации на предыдущий экран. + * @param onLabelClick Функция, вызываемая при нажатии на метку. + * @param onAddNewLabelClick Функция, вызываемая при нажатии на FAB для добавления новой метки. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun LabelsListScreen() { - // [ACTION] - Text(text = "Labels List Screen") +fun LabelsListScreen( + viewModel: LabelsListViewModel = hiltViewModel(), + onNavigateBack: () -> Unit, + onLabelClick: (String) -> Unit, + onAddNewLabelClick: () -> Unit +) { + // [STATE] + val uiState by viewModel.uiState.collectAsState() + + // [UI] + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(id = R.string.nav_labels)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.cd_navigate_back) + ) + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = onAddNewLabelClick) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(id = R.string.cd_add_new_label) + ) + } + } + ) { paddingValues -> + // [ANCHOR] Основной контент в зависимости от состояния + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + when (val state = uiState) { + is LabelsListUiState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + is LabelsListUiState.Error -> { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Center).padding(16.dp) + ) + } + is LabelsListUiState.Success -> { + LabelsList( + labels = state.labels, + onLabelClick = onLabelClick + ) + } + } + } + } } -@Preview(showBackground = true) +// [HELPER] +/** + * [CONTRACT] + * @summary Отображает список меток. + */ @Composable -fun LabelsListScreenPreview() { - LabelsListScreen() +private fun LabelsList( + labels: List, + onLabelClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(labels) { label -> + LabelListItem( + label = label, + onClick = { onLabelClick(label.id) } + ) + } + } } -// [END_FILE_LabelsListScreen.kt] + +// [HELPER] +/** + * [CONTRACT] + * @summary Отображает один элемент списка меток. + */ +@Composable +private fun LabelListItem( + label: LabelOut, + onClick: () -> Unit +) { + ListItem( + headlineContent = { Text(label.name) }, + leadingContent = { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = null + ) + }, + modifier = Modifier.clickable(onClick = onClick) + ) +} + +// [PREVIEW] +@Preview(showBackground = true, name = "Labels List Success") +@Composable +private fun LabelsListScreenSuccessPreview() { + val labels = listOf( + LabelOut("1", "Electronics", "#FF0000", false, "", ""), + LabelOut("2", "Books", "#00FF00", false, "", ""), + LabelOut("3", "Winter Clothes", "#0000FF", false, "", "") + ) + HomeboxLensTheme { + LabelsList(labels = labels, onLabelClick = {}) + } +} + +// [PREVIEW] +@Preview(showBackground = true, name = "Labels List Empty") +@Composable +private fun LabelsListScreenEmptyPreview() { + HomeboxLensTheme { + LabelsList(labels = emptyList(), onLabelClick = {}) + } +} +// [END_FILE_LabelsListScreen.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListUiState.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListUiState.kt new file mode 100644 index 0000000..c76a0fd --- /dev/null +++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListUiState.kt @@ -0,0 +1,36 @@ +// [PACKAGE] com.homebox.lens.ui.screen.labelslist +// [FILE] LabelsListUiState.kt +// [SEMANTICS] ui, state, labels_list +package com.homebox.lens.ui.screen.labelslist + +import com.homebox.lens.domain.model.LabelOut + +// [CORE-LOGIC] +// [ENTITY: SealedInterface('LabelsListUiState')] +/** + * [CONTRACT] + * Определяет все возможные состояния для экрана "Список меток". + * @invariant В любой момент времени экран может находиться только в одном из этих состояний. + */ +sealed interface LabelsListUiState { + /** + * [CONTRACT] + * Состояние успешной загрузки данных. + * @property labels Список меток. + */ + data class Success(val labels: List) : LabelsListUiState + + /** + * [CONTRACT] + * Состояние ошибки во время загрузки данных. + * @property message Человекочитаемое сообщение об ошибке. + */ + data class Error(val message: String) : LabelsListUiState + + /** + * [CONTRACT] + * Состояние, когда данные для экрана загружаются. + */ + data object Loading : LabelsListUiState +} +// [END_FILE_LabelsListUiState.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt index b09f0a4..3f7ad41 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/labelslist/LabelsListViewModel.kt @@ -1,16 +1,73 @@ // [PACKAGE] com.homebox.lens.ui.screen.labelslist // [FILE] LabelsListViewModel.kt - +// [SEMANTICS] ui_logic, labels_list, state_management package com.homebox.lens.ui.screen.labelslist import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.homebox.lens.domain.usecase.GetAllLabelsUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject // [VIEWMODEL] +// [ENTITY: ViewModel('LabelsListViewModel')] +/** + * [CONTRACT] + * @summary ViewModel для экрана со списком меток. + * @description Управляет состоянием экрана, загружает список меток и обрабатывает ошибки. + * @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`. + */ @HiltViewModel -class LabelsListViewModel @Inject constructor() : ViewModel() { +class LabelsListViewModel @Inject constructor( + private val getAllLabelsUseCase: GetAllLabelsUseCase +) : ViewModel() { + // [STATE] - // TODO: Implement UI state + private val _uiState = MutableStateFlow(LabelsListUiState.Loading) + val uiState = _uiState.asStateFlow() + + // [LIFECYCLE_HANDLER] + init { + loadLabels() + } + + /** + * [CONTRACT] + * @summary Загружает список меток. + * @description Выполняет `GetAllLabelsUseCase` и обновляет UI, переключая его + * между состояниями `Loading`, `Success` и `Error`. + * @sideeffect Асинхронно обновляет `_uiState`. + */ + fun loadLabels() { + // [ENTRYPOINT] + viewModelScope.launch { + _uiState.value = LabelsListUiState.Loading + Timber.i("[ACTION] Starting labels list load. State -> Loading.") + + // [CORE-LOGIC] + val result = runCatching { + getAllLabelsUseCase() + } + + // [RESULT_HANDLER] + result.fold( + onSuccess = { labels -> + Timber.i("[SUCCESS] Labels loaded successfully. Count: ${labels.size}. State -> Success.") + _uiState.value = LabelsListUiState.Success(labels) + }, + onFailure = { exception -> + Timber.e(exception, "[ERROR] Failed to load labels. State -> Error.") + _uiState.value = LabelsListUiState.Error( + message = exception.message ?: "Could not load labels." + ) + } + ) + } + } + // [END_CLASS_LabelsListViewModel] } -// [END_FILE_LabelsListViewModel.kt] +// [END_FILE_LabelsListViewModel.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt index 63940a2..93d837f 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListScreen.kt @@ -1,22 +1,175 @@ // [PACKAGE] com.homebox.lens.ui.screen.locationslist // [FILE] LocationsListScreen.kt - +// [SEMANTICS] ui, screen, locations_list, compose package com.homebox.lens.ui.screen.locationslist -import androidx.compose.material3.Text +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Place +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.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.homebox.lens.R +import com.homebox.lens.domain.model.LocationOutCount +import com.homebox.lens.ui.theme.HomeboxLensTheme // [ENTRYPOINT] +/** + * [CONTRACT] + * @summary Главная Composable-функция для экрана списка локаций. + * @param onNavigateBack Функция для навигации на предыдущий экран. + * @param onLocationClick Функция, вызываемая при нажатии на локацию. + * @param onAddNewLocationClick Функция, вызываемая при нажатии на FAB для добавления новой локации. + */ +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun LocationsListScreen() { - // [ACTION] - Text(text = "Locations List Screen") +fun LocationsListScreen( + viewModel: LocationsListViewModel = hiltViewModel(), + onNavigateBack: () -> Unit, + onLocationClick: (String) -> Unit, + onAddNewLocationClick: () -> Unit +) { + // [STATE] + val uiState by viewModel.uiState.collectAsState() + + // [UI] + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(id = R.string.nav_locations)) }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.cd_navigate_back) + ) + } + } + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = onAddNewLocationClick) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(id = R.string.cd_add_new_location) + ) + } + } + ) { paddingValues -> + // [ANCHOR] Основной контент в зависимости от состояния + Box( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + when (val state = uiState) { + is LocationsListUiState.Loading -> { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } + is LocationsListUiState.Error -> { + Text( + text = state.message, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.Center).padding(16.dp) + ) + } + is LocationsListUiState.Success -> { + LocationsList( + locations = state.locations, + onLocationClick = onLocationClick + ) + } + } + } + } } -@Preview(showBackground = true) +// [HELPER] +/** + * [CONTRACT] + * @summary Отображает список локаций. + */ @Composable -fun LocationsListScreenPreview() { - LocationsListScreen() +private fun LocationsList( + locations: List, + onLocationClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(locations) { location -> + LocationListItem( + location = location, + onClick = { onLocationClick(location.id) } + ) + } + } } -// [END_FILE_LocationsListScreen.kt] + +// [HELPER] +/** + * [CONTRACT] + * @summary Отображает один элемент списка локаций. + */ +@Composable +private fun LocationListItem( + location: LocationOutCount, + onClick: () -> Unit +) { + ListItem( + headlineContent = { Text(location.name) }, + leadingContent = { + Icon( + imageVector = Icons.Default.Place, + contentDescription = null + ) + }, + trailingContent = { + Text(text = location.itemCount.toString()) + }, + modifier = Modifier.clickable(onClick = onClick) + ) +} + +// [PREVIEW] +@Preview(showBackground = true, name = "Locations List Success") +@Composable +private fun LocationsListScreenSuccessPreview() { + val locations = listOf( + LocationOutCount("1", "Garage", "#FF0000", false, 12, "", ""), + LocationOutCount("2", "Kitchen", "#00FF00", false, 3, "", ""), + LocationOutCount("3", "Office", "#0000FF", false, 25, "", "") + ) + HomeboxLensTheme { + LocationsList(locations = locations, onLocationClick = {}) + } +} + +// [PREVIEW] +@Preview(showBackground = true, name = "Locations List Empty") +@Composable +private fun LocationsListScreenEmptyPreview() { + HomeboxLensTheme { + LocationsList(locations = emptyList(), onLocationClick = {}) + } +} +// [END_FILE_LocationsListScreen.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListUiState.kt b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListUiState.kt new file mode 100644 index 0000000..334c5d8 --- /dev/null +++ b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListUiState.kt @@ -0,0 +1,36 @@ +// [PACKAGE] com.homebox.lens.ui.screen.locationslist +// [FILE] LocationsListUiState.kt +// [SEMANTICS] ui, state, locations_list +package com.homebox.lens.ui.screen.locationslist + +import com.homebox.lens.domain.model.LocationOutCount + +// [CORE-LOGIC] +// [ENTITY: SealedInterface('LocationsListUiState')] +/** + * [CONTRACT] + * Определяет все возможные состояния для экрана "Список локаций". + * @invariant В любой момент времени экран может находиться только в одном из этих состояний. + */ +sealed interface LocationsListUiState { + /** + * [CONTRACT] + * Состояние успешной загрузки данных. + * @property locations Список локаций со счетчиками. + */ + data class Success(val locations: List) : LocationsListUiState + + /** + * [CONTRACT] + * Состояние ошибки во время загрузки данных. + * @property message Человекочитаемое сообщение об ошибке. + */ + data class Error(val message: String) : LocationsListUiState + + /** + * [CONTRACT] + * Состояние, когда данные для экрана загружаются. + */ + data object Loading : LocationsListUiState +} +// [END_FILE_LocationsListUiState.kt] \ No newline at end of file diff --git a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt index 90a1dec..17bc201 100644 --- a/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt +++ b/app/src/main/java/com/homebox/lens/ui/screen/locationslist/LocationsListViewModel.kt @@ -1,16 +1,73 @@ // [PACKAGE] com.homebox.lens.ui.screen.locationslist // [FILE] LocationsListViewModel.kt - +// [SEMANTICS] ui_logic, locations_list, state_management package com.homebox.lens.ui.screen.locationslist import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.homebox.lens.domain.usecase.GetAllLocationsUseCase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject // [VIEWMODEL] +// [ENTITY: ViewModel('LocationsListViewModel')] +/** + * [CONTRACT] + * @summary ViewModel для экрана со списком локаций. + * @description Управляет состоянием экрана, загружает список локаций и обрабатывает ошибки. + * @invariant `uiState` всегда является одним из состояний, определенных в `LocationsListUiState`. + */ @HiltViewModel -class LocationsListViewModel @Inject constructor() : ViewModel() { +class LocationsListViewModel @Inject constructor( + private val getAllLocationsUseCase: GetAllLocationsUseCase +) : ViewModel() { + // [STATE] - // TODO: Implement UI state + private val _uiState = MutableStateFlow(LocationsListUiState.Loading) + val uiState = _uiState.asStateFlow() + + // [LIFECYCLE_HANDLER] + init { + loadLocations() + } + + /** + * [CONTRACT] + * @summary Загружает список локаций. + * @description Выполняет `GetAllLocationsUseCase` и обновляет UI, переключая его + * между состояниями `Loading`, `Success` и `Error`. + * @sideeffect Асинхронно обновляет `_uiState`. + */ + fun loadLocations() { + // [ENTRYPOINT] + viewModelScope.launch { + _uiState.value = LocationsListUiState.Loading + Timber.i("[ACTION] Starting locations list load. State -> Loading.") + + // [CORE-LOGIC] + val result = runCatching { + getAllLocationsUseCase() + } + + // [RESULT_HANDLER] + result.fold( + onSuccess = { locations -> + Timber.i("[SUCCESS] Locations loaded successfully. Count: ${locations.size}. State -> Success.") + _uiState.value = LocationsListUiState.Success(locations) + }, + onFailure = { exception -> + Timber.e(exception, "[ERROR] Failed to load locations. State -> Error.") + _uiState.value = LocationsListUiState.Error( + message = exception.message ?: "Could not load locations." + ) + } + ) + } + } + // [END_CLASS_LocationsListViewModel] } -// [END_FILE_LocationsListViewModel.kt] +// [END_FILE_LocationsListViewModel.kt] \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 5519af4..0540c94 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -12,6 +12,9 @@ Open navigation drawer Scan QR code + Navigate back + Add new location + Add new label Dashboard @@ -28,5 +31,6 @@ Locations + Labels \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5ba762b..addea58 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,6 +12,9 @@ Открыть боковое меню Сканировать QR-код + Вернуться назад + Добавить новую локацию + Добавить новую метку Главная @@ -28,5 +31,6 @@ Локации + Метки \ No newline at end of file diff --git a/tech_spec/tech_spec.txt b/tech_spec/tech_spec.txt index 5354374..91818dd 100644 --- a/tech_spec/tech_spec.txt +++ b/tech_spec/tech_spec.txt @@ -231,6 +231,44 @@ + + + + Экран "Метки" + + Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения. + + + + Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад". + + + Основная область контента, занимающая все доступное пространство под TopAppBar. + + Вертикальный, прокручиваемый список (LazyColumn) всех меток. + + Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой. + + + + + + Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку. + + + + + + Нажатие на элемент списка меток + Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке. + + + Нажатие на FloatingActionButton + Открывается диалоговое окно или новый экран для создания новой метки. + + + +