2 Commits

Author SHA1 Message Date
c69f255fff Labels + Location list 2025-08-09 11:53:33 +03:00
8db12a7599 Add start dashboard 2025-08-09 11:34:40 +03:00
11 changed files with 1130 additions and 101 deletions

View File

@@ -36,7 +36,16 @@ fun NavGraph() {
}) })
} }
composable(route = Screen.Dashboard.route) { composable(route = Screen.Dashboard.route) {
DashboardScreen() DashboardScreen(
onNavigateToLocations = { navController.navigate(Screen.LocationsList.route) },
onNavigateToSearch = { navController.navigate(Screen.Search.route) },
onNavigateToCreateItem = { navController.navigate(Screen.ItemEdit.createRoute("new")) },
onLogout = {
navController.navigate(Screen.Setup.route) {
popUpTo(Screen.Dashboard.route) { inclusive = true }
}
}
)
} }
composable(route = Screen.InventoryList.route) { composable(route = Screen.InventoryList.route) {
InventoryListScreen() InventoryListScreen()
@@ -48,10 +57,28 @@ fun NavGraph() {
ItemEditScreen() ItemEditScreen()
} }
composable(route = Screen.LabelsList.route) { 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) { 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) { composable(route = Screen.Search.route) {
SearchScreen() SearchScreen()

View File

@@ -1,100 +1,385 @@
// [PACKAGE] com.homebox.lens.ui.screen.dashboard // [PACKAGE] com.homebox.lens.ui.screen.dashboard
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt // [FILE] DashboardScreen.kt
// [SEMANTICS] ui, screen, dashboard, compose
// [IMPORTS]
package com.homebox.lens.ui.screen.dashboard package com.homebox.lens.ui.screen.dashboard
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.Scaffold import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import timber.log.Timber import com.homebox.lens.R
import com.homebox.lens.domain.model.*
import com.homebox.lens.ui.theme.HomeboxLensTheme
import kotlinx.coroutines.launch
// [CORE-LOGIC] // [ANCHOR] Главная точка входа для экрана Dashboard
/** @OptIn(ExperimentalMaterial3Api::class)
* [CONTRACT]
* Главный Composable для экрана "Дэшборд".
* @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
*/
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel() viewModel: DashboardViewModel = hiltViewModel(),
onNavigateToLocations: () -> Unit,
onNavigateToSearch: () -> Unit,
onNavigateToCreateItem: () -> Unit,
onLogout: () -> Unit
) { ) {
// [ACTION] Собираем состояние из ViewModel
val uiState by viewModel.uiState.collectAsState() val uiState by viewModel.uiState.collectAsState()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
Scaffold { paddingValues -> // [ANCHOR] Определяем навигационное меню
Box( ModalNavigationDrawer(
modifier = Modifier drawerState = drawerState,
.fillMaxSize() drawerContent = {
.padding(paddingValues) DrawerContent(
) { onNavigateToLocations = onNavigateToLocations,
when (val state = uiState) { onNavigateToSearch = onNavigateToSearch,
is DashboardUiState.Loading -> { onNavigateToCreateItem = onNavigateToCreateItem,
// [UI-ACTION] Показываем индикатор загрузки onLogout = onLogout,
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) onCloseDrawer = { scope.launch { drawerState.close() } }
)
}
) {
// [ANCHOR] Основной Scaffold экрана
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.dashboard_title)) },
navigationIcon = {
IconButton(onClick = { scope.launch { drawerState.open() } }) {
Icon(Icons.Default.Menu, contentDescription = stringResource(id = R.string.cd_open_navigation_drawer))
}
},
actions = {
IconButton(onClick = { /* TODO: Handle scanner click */ }) {
Icon(Icons.Default.Search, contentDescription = stringResource(id = R.string.cd_scan_qr_code))
}
}
)
}
) { paddingValues ->
// [ANCHOR] Основной контент экрана
DashboardContent(
modifier = Modifier.padding(paddingValues),
uiState = uiState,
onLocationClick = { /* TODO */ },
onLabelClick = { /* TODO */ }
)
}
}
}
// [ANCHOR] Компонент основного контента
@Composable
private fun DashboardContent(
modifier: Modifier = Modifier,
uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit
) {
// [FIX] Based on the UiState, we decide what to show
when (uiState) {
is DashboardUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
}
is DashboardUiState.Error -> {
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.Center) {
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
}
}
is DashboardUiState.Success -> {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
// [ANCHOR] Секция "Быстрая статистика"
item {
StatisticsSection(statistics = uiState.statistics)
} }
is DashboardUiState.Error -> {
// [UI-ACTION] Показываем сообщение об ошибке // [ANCHOR] Секция "Недавно добавлено"
val errorMessage = "Error: ${state.message}" item {
Text( // TODO: Add recently added items to UiState and display them here
text = errorMessage, // RecentlyAddedSection(items = uiState.recentlyAddedItems)
modifier = Modifier.align(Alignment.Center)
)
Timber.w("[UI-STATE] Displaying Error: $errorMessage")
} }
is DashboardUiState.Success -> {
// [UI-ACTION] Отображаем основной контент // [ANCHOR] Секция "Места хранения"
Timber.d("[UI-STATE] Displaying Success") item {
DashboardContent(state) LocationsSection(locations = uiState.locations, onLocationClick = onLocationClick)
}
// [ANCHOR] Секция "Метки"
item {
LabelsSection(labels = uiState.labels, onLabelClick = onLabelClick)
}
item { Spacer(modifier = Modifier.height(16.dp)) }
}
}
}
}
// [ANCHOR] Секция статистики
@Composable
private fun StatisticsSection(statistics: GroupStatistics) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_quick_stats),
style = MaterialTheme.typography.titleMedium
)
Card {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier.height(120.dp).fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_items), value = statistics.items.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_value), value = statistics.totalValue.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_labels), value = statistics.labels.toString()) }
item { StatisticCard(title = stringResource(id = R.string.dashboard_stat_total_locations), value = statistics.locations.toString()) }
}
}
}
}
@Composable
private fun StatisticCard(title: String, value: String) {
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
Text(text = title, style = MaterialTheme.typography.labelMedium, textAlign = TextAlign.Center)
Text(text = value, style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center)
}
}
// [ANCHOR] Секция недавно добавленных
@Composable
private fun RecentlyAddedSection(items: List<ItemSummary>) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_recently_added),
style = MaterialTheme.typography.titleMedium
)
if (items.isEmpty()) {
Text(
text = stringResource(id = R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center
)
} else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
items(items) { item ->
ItemCard(item = item)
} }
} }
} }
} }
} }
/**
* [CONTRACT]
* Composable для отображения успешного состояния дэшборда.
* @param state Состояние UI с данными.
*/
@Composable @Composable
fun DashboardContent(state: DashboardUiState.Success) { private fun ItemCard(item: ItemSummary) {
Column( Card(modifier = Modifier.width(150.dp)) {
modifier = Modifier Column(modifier = Modifier.padding(8.dp)) {
.fillMaxSize() // TODO: Add image here from item.image
.padding(16.dp), Spacer(modifier = Modifier.height(80.dp).fillMaxWidth().background(MaterialTheme.colorScheme.secondaryContainer))
verticalArrangement = Arrangement.spacedBy(16.dp) Spacer(modifier = Modifier.height(8.dp))
) { Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
// [UI-COMPONENT] Статистика Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
Text(text = "Statistics:")
Text(text = " Items: ${state.statistics.items}")
Text(text = " Locations: ${state.statistics.locations}")
Text(text = " Labels: ${state.statistics.labels}")
Text(text = " Total Value: ${state.statistics.totalValue}")
// [UI-COMPONENT] Локации
Text(text = "Locations:")
state.locations.forEach { location ->
Text(text = " - ${location.name} (${location.itemCount})")
}
// [UI-COMPONENT] Метки
Text(text = "Labels:")
state.labels.forEach { label ->
Text(text = " - ${label.name}")
} }
} }
} }
// [END_FILE_DashboardScreen.kt]
// [ANCHOR] Секция местоположений
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_locations),
style = MaterialTheme.typography.titleMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
locations.forEach { location ->
SuggestionChip(
onClick = { onLocationClick(location) },
label = { Text("${location.name} (${location.itemCount})") }
)
}
}
}
}
// [ANCHOR] Секция меток
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(id = R.string.dashboard_section_labels),
style = MaterialTheme.typography.titleMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
labels.forEach { label ->
SuggestionChip(
onClick = { onLabelClick(label) },
label = { Text(label.name) }
)
}
}
}
}
// [ANCHOR] Контент бокового меню
@Composable
private fun DrawerContent(
onNavigateToLocations: () -> Unit,
onNavigateToSearch: () -> Unit,
onNavigateToCreateItem: () -> Unit,
onLogout: () -> Unit,
onCloseDrawer: () -> Unit
) {
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
Button(
onClick = {
onNavigateToCreateItem()
onCloseDrawer()
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text(stringResource(id = R.string.create))
}
Spacer(Modifier.height(12.dp))
Divider()
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.dashboard_title)) },
selected = true,
onClick = { onCloseDrawer() }
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.nav_locations)) },
selected = false,
onClick = {
onNavigateToLocations()
onCloseDrawer()
}
)
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.search)) },
selected = false,
onClick = {
onNavigateToSearch()
onCloseDrawer()
}
)
// TODO: Add Profile and Tools items
Divider()
NavigationDrawerItem(
label = { Text(stringResource(id = R.string.logout)) },
selected = false,
onClick = {
onLogout()
onCloseDrawer()
}
)
}
}
// [ANCHOR] Preview для DashboardContent
@Preview(showBackground = true, name = "Dashboard Success State")
@Composable
fun DashboardContentSuccessPreview() {
val previewState = DashboardUiState.Success(
statistics = GroupStatistics(
items = 123,
totalValue = 9999.99,
locations = 5,
labels = 8
),
locations = listOf(
LocationOutCount(id="1", name="Office", color = "#FF0000", isArchived = false, itemCount = 10, createdAt = "", updatedAt = ""),
LocationOutCount(id="2", name="Garage", color = "#00FF00", isArchived = false, itemCount = 5, createdAt = "", updatedAt = ""),
LocationOutCount(id="3",name="Living Room", color = "#0000FF", isArchived = false, itemCount = 15, createdAt = "", updatedAt = ""),
LocationOutCount(id="4",name="Kitchen", color = "#FFFF00", isArchived = false, itemCount = 20, createdAt = "", updatedAt = ""),
LocationOutCount(id="5",name="Basement", color = "#00FFFF", isArchived = false, itemCount = 3, createdAt = "", updatedAt = "")
),
labels = listOf(
LabelOut(id="1", name="electronics", color = "#FF0000", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="2", name="important", color = "#00FF00", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="3", name="seasonal", color = "#0000FF", isArchived = false, createdAt = "", updatedAt = ""),
LabelOut(id="4", name="hobby", color = "#FFFF00", isArchived = false, createdAt = "", updatedAt = "")
)
)
HomeboxLensTheme {
DashboardContent(
uiState = previewState,
onLocationClick = {},
onLabelClick = {}
)
}
}
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
fun DashboardContentLoadingPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Loading,
onLocationClick = {},
onLabelClick = {}
)
}
}
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
fun DashboardContentErrorPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
onLocationClick = {},
onLabelClick = {}
)
}
}

View File

@@ -1,22 +1,172 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist // [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListScreen.kt // [FILE] LabelsListScreen.kt
// [SEMANTICS] ui, screen, labels_list, compose
package com.homebox.lens.ui.screen.labelslist 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.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.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] // [ENTRYPOINT]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана списка меток.
* @param onNavigateBack Функция для навигации на предыдущий экран.
* @param onLabelClick Функция, вызываемая при нажатии на метку.
* @param onAddNewLabelClick Функция, вызываемая при нажатии на FAB для добавления новой метки.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LabelsListScreen() { fun LabelsListScreen(
// [ACTION] viewModel: LabelsListViewModel = hiltViewModel(),
Text(text = "Labels List Screen") 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 @Composable
fun LabelsListScreenPreview() { private fun LabelsList(
LabelsListScreen() labels: List<LabelOut>,
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) }
)
}
}
}
// [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] // [END_FILE_LabelsListScreen.kt]

View File

@@ -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<LabelOut>) : LabelsListUiState
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : LabelsListUiState
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
data object Loading : LabelsListUiState
}
// [END_FILE_LabelsListUiState.kt]

View File

@@ -1,16 +1,73 @@
// [PACKAGE] com.homebox.lens.ui.screen.labelslist // [PACKAGE] com.homebox.lens.ui.screen.labelslist
// [FILE] LabelsListViewModel.kt // [FILE] LabelsListViewModel.kt
// [SEMANTICS] ui_logic, labels_list, state_management
package com.homebox.lens.ui.screen.labelslist package com.homebox.lens.ui.screen.labelslist
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLabelsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel 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 import javax.inject.Inject
// [VIEWMODEL] // [VIEWMODEL]
// [ENTITY: ViewModel('LabelsListViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком меток.
* @description Управляет состоянием экрана, загружает список меток и обрабатывает ошибки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LabelsListUiState`.
*/
@HiltViewModel @HiltViewModel
class LabelsListViewModel @Inject constructor() : ViewModel() { class LabelsListViewModel @Inject constructor(
private val getAllLabelsUseCase: GetAllLabelsUseCase
) : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI state private val _uiState = MutableStateFlow<LabelsListUiState>(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]

View File

@@ -1,22 +1,175 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist // [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListScreen.kt // [FILE] LocationsListScreen.kt
// [SEMANTICS] ui, screen, locations_list, compose
package com.homebox.lens.ui.screen.locationslist 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.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.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] // [ENTRYPOINT]
/**
* [CONTRACT]
* @summary Главная Composable-функция для экрана списка локаций.
* @param onNavigateBack Функция для навигации на предыдущий экран.
* @param onLocationClick Функция, вызываемая при нажатии на локацию.
* @param onAddNewLocationClick Функция, вызываемая при нажатии на FAB для добавления новой локации.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LocationsListScreen() { fun LocationsListScreen(
// [ACTION] viewModel: LocationsListViewModel = hiltViewModel(),
Text(text = "Locations List Screen") 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 @Composable
fun LocationsListScreenPreview() { private fun LocationsList(
LocationsListScreen() locations: List<LocationOutCount>,
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) }
)
}
}
}
// [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] // [END_FILE_LocationsListScreen.kt]

View File

@@ -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<LocationOutCount>) : LocationsListUiState
/**
* [CONTRACT]
* Состояние ошибки во время загрузки данных.
* @property message Человекочитаемое сообщение об ошибке.
*/
data class Error(val message: String) : LocationsListUiState
/**
* [CONTRACT]
* Состояние, когда данные для экрана загружаются.
*/
data object Loading : LocationsListUiState
}
// [END_FILE_LocationsListUiState.kt]

View File

@@ -1,16 +1,73 @@
// [PACKAGE] com.homebox.lens.ui.screen.locationslist // [PACKAGE] com.homebox.lens.ui.screen.locationslist
// [FILE] LocationsListViewModel.kt // [FILE] LocationsListViewModel.kt
// [SEMANTICS] ui_logic, locations_list, state_management
package com.homebox.lens.ui.screen.locationslist package com.homebox.lens.ui.screen.locationslist
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.homebox.lens.domain.usecase.GetAllLocationsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel 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 import javax.inject.Inject
// [VIEWMODEL] // [VIEWMODEL]
// [ENTITY: ViewModel('LocationsListViewModel')]
/**
* [CONTRACT]
* @summary ViewModel для экрана со списком локаций.
* @description Управляет состоянием экрана, загружает список локаций и обрабатывает ошибки.
* @invariant `uiState` всегда является одним из состояний, определенных в `LocationsListUiState`.
*/
@HiltViewModel @HiltViewModel
class LocationsListViewModel @Inject constructor() : ViewModel() { class LocationsListViewModel @Inject constructor(
private val getAllLocationsUseCase: GetAllLocationsUseCase
) : ViewModel() {
// [STATE] // [STATE]
// TODO: Implement UI state private val _uiState = MutableStateFlow<LocationsListUiState>(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]

View File

@@ -0,0 +1,36 @@
<resources>
<string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Create</string>
<string name="search">Search</string>
<string name="logout">Logout</string>
<string name="no_location">No location</string>
<string name="items_not_found">Items not found</string>
<string name="error_loading_failed">Failed to load data. Please try again.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Open navigation drawer</string>
<string name="cd_scan_qr_code">Scan QR code</string>
<string name="cd_navigate_back">Navigate back</string>
<string name="cd_add_new_location">Add new location</string>
<string name="cd_add_new_label">Add new label</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Dashboard</string>
<string name="dashboard_section_quick_stats">Quick Stats</string>
<string name="dashboard_section_recently_added">Recently Added</string>
<string name="dashboard_section_locations">Locations</string>
<string name="dashboard_section_labels">Labels</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Total Items</string>
<string name="dashboard_stat_total_value">Total Value</string>
<string name="dashboard_stat_total_labels">Total Labels</string>
<string name="dashboard_stat_total_locations">Total Locations</string>
<!-- Navigation -->
<string name="nav_locations">Locations</string>
<string name="nav_labels">Labels</string>
</resources>

View File

@@ -1,3 +1,36 @@
<resources> <resources>
<string name="app_name">Homebox Lens</string> <string name="app_name">Homebox Lens</string>
<!-- Common -->
<string name="create">Создать</string>
<string name="search">Поиск</string>
<string name="logout">Выйти</string>
<string name="no_location">Нет локации</string>
<string name="items_not_found">Элементы не найдены</string>
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
<!-- Content Descriptions -->
<string name="cd_open_navigation_drawer">Открыть боковое меню</string>
<string name="cd_scan_qr_code">Сканировать QR-код</string>
<string name="cd_navigate_back">Вернуться назад</string>
<string name="cd_add_new_location">Добавить новую локацию</string>
<string name="cd_add_new_label">Добавить новую метку</string>
<!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string>
<string name="dashboard_section_quick_stats">Быстрая статистика</string>
<string name="dashboard_section_recently_added">Недавно добавлено</string>
<string name="dashboard_section_locations">Места хранения</string>
<string name="dashboard_section_labels">Метки</string>
<!-- Dashboard Statistics -->
<string name="dashboard_stat_total_items">Всего вещей</string>
<string name="dashboard_stat_total_value">Общая стоимость</string>
<string name="dashboard_stat_total_labels">Всего меток</string>
<string name="dashboard_stat_total_locations">Всего локаций</string>
<!-- Navigation -->
<string name="nav_locations">Локации</string>
<string name="nav_labels">Метки</string>
</resources> </resources>

View File

@@ -10,6 +10,41 @@
<summary>Библиотека логирования</summary> <summary>Библиотека логирования</summary>
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description> <description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
</DECISION> </DECISION>
<DECISION id="tech_i18n" status="defined">
<summary>Интернационализация (Мультиязычность)</summary>
<description>
Приложение должно поддерживать несколько языков для обеспечения доступности для глобальной аудитории.
Реализация будет основана на стандартном механизме ресурсов Android.
- Все строки, видимые пользователю, должны быть вынесены в файл `app/src/main/res/values/strings.xml`. Использование жестко закодированных строк в коде запрещено.
- Язык по умолчанию - русский (ru). Файл `strings.xml` будет содержать русские строки.
- Для поддержки других языков (например, английского - en) будут создаваться соответствующие каталоги ресурсов (например, `app/src/main/res/values-en/strings.xml`).
- В коде для доступа к строкам необходимо использовать ссылки на ресурсы (например, `R.string.app_name`).
</description>
</DECISION>
<DECISION id="tech_ui_framework" status="defined">
<summary>UI Framework</summary>
<description>Пользовательский интерфейс приложения построен с использованием Jetpack Compose, современного декларативного UI-фреймворка от Google. Это обеспечивает быстрое создание, гибкость и поддержку динамических данных.</description>
</DECISION>
<DECISION id="tech_di" status="defined">
<summary>Внедрение зависимостей (Dependency Injection)</summary>
<description>Для управления зависимостями в проекте используется Hilt. Он интегрирован с компонентами Jetpack и упрощает внедрение зависимостей в Android-приложениях.</description>
</DECISION>
<DECISION id="tech_navigation" status="defined">
<summary>Навигация</summary>
<description>Навигация между экранами (Composable-функциями) реализована с помощью библиотеки Navigation Compose, которая является частью Jetpack Navigation.</description>
</DECISION>
<DECISION id="tech_async" status="defined">
<summary>Асинхронные операции</summary>
<description>Все асинхронные операции, такие как сетевые запросы или доступ к базе данных, выполняются с использованием Kotlin Coroutines. Это обеспечивает эффективное управление фоновыми задачами без блокировки основного потока.</description>
</DECISION>
<DECISION id="tech_networking" status="defined">
<summary>Сетевое взаимодействие</summary>
<description>Для взаимодействия с API сервера Homebox используется стек технологий: Retrofit для создания типобезопасных HTTP-клиентов, OkHttp в качестве HTTP-клиента и Moshi для парсинга JSON.</description>
</DECISION>
<DECISION id="tech_database" status="defined">
<summary>Локальное хранилище</summary>
<description>Для кэширования данных на устройстве используется библиотека Room. Она предоставляет абстракцию над SQLite и обеспечивает надежное локальное хранение данных.</description>
</DECISION>
</TECHNICAL_DECISIONS> </TECHNICAL_DECISIONS>
<FEATURES> <FEATURES>
@@ -113,6 +148,130 @@
</FEATURE> </FEATURE>
</FEATURES> </FEATURES>
<UI_SPECIFICATIONS>
<SCREEN id="screen_dashboard" status="defined">
<summary>Главный экран "Панель управления"</summary>
<description>
Экран предоставляет обзорную информацию и быстрый доступ к основным функциям. Компоновка должна быть чистой и интуитивно понятной, аналогично веб-интерфейсу HomeBox.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Верхняя панель приложения. Содержит иконку навигационного меню (гамбургер), название/логотип приложения и иконку для запуска сканера (например, QR-кода).</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Боковое навигационное меню. Открывается по нажатию на иконку в TopAppBar. Содержит основные разделы: Главная, Локации, Поиск, Профиль, Инструменты, а также кнопку "Выйти".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical" scrollable="true">
<description>Основная область контента. Содержит несколько информационных блоков.</description>
<SUB_COMPONENT type="Section" title="Быстрая статистика">
<description>Сетка из 2x2 карточек, отображающих ключевые метрики.</description>
<ELEMENT type="Card" name="Общая стоимость" />
<ELEMENT type="Card" name="Всего вещей" />
<ELEMENT type="Card" name="Общее количество местоположений" />
<ELEMENT type="Card" name="Всего меток" />
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Недавно добавлено">
<description>Горизонтально прокручиваемый список карточек недавно добавленных предметов. Если предметов нет, отображается сообщение "Элементы не найдены".</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Места хранения">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими местоположения. Нажатие на чип ведет к списку предметов в этом местоположении.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="Section" title="Метки">
<description>Сетка или гибкий контейнер (FlowRow) с кликабельными чипами, представляющими метки. Нажатие на чип ведет к списку предметов с этой меткой.</description>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton_or_PrimaryButton" icon="add">
<description>
Вместо плавающей кнопки (FAB), в референсе используется заметная кнопка "Создать" в навигационном меню. Мы будем придерживаться этого подхода для консистентности. Эта кнопка инициирует процесс создания нового предмета.
</description>
</COMPONENT>
</LAYOUT>
</SCREEN>
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Локаций. Добавлено на основе референса HomeBox. -->
<SCREEN id="screen_locations_list" status="defined">
<summary>Экран "Локации"</summary>
<description>
Отображает вертикальный список всех доступных местоположений. Экран должен быть интегрирован в общую структуру навигации приложения (TopAppBar, NavigationDrawer).
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения, аналогичная экрану "Панель управления".</description>
</COMPONENT>
<COMPONENT type="NavigationDrawer">
<description>Общее боковое меню навигации.</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="Header" title="Локации">
<description>Заголовок экрана, расположенный вверху основной области контента.</description>
</SUB_COMPONENT>
<SUB_COMPONENT type="List" name="LocationsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех местоположений.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одно местоположение. Состоит из иконки (например, 'place') и названия местоположения. Весь элемент является кликабельным и ведет на экран со списком предметов в данной локации.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новое местоположение. В веб-версии для этого используются иконки в углу, но FAB является более нативным паттерном для Android.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка локаций</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной локации.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания нового местоположения.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Локаций. -->
<!-- [ЯКОРЬ] Начало спецификации UI для экрана Меток. -->
<SCREEN id="screen_labels_list" status="defined">
<summary>Экран "Метки"</summary>
<description>
Отображает вертикальный список всех доступных меток. Экран должен быть интегрирован в общую структуру навигации приложения.
</description>
<LAYOUT>
<COMPONENT type="TopAppBar">
<description>Общая верхняя панель приложения с заголовком "Метки" и кнопкой "назад".</description>
</COMPONENT>
<COMPONENT type="MainContent" orientation="vertical">
<description>Основная область контента, занимающая все доступное пространство под TopAppBar.</description>
<SUB_COMPONENT type="List" name="LabelsList">
<description>Вертикальный, прокручиваемый список (LazyColumn) всех меток.</description>
<ELEMENT type="ListItem">
<description>Элемент списка, представляющий одну метку. Состоит из иконки (например, 'label') и названия метки. Весь элемент является кликабельным и ведет на экран со списком предметов с данной меткой.</description>
</ELEMENT>
</SUB_COMPONENT>
</COMPONENT>
<COMPONENT type="FloatingActionButton" icon="add">
<description>
Плавающая кнопка действия, расположенная в правом нижнем углу. Позволяет пользователю добавить новую метку.
</description>
</COMPONENT>
</LAYOUT>
<USER_INTERACTIONS>
<INTERACTION>
<action>Нажатие на элемент списка меток</action>
<reaction>Осуществляется навигация на экран списка инвентаря, отфильтрованного по выбранной метке.</reaction>
</INTERACTION>
<INTERACTION>
<action>Нажатие на FloatingActionButton</action>
<reaction>Открывается диалоговое окно или новый экран для создания новой метки.</reaction>
</INTERACTION>
</USER_INTERACTIONS>
</SCREEN>
<!-- [ЯКОРЬ] Конец спецификации UI для экрана Меток. -->
</UI_SPECIFICATIONS>
<IMPLEMENTATION_MAP> <IMPLEMENTATION_MAP>
<!-- Use Cases --> <!-- Use Cases -->
<USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" /> <USE_CASE id="uc_get_stats" file_ref="domain/src/main/java/com/homebox/lens/domain/usecase/GetStatisticsUseCase.kt" />