Add start dashboard
This commit is contained in:
@@ -36,7 +36,16 @@ fun NavGraph() {
|
||||
})
|
||||
}
|
||||
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) {
|
||||
InventoryListScreen()
|
||||
|
||||
@@ -1,100 +1,385 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.dashboard
|
||||
// [FILE] app/src/main/java/com/homebox/lens/ui/screen/dashboard/DashboardScreen.kt
|
||||
// [SEMANTICS] ui, screen, dashboard, compose
|
||||
// [FILE] DashboardScreen.kt
|
||||
|
||||
// [IMPORTS]
|
||||
package com.homebox.lens.ui.screen.dashboard
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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 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]
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Главный Composable для экрана "Дэшборд".
|
||||
* @param viewModel ViewModel для этого экрана, предоставляемая Hilt.
|
||||
*/
|
||||
// [ANCHOR] Главная точка входа для экрана Dashboard
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
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 drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (val state = uiState) {
|
||||
is DashboardUiState.Loading -> {
|
||||
// [UI-ACTION] Показываем индикатор загрузки
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
// [ANCHOR] Определяем навигационное меню
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
DrawerContent(
|
||||
onNavigateToLocations = onNavigateToLocations,
|
||||
onNavigateToSearch = onNavigateToSearch,
|
||||
onNavigateToCreateItem = onNavigateToCreateItem,
|
||||
onLogout = onLogout,
|
||||
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] Показываем сообщение об ошибке
|
||||
val errorMessage = "Error: ${state.message}"
|
||||
Text(
|
||||
text = errorMessage,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
Timber.w("[UI-STATE] Displaying Error: $errorMessage")
|
||||
|
||||
// [ANCHOR] Секция "Недавно добавлено"
|
||||
item {
|
||||
// TODO: Add recently added items to UiState and display them here
|
||||
// RecentlyAddedSection(items = uiState.recentlyAddedItems)
|
||||
}
|
||||
is DashboardUiState.Success -> {
|
||||
// [UI-ACTION] Отображаем основной контент
|
||||
Timber.d("[UI-STATE] Displaying Success")
|
||||
DashboardContent(state)
|
||||
|
||||
// [ANCHOR] Секция "Места хранения"
|
||||
item {
|
||||
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
|
||||
fun DashboardContent(state: DashboardUiState.Success) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// [UI-COMPONENT] Статистика
|
||||
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}")
|
||||
private fun ItemCard(item: ItemSummary) {
|
||||
Card(modifier = Modifier.width(150.dp)) {
|
||||
Column(modifier = Modifier.padding(8.dp)) {
|
||||
// TODO: Add image here from item.image
|
||||
Spacer(modifier = Modifier.height(80.dp).fillMaxWidth().background(MaterialTheme.colorScheme.secondaryContainer))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = item.name, style = MaterialTheme.typography.titleSmall, maxLines = 1)
|
||||
Text(text = item.location?.name ?: stringResource(id = R.string.no_location), style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
// [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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
32
app/src/main/res/values-en/strings.xml
Normal file
32
app/src/main/res/values-en/strings.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
</resources>
|
||||
@@ -1,3 +1,32 @@
|
||||
<resources>
|
||||
<string name="app_name">Homebox Lens</string>
|
||||
</resources>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
</resources>
|
||||
@@ -10,6 +10,41 @@
|
||||
<summary>Библиотека логирования</summary>
|
||||
<description>В проекте используется Timber (timber.log.Timber) для всех целей логирования. Он предоставляет простой и расширяемый API для логирования.</description>
|
||||
</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>
|
||||
|
||||
<FEATURES>
|
||||
@@ -112,6 +147,92 @@
|
||||
</FUNCTIONALITY>
|
||||
</FEATURE>
|
||||
</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_SPECIFICATIONS>
|
||||
|
||||
<IMPLEMENTATION_MAP>
|
||||
<!-- Use Cases -->
|
||||
|
||||
Reference in New Issue
Block a user