REFACTOR END

This commit is contained in:
2025-09-28 10:10:01 +03:00
parent 394e0040de
commit 9b914b2904
117 changed files with 3070 additions and 5447 deletions

View File

@@ -4,29 +4,26 @@
package com.homebox.lens.feature.dashboard
// [IMPORTS]
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.common.NavigationActions
// [END_IMPORTS]
// [ENTITY: Function('addDashboardScreen')]
// [RELATION: Function('addDashboardScreen')] -> [DEPENDS_ON] -> [Function('DashboardScreen')]
/**
* @summary Extension function for NavGraphBuilder to add the Dashboard screen to the navigation graph.
* @description Registers the Dashboard route and composes the DashboardScreen with appropriate navigation actions and common UI components.
* @param route The route string for the Dashboard screen.
* @param currentRoute The current navigation route, used for highlighting.
* @param navigateToScan Lambda for navigating to the scan screen.
* @param navigateToSearch Lambda for navigating to the search screen.
* @param navigateToInventoryListWithLocation Lambda for navigating to inventory filtered by location.
* @param navigateToInventoryListWithLabel Lambda for navigating to inventory filtered by label.
* @param MainScaffoldContent Composable lambda for the main scaffold structure.
* @param HomeboxLensTheme Composable lambda for applying the application theme.
* @sideeffect Adds a composable route for the Dashboard screen.
*/
// [ANCHOR:addDashboardScreen:Function]
// [RELATION:DEPENDS_ON:DashboardScreen]
// [CONTRACT:addDashboardScreen]
// [PURPOSE] Extension function for NavGraphBuilder to add the Dashboard screen to the navigation graph. Registers the Dashboard route and composes the DashboardScreen with appropriate navigation actions and common UI components.
// [PARAM:route:String] The route string for the Dashboard screen.
// [PARAM:currentRoute:String] The current navigation route, used for highlighting.
// [PARAM:navigateToScan:Unit] Lambda for navigating to the scan screen.
// [PARAM:navigateToSearch:Unit] Lambda for navigating to the search screen.
// [PARAM:navigateToInventoryListWithLocation:Unit] Lambda for navigating to inventory filtered by location.
// [PARAM:navigateToInventoryListWithLabel:Unit] Lambda for navigating to inventory filtered by label.
// [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
// [PARAM:navController:NavHostController] Контроллер навигации.
// [SIDE_EFFECT] Adds a composable route for the Dashboard screen.
// [END_CONTRACT:addDashboardScreen]
fun NavGraphBuilder.addDashboardScreen(
route: String,
currentRoute: String?,
@@ -36,14 +33,6 @@ fun NavGraphBuilder.addDashboardScreen(
navigateToInventoryListWithLabel: (String) -> Unit,
navigationActions: NavigationActions,
navController: NavHostController,
MainScaffoldContent: @Composable (
topBarTitle: String,
currentRoute: String?,
navigationActions: NavigationActions,
topBarActions: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit
) -> Unit,
HomeboxLensTheme: @Composable (content: @Composable () -> Unit) -> Unit
) {
composable(route = route) {
DashboardScreen(
@@ -52,12 +41,10 @@ fun NavGraphBuilder.addDashboardScreen(
navigateToSearch = navigateToSearch,
navigateToInventoryListWithLocation = navigateToInventoryListWithLocation,
navigateToInventoryListWithLabel = navigateToInventoryListWithLabel,
MainScaffoldContent = MainScaffoldContent,
HomeboxLensTheme = HomeboxLensTheme,
navigationActions = navigationActions,
navController = navController
navController = navController,
)
}
}
// [END_ENTITY: Function('addDashboardScreen')]
// [END_FILE_DashboardNavigation.kt]
// [END_ANCHOR:addDashboardScreen]
// [END_FILE_DashboardNavigation.kt]

View File

@@ -4,10 +4,18 @@
package com.homebox.lens.feature.dashboard
// [IMPORTS]
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.ExperimentalLayoutApi
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
@@ -16,7 +24,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -29,29 +43,30 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import com.homebox.lens.domain.model.*
import com.homebox.lens.feature.dashboard.R
import com.homebox.lens.navigation.NavigationActions
import com.homebox.lens.ui.theme.HomeboxLensTheme
import com.homebox.lens.ui.common.mainScaffold
import com.homebox.lens.ui.common.NavigationActions
import com.homebox.lens.feature.dashboard.ui.theme.HomeboxLensTheme
import timber.log.Timber
// [END_IMPORTS]
// [ENTITY: Function('DashboardScreen')]
// [RELATION: Function('DashboardScreen')] -> [DEPENDS_ON] -> [ViewModel('DashboardViewModel')]
// [RELATION: Function('DashboardScreen')] -> [CALLS] -> [Function('MainScaffold')]
/**
* @summary Главная Composable-функция для экрана "Панель управления".
* @param viewModel ViewModel для этого экрана, предоставляется через Hilt.
* @param currentRoute Текущий маршрут для подсветки активного элемента в Drawer.
* @param navigateToScan Лямбда для навигации на экран сканирования.
* @param navigateToSearch Лямбда для навигации на экран поиска.
* @param navigateToInventoryListWithLocation Лямбда для навигации на список инвентаря с фильтром по локации.
* @param navigateToInventoryListWithLabel Лямбда для навигации на список инвентаря с фильтром по метке.
* @param MainScaffoldContent Composable-функция для отображения основного Scaffold.
* @param HomeboxLensTheme Composable-функция для применения темы.
* @sideeffect Вызывает навигационные лямбды при взаимодействии с UI.
*/
// [ANCHOR:DashboardScreen:Function]
// [RELATION:CALLS:DashboardViewModel]
// [RELATION:CALLS:mainScaffold]
// [CONTRACT:DashboardScreen]
// [PURPOSE] Главная Composable-функция для экрана "Панель управления".
// [PARAM:viewModel:DashboardViewModel] ViewModel для этого экрана, предоставляется через Hilt.
// [PARAM:currentRoute:String] Текущий маршрут для подсветки активного элемента в Drawer.
// [PARAM:navigateToScan:Unit] Лямбда для навигации на экран сканирования.
// [PARAM:navigateToSearch:Unit] Лямбда для навигации на экран поиска.
// [PARAM:navigateToInventoryListWithLocation:Unit] Лямбда для навигации на список инвентаря с фильтром по локации.
// [PARAM:navigateToInventoryListWithLabel:Unit] Лямбда для навигации на список инвентаря с фильтром по метке.
// [PARAM:navigationActions:NavigationActions] Объект с навигационными действиями.
// [PARAM:navController:NavHostController] Контроллер навигации.
// [SIDE_EFFECT] Вызывает навигационные лямбды при взаимодействии с UI.
// [END_CONTRACT:DashboardScreen]
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DashboardScreen(
viewModel: DashboardViewModel = hiltViewModel(),
@@ -62,17 +77,6 @@ fun DashboardScreen(
navigateToInventoryListWithLabel: (String) -> Unit,
navigationActions: NavigationActions,
navController: NavHostController,
MainScaffoldContent: @Composable (
topBarTitle: String,
currentRoute: String?,
navigationActions: NavigationActions,
onNavigateUp: (() -> Unit)?,
topBarActions: @Composable () -> Unit,
snackbarHost: @Composable () -> Unit,
floatingActionButton: @Composable () -> Unit,
content: @Composable (PaddingValues) -> Unit
) -> Unit,
HomeboxLensTheme: @Composable (content: @Composable () -> Unit) -> Unit
) {
val uiState by viewModel.uiState.collectAsState()
@@ -81,60 +85,60 @@ fun DashboardScreen(
}
HomeboxLensTheme {
MainScaffoldContent(
topBarTitle = stringResource(id = R.string.dashboard_title),
mainScaffold(
topBarTitle = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_title),
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateUp = null, // Dashboard doesn't have an "Up" button
topBarActions = {
IconButton(onClick = navigateToScan) {
Icon(
Icons.Default.QrCodeScanner,
contentDescription = stringResource(id = R.string.cd_scan_qr_code)
Icons.Filled.QrCodeScanner,
contentDescription = stringResource(id = com.homebox.lens.feature.dashboard.R.string.cd_scan_qr_code),
)
}
IconButton(onClick = navigateToSearch) {
Icon(
Icons.Default.Search,
contentDescription = stringResource(id = R.string.cd_search)
contentDescription = stringResource(id = com.homebox.lens.feature.dashboard.R.string.cd_search),
)
}
},
snackbarHost = {}, // Not used in Dashboard
floatingActionButton = {} // Not used in Dashboard
snackbarHost = { },
floatingActionButton = { },
) { paddingValues ->
DashboardContent(
modifier = Modifier.padding(paddingValues),
uiState = uiState,
onLocationClick = { location ->
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location] Location chip clicked: ${location.id}. Navigating...")
Timber.i("[INFO][ACTION][navigate_to_inventory_with_location]", "Location chip clicked", "locationId", location.id)
navigateToInventoryListWithLocation(location.id)
},
onLabelClick = { label ->
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label] Label chip clicked: ${label.id}. Navigating...")
Timber.i("[INFO][ACTION][navigate_to_inventory_with_label]", "Label chip clicked", "labelId", label.id)
navigateToInventoryListWithLabel(label.id)
}
},
)
}
}
}
// [END_ENTITY: Function('DashboardScreen')]
// [END_ANCHOR:DashboardScreen]
// [ENTITY: Function('DashboardContent')]
// [RELATION: Function('DashboardContent')] -> [CONSUMES_STATE] -> [SealedInterface('DashboardUiState')]
/**
* @summary Отображает основной контент экрана в зависимости от uiState.
* @param modifier Модификатор для стилизации.
* @param uiState Текущее состояние UI экрана.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
* @param onLabelClick Лямбда-обработчик нажатия на метку.
*/
// [ANCHOR:DashboardContent:Function]
// [RELATION:CONSUMES_STATE:DashboardUiState]
// [CONTRACT:DashboardContent]
// [PURPOSE] Отображает основной контент экрана в зависимости от uiState.
// [PARAM:modifier:Modifier] Модификатор для стилизации.
// [PARAM:uiState:DashboardUiState] Текущее состояние UI экрана.
// [PARAM:onLocationClick:Unit] Лямбда-обработчик нажатия на местоположение.
// [PARAM:onLabelClick:Unit] Лямбда-обработчик нажатия на метку.
// [END_CONTRACT:DashboardContent]
@Composable
private fun DashboardContent(
modifier: Modifier = Modifier,
uiState: DashboardUiState,
onLocationClick: (LocationOutCount) -> Unit,
onLabelClick: (LabelOut) -> Unit
onLabelClick: (LabelOut) -> Unit,
) {
when (uiState) {
is DashboardUiState.Loading -> {
@@ -147,16 +151,17 @@ private fun DashboardContent(
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
)
}
}
is DashboardUiState.Success -> {
LazyColumn(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
modifier =
modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
item { Spacer(modifier = Modifier.height(8.dp)) }
item { StatisticsSection(statistics = uiState.statistics) }
@@ -168,77 +173,102 @@ private fun DashboardContent(
}
}
}
// [END_ENTITY: Function('DashboardContent')]
// [END_ANCHOR:DashboardContent]
// [ENTITY: Function('StatisticsSection')]
// [RELATION: Function('StatisticsSection')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
/**
* @summary Секция для отображения общей статистики.
* @param statistics Объект со статистическими данными.
*/
// [ANCHOR:StatisticsSection:Function]
// [RELATION:DEPENDS_ON:GroupStatistics]
// [CONTRACT:StatisticsSection]
// [PURPOSE] Секция для отображения общей статистики.
// [PARAM:statistics:GroupStatistics] Объект со статистическими данными.
// [END_CONTRACT:StatisticsSection]
@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
text = stringResource(id = com.homebox.lens.feature.dashboard.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),
modifier =
Modifier
.height(120.dp)
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = 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()) }
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_items),
value = statistics.items.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_value),
value = statistics.totalValue.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_labels),
value = statistics.labels.toString(),
)
}
item {
StatisticCard(
title = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_stat_total_locations),
value = statistics.locations.toString(),
)
}
}
}
}
}
// [END_ENTITY: Function('StatisticsSection')]
// [END_ANCHOR:StatisticsSection]
// [ENTITY: Function('StatisticCard')]
/**
* @summary Карточка для отображения одного статистического показателя.
* @param title Название показателя.
* @param value Значение показателя.
*/
// [ANCHOR:StatisticCard:Function]
// [CONTRACT:StatisticCard]
// [PURPOSE] Карточка для отображения одного статистического показателя.
// [PARAM:title:String] Название показателя.
// [PARAM:value:String] Значение показателя.
// [END_CONTRACT:StatisticCard]
@Composable
private fun StatisticCard(title: String, value: String) {
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)
}
}
// [END_ENTITY: Function('StatisticCard')]
// [END_ANCHOR:StatisticCard]
// [ENTITY: Function('RecentlyAddedSection')]
// [RELATION: Function('RecentlyAddedSection')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/**
* @summary Секция для отображения недавно добавленных элементов.
* @param items Список элементов для отображения.
*/
// [ANCHOR:RecentlyAddedSection:Function]
// [RELATION:DEPENDS_ON:ItemSummary]
// [CONTRACT:RecentlyAddedSection]
// [PURPOSE] Секция для отображения недавно добавленных элементов.
// [PARAM:items:List<ItemSummary>] Список элементов для отображения.
// [END_CONTRACT:RecentlyAddedSection]
@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
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_recently_added),
style = MaterialTheme.typography.titleMedium,
)
if (items.isEmpty()) {
Text(
text = stringResource(id = R.string.items_not_found),
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.items_not_found),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
textAlign = TextAlign.Center,
)
} else {
LazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -249,126 +279,189 @@ private fun RecentlyAddedSection(items: List<ItemSummary>) {
}
}
}
// [END_ENTITY: Function('RecentlyAddedSection')]
// [END_ANCHOR:RecentlyAddedSection]
// [ENTITY: Function('ItemCard')]
// [RELATION: Function('ItemCard')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/**
* @summary Карточка для отображения краткой информации об элементе.
* @param item Элемент для отображения.
*/
// [ANCHOR:ItemCard:Function]
// [RELATION:DEPENDS_ON:ItemSummary]
// [CONTRACT:ItemCard]
// [PURPOSE] Карточка для отображения краткой информации об элементе.
// [PARAM:item:ItemSummary] Элемент для отображения.
// [END_CONTRACT:ItemCard]
@Composable
private fun ItemCard(item: ItemSummary) {
Card(modifier = Modifier.width(150.dp)) {
Column(modifier = Modifier.padding(8.dp)) {
// [AI_NOTE]: Add image here from item.image
Spacer(modifier = Modifier
.height(80.dp)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondaryContainer))
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)
Text(
text = item.location?.name ?: stringResource(id = com.homebox.lens.feature.dashboard.R.string.items_not_found),
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
)
}
}
}
// [END_ENTITY: Function('ItemCard')]
// [END_ANCHOR:ItemCard]
// [ENTITY: Function('LocationsSection')]
// [RELATION: Function('LocationsSection')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
/**
* @summary Секция для отображения местоположений в виде чипсов.
* @param locations Список местоположений.
* @param onLocationClick Лямбда-обработчик нажатия на местоположение.
*/
// [ANCHOR:LocationsSection:Function]
// [RELATION:DEPENDS_ON:LocationOutCount]
// [CONTRACT:LocationsSection]
// [PURPOSE] Секция для отображения местоположений в виде чипсов.
// [PARAM:locations:List<LocationOutCount>] Список местоположений.
// [PARAM:onLocationClick:Unit] Лямбда-обработчик нажатия на местоположение.
// [END_CONTRACT:LocationsSection]
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LocationsSection(locations: List<LocationOutCount>, onLocationClick: (LocationOutCount) -> Unit) {
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
text = stringResource(id = com.homebox.lens.feature.dashboard.R.string.dashboard_section_locations),
style = MaterialTheme.typography.titleMedium,
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
locations.forEach { location ->
SuggestionChip(
onClick = { onLocationClick(location) },
label = { Text(stringResource(id = R.string.location_chip_label, location.name, location.itemCount)) }
onClick = {
Timber.i("[INFO][ACTION][location_chip_click]", "Location chip clicked", "locationId", location.id)
onLocationClick(location)
},
label = { Text(stringResource(id = com.homebox.lens.feature.dashboard.R.string.location_chip_label, location.name, location.itemCount)) },
)
}
}
}
}
// [END_ENTITY: Function('LocationsSection')]
// [END_ANCHOR:LocationsSection]
// [ENTITY: Function('LabelsSection')]
// [RELATION: Function('LabelsSection')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
/**
* @summary Секция для отображения меток в виде чипсов.
* @param labels Список меток.
* @param onLabelClick Лямбда-обработчик нажатия на метку.
*/
// [ANCHOR:LabelsSection:Function]
// [RELATION:DEPENDS_ON:LabelOut]
// [CONTRACT:LabelsSection]
// [PURPOSE] Секция для отображения меток в виде чипсов.
// [PARAM:labels:List<LabelOut>] Список меток.
// [PARAM:onLabelClick:Unit] Лямбда-обработчик нажатия на метку.
// [END_CONTRACT:LabelsSection]
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LabelsSection(labels: List<LabelOut>, onLabelClick: (LabelOut) -> Unit) {
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
text = stringResource(id = com.homebox.lens.feature.dashboard.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) }
onClick = {
Timber.i("[INFO][ACTION][label_chip_click]", "Label chip clicked", "labelId", label.id)
onLabelClick(label)
},
label = { Text(label.name) },
)
}
}
}
}
// [END_ENTITY: Function('LabelsSection')]
// [END_ANCHOR:LabelsSection]
// [ENTITY: Function('DashboardContentSuccessPreview')]
// [ANCHOR:DashboardContentSuccessPreview:Function]
@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 = "")
),
recentlyAddedItems = emptyList()
)
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 = ""),
),
recentlyAddedItems = emptyList(),
)
HomeboxLensTheme {
DashboardContent(
uiState = previewState,
onLocationClick = {},
onLabelClick = {}
onLabelClick = {},
)
}
}
// [END_ENTITY: Function('DashboardContentSuccessPreview')]
// [END_ANCHOR:DashboardContentSuccessPreview]
// [ENTITY: Function('DashboardContentLoadingPreview')]
// [ANCHOR:DashboardContentLoadingPreview:Function]
@Preview(showBackground = true, name = "Dashboard Loading State")
@Composable
fun DashboardContentLoadingPreview() {
@@ -376,23 +469,23 @@ fun DashboardContentLoadingPreview() {
DashboardContent(
uiState = DashboardUiState.Loading,
onLocationClick = {},
onLabelClick = {}
onLabelClick = {},
)
}
}
// [END_ENTITY: Function('DashboardContentLoadingPreview')]
// [END_ANCHOR:DashboardContentLoadingPreview]
// [ENTITY: Function('DashboardContentErrorPreview')]
// [ANCHOR:DashboardContentErrorPreview:Function]
@Preview(showBackground = true, name = "Dashboard Error State")
@Composable
fun DashboardContentErrorPreview() {
HomeboxLensTheme {
DashboardContent(
uiState = DashboardUiState.Error(stringResource(id = R.string.error_loading_failed)),
uiState = DashboardUiState.Error(stringResource(id = com.homebox.lens.feature.dashboard.R.string.error_loading_failed)),
onLocationClick = {},
onLabelClick = {}
onLabelClick = {},
)
}
}
// [END_ENTITY: Function('DashboardContentErrorPreview')]
// [END_FILE_DashboardScreen.kt]
// [END_ANCHOR:DashboardContentErrorPreview]
// [END_FILE_DashboardScreen.kt]

View File

@@ -10,46 +10,46 @@ import com.homebox.lens.domain.model.LabelOut
import com.homebox.lens.domain.model.LocationOutCount
// [END_IMPORTS]
// [ENTITY: SealedInterface('DashboardUiState')]
/**
* @summary Определяет все возможные состояния для экрана "Дэшборд".
* @invariant В любой момент времени экран может находиться только в одном из этих состояний.
*/
// [ANCHOR:DashboardUiState:SealedInterface]
// [CONTRACT:DashboardUiState]
// [PURPOSE] Определяет все возможные состояния для экрана "Дэшборд".
// [INVARIANT] В любой момент времени экран может находиться только в одном из этих состояний.
// [END_CONTRACT:DashboardUiState]
sealed interface DashboardUiState {
// [ENTITY: DataClass('Success')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('GroupStatistics')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LocationOutCount')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('LabelOut')]
// [RELATION: DataClass('Success')] -> [DEPENDS_ON] -> [DataClass('ItemSummary')]
/**
* @summary Состояние успешной загрузки данных.
* @param statistics Статистика по инвентарю.
* @param locations Список локаций со счетчиками.
* @param labels Список всех меток.
* @param recentlyAddedItems Список недавно добавленных товаров.
*/
// [ANCHOR:Success:DataClass]
// [RELATION:DEPENDS_ON:GroupStatistics]
// [RELATION:DEPENDS_ON:LocationOutCount]
// [RELATION:DEPENDS_ON:LabelOut]
// [RELATION:DEPENDS_ON:ItemSummary]
// [CONTRACT:Success]
// [PURPOSE] Состояние успешной загрузки данных.
// [PARAM:statistics:GroupStatistics] Статистика по инвентарю.
// [PARAM:locations:List<LocationOutCount>] Список локаций со счетчиками.
// [PARAM:labels:List<LabelOut>] Список всех меток.
// [PARAM:recentlyAddedItems:List<ItemSummary>] Список недавно добавленных товаров.
// [END_CONTRACT:Success]
data class Success(
val statistics: GroupStatistics,
val locations: List<LocationOutCount>,
val labels: List<LabelOut>,
val recentlyAddedItems: List<ItemSummary>
val recentlyAddedItems: List<ItemSummary>,
) : DashboardUiState
// [END_ENTITY: DataClass('Success')]
// [END_ANCHOR:Success]
// [ENTITY: DataClass('Error')]
/**
* @summary Состояние ошибки во время загрузки данных.
* @param message Человекочитаемое сообщение об ошибке.
*/
// [ANCHOR:Error:DataClass]
// [CONTRACT:Error]
// [PURPOSE] Состояние ошибки во время загрузки данных.
// [PARAM:message:String] Человекочитаемое сообщение об ошибке.
// [END_CONTRACT:Error]
data class Error(val message: String) : DashboardUiState
// [END_ENTITY: DataClass('Error')]
// [END_ANCHOR:Error]
// [ENTITY: Object('Loading')]
/**
* @summary Состояние, когда данные для экрана загружаются.
*/
// [ANCHOR:Loading:Object]
// [CONTRACT:Loading]
// [PURPOSE] Состояние, когда данные для экрана загружаются.
// [END_CONTRACT:Loading]
data object Loading : DashboardUiState
// [END_ENTITY: Object('Loading')]
// [END_ANCHOR:Loading]
}
// [END_ENTITY: SealedInterface('DashboardUiState')]
// [END_FILE_DashboardUiState.kt]
// [END_ANCHOR:DashboardUiState]
// [END_FILE_DashboardUiState.kt]

View File

@@ -17,71 +17,67 @@ import timber.log.Timber
import javax.inject.Inject
// [END_IMPORTS]
// [ENTITY: ViewModel('DashboardViewModel')]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetStatisticsUseCase')]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLocationsUseCase')]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetAllLabelsUseCase')]
// [RELATION: ViewModel('DashboardViewModel')] -> [DEPENDS_ON] -> [UseCase('GetRecentlyAddedItemsUseCase')]
// [RELATION: ViewModel('DashboardViewModel')] -> [EMITS_STATE] -> [SealedInterface('DashboardUiState')]
/**
* @summary ViewModel для главного экрана (Dashboard).
* @description Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний
* (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
* @invariant `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
*/
// [ANCHOR:DashboardViewModel:ViewModel]
// [RELATION:DEPENDS_ON:GetStatisticsUseCase]
// [RELATION:DEPENDS_ON:GetAllLocationsUseCase]
// [RELATION:DEPENDS_ON:GetAllLabelsUseCase]
// [RELATION:DEPENDS_ON:GetRecentlyAddedItemsUseCase]
// [RELATION:EMITS_STATE:DashboardUiState]
// [CONTRACT:DashboardViewModel]
// [PURPOSE] ViewModel для главного экрана (Dashboard). Оркестрирует загрузку данных для Dashboard, используя строгую модель состояний (`DashboardUiState`), и обрабатывает параллельные запросы без состояний гонки.
// [INVARIANT] `uiState` всегда является одним из состояний, определенных в `DashboardUiState`.
// [END_CONTRACT:DashboardViewModel]
@HiltViewModel
class DashboardViewModel @Inject constructor(
private val getStatisticsUseCase: GetStatisticsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase,
private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase
) : ViewModel() {
class DashboardViewModel
@Inject
constructor(
private val getStatisticsUseCase: GetStatisticsUseCase,
private val getAllLocationsUseCase: GetAllLocationsUseCase,
private val getAllLabelsUseCase: GetAllLabelsUseCase,
private val getRecentlyAddedItemsUseCase: GetRecentlyAddedItemsUseCase,
) : ViewModel() {
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
val uiState = _uiState.asStateFlow()
private val _uiState = MutableStateFlow<DashboardUiState>(DashboardUiState.Loading)
val uiState = _uiState.asStateFlow()
// [ANCHOR:loadDashboardData:Function]
// [CONTRACT:loadDashboardData]
// [PURPOSE] Загружает все необходимые данные для экрана Dashboard. Выполняет UseCase'ы параллельно и обновляет UI, переключая его между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
// [SIDE_EFFECT] Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
// [END_CONTRACT:loadDashboardData]
fun loadDashboardData() {
if (uiState.value is DashboardUiState.Success || uiState.value is DashboardUiState.Loading) {
Timber.i("[INFO][SKIP][already_loaded] Dashboard data load skipped - already in progress or loaded.")
return
}
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.")
val statsFlow = flow { emit(getStatisticsUseCase()) }
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
val labelsFlow = flow { emit(getAllLabelsUseCase()) }
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
// [ENTITY: Function('loadDashboardData')]
/**
* @summary Загружает все необходимые данные для экрана Dashboard.
* @description Выполняет UseCase'ы параллельно и обновляет UI, переключая его
* между состояниями `Loading`, `Success` и `Error` из `DashboardUiState`.
* @sideeffect Асинхронно обновляет `_uiState` одним из состояний `DashboardUiState`.
*/
fun loadDashboardData() {
if (uiState.value is DashboardUiState.Success || uiState.value is DashboardUiState.Loading) {
Timber.i("[INFO][SKIP][already_loaded] Dashboard data load skipped - already in progress or loaded.")
return
}
viewModelScope.launch {
_uiState.value = DashboardUiState.Loading
Timber.i("[INFO][ENTRYPOINT][loading_dashboard_data] Starting dashboard data collection.")
val statsFlow = flow { emit(getStatisticsUseCase()) }
val locationsFlow = flow { emit(getAllLocationsUseCase()) }
val labelsFlow = flow { emit(getAllLabelsUseCase()) }
val recentItemsFlow = getRecentlyAddedItemsUseCase(limit = 10)
combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
DashboardUiState.Success(
statistics = stats,
locations = locations,
labels = labels,
recentlyAddedItems = recentItems
)
}.catch { exception ->
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
_uiState.value = DashboardUiState.Error(
message = exception.message ?: "Could not load dashboard data."
)
}.collect { successState ->
Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
_uiState.value = successState
combine(statsFlow, locationsFlow, labelsFlow, recentItemsFlow) { stats, locations, labels, recentItems ->
DashboardUiState.Success(
statistics = stats,
locations = locations,
labels = labels,
recentlyAddedItems = recentItems,
)
}.catch { exception ->
Timber.e(exception, "[ERROR][EXCEPTION][loading_failed] Failed to load dashboard data. State -> Error.")
_uiState.value =
DashboardUiState.Error(
message = exception.message ?: "Could not load dashboard data.",
)
}.collect { successState ->
Timber.i("[INFO][SUCCESS][dashboard_data_loaded] Dashboard data loaded successfully. State -> Success.")
_uiState.value = successState
}
}
}
// [END_ANCHOR:loadDashboardData]
}
// [END_ENTITY: Function('loadDashboardData')]
}
// [END_ENTITY: ViewModel('DashboardViewModel')]
// [END_FILE_DashboardViewModel.kt]
// [END_ANCHOR:DashboardViewModel]
// [END_FILE_DashboardViewModel.kt]

View File

@@ -0,0 +1,161 @@
// [PACKAGE] com.homebox.lens.navigation
// [FILE] NavGraph.kt
// [SEMANTICS] navigation, compose, nav_host
package com.homebox.lens.feature.dashboard.navigation
// [IMPORTS]
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.homebox.lens.feature.dashboard.addDashboardScreen
import com.homebox.lens.ui.common.NavigationActions
import com.homebox.lens.feature.inventorylist.InventoryListScreen
import com.homebox.lens.feature.itemdetails.ItemDetailsScreen
import com.homebox.lens.feature.itemedit.ItemEditScreen
import com.homebox.lens.feature.labeledit.LabelEditScreen
import com.homebox.lens.feature.labelslist.LabelsListScreen
import com.homebox.lens.feature.locationedit.LocationEditScreen
import com.homebox.lens.feature.locationslist.LocationsListScreen
import com.homebox.lens.feature.scan.ScanScreen
import com.homebox.lens.feature.search.SearchScreen
import com.homebox.lens.feature.settings.SettingsScreen
import com.homebox.lens.feature.setup.SetupScreen
// [END_IMPORTS]
// [ANCHOR:NavGraph:Function]
// [RELATION:DEPENDS_ON:NavHostController]
// [RELATION:CREATES_INSTANCE_OF:NavigationActions]
// [CONTRACT:NavGraph]
// [PURPOSE] Определяет граф навигации для всего приложения с использованием Jetpack Compose Navigation.
// [PARAM:navController:NavHostController] Контроллер навигации.
// [SEE] Screen
// [SIDE_EFFECT] Регистрирует все экраны и управляет состоянием навигации.
// [INVARIANT] Стартовый экран - `Screen.Setup`.
// [END_CONTRACT:NavGraph]
@Composable
fun navGraph(navController: NavHostController = rememberNavController()) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val navigationActions =
remember(navController) {
NavigationActions(navController)
}
NavHost(
navController = navController,
startDestination = Screen.Setup.route,
) {
composable(route = Screen.Setup.route) {
SetupScreen(onSetupComplete = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Setup.route) { inclusive = true }
}
})
}
addDashboardScreen(
route = Screen.Dashboard.route,
currentRoute = currentRoute,
navigateToScan = navigationActions::navigateToScan,
navigateToSearch = navigationActions::navigateToSearch,
navigateToInventoryListWithLocation = navigationActions::navigateToInventoryListWithLocation,
navigateToInventoryListWithLabel = navigationActions::navigateToInventoryListWithLabel,
navigationActions = navigationActions,
navController = navController,
)
composable(route = Screen.InventoryList.route) {
InventoryListScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
)
}
composable(
route = Screen.ItemDetails.route,
arguments = listOf(navArgument("itemId") { nullable = true }),
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")
ItemDetailsScreen(
itemId = itemId,
currentRoute = currentRoute,
navigationActions = navigationActions,
)
}
composable(
route = Screen.ItemEdit.route,
arguments = listOf(navArgument("itemId") { nullable = true }),
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getString("itemId")
ItemEditScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
itemId = itemId,
onSaveSuccess = { navController.popBackStack() },
)
}
composable(Screen.LabelsList.route) {
LabelsListScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
)
}
composable(route = Screen.LocationsList.route) {
LocationsListScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
onLocationClick = { locationId: String ->
// [AI_NOTE]: Navigate to a pre-filtered inventory list screen
navigationActions.navigateToInventoryListWithLocation(locationId)
},
onAddNewLocationClick = {
navController.navigate(Screen.LocationEdit.createRoute("new"))
},
)
}
composable(route = Screen.LocationEdit.route) { backStackEntry ->
val locationId = backStackEntry.arguments?.getString("locationId")
LocationEditScreen(
locationId = locationId,
)
}
composable(
route = Screen.LabelEdit.route,
arguments = listOf(navArgument("labelId") { nullable = true }),
) { backStackEntry ->
val labelId = backStackEntry.arguments?.getString("labelId")
LabelEditScreen(
labelId = labelId,
onBack = { navController.popBackStack() },
onLabelSaved = { navController.popBackStack() },
)
}
composable(route = Screen.Search.route) {
SearchScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
)
}
composable(Screen.Settings.route) {
SettingsScreen(
currentRoute = currentRoute,
navigationActions = navigationActions,
onNavigateUp = { navController.navigateUp() },
)
}
composable(Screen.Scan.route) { backStackEntry ->
ScanScreen(onBarcodeResult = { barcode: String ->
val previousBackStackEntry = navController.previousBackStackEntry
previousBackStackEntry?.savedStateHandle?.set("barcodeResult", barcode)
navController.popBackStack()
})
}
}
}
// [END_ANCHOR:NavGraph]
// [END_FILE_NavGraph.kt]

View File

@@ -0,0 +1,24 @@
package com.homebox.lens.feature.dashboard.navigation
sealed class Screen(val route: String) {
object Dashboard : Screen("dashboard")
object InventoryList : Screen("inventoryList")
object ItemDetails : Screen("itemDetails/{itemId}") {
fun createRoute(itemId: String) = "itemDetails/$itemId"
}
object ItemEdit : Screen("itemEdit?itemId={itemId}") {
fun createRoute(itemId: String?) = "itemEdit" + (itemId?.let { "?itemId=$it" } ?: "")
}
object LabelEdit : Screen("labelEdit?labelId={labelId}") {
fun createRoute(labelId: String?) = "labelEdit" + (labelId?.let { "?labelId=$it" } ?: "")
}
object LabelsList : Screen("labelsList")
object LocationEdit : Screen("locationEdit?locationId={locationId}") {
fun createRoute(locationId: String?) = "locationEdit" + (locationId?.let { "?locationId=$it" } ?: "")
}
object LocationsList : Screen("locationsList")
object Scan : Screen("scan")
object Search : Screen("search")
object Settings : Screen("settings")
object Setup : Screen("setup")
}

View File

@@ -0,0 +1,18 @@
// [FILE] Color.kt
// [SEMANTICS] ui, theme, color
package com.homebox.lens.feature.dashboard.ui.theme
// [IMPORTS]
import androidx.compose.ui.graphics.Color
// [END_IMPORTS]
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
// [END_FILE_Color.kt]

View File

@@ -0,0 +1,93 @@
// [FILE] Theme.kt
// [SEMANTICS] ui, theme
package com.homebox.lens.feature.dashboard.ui.theme
// [IMPORTS]
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
import timber.log.Timber
// [END_IMPORTS]
private val DarkColorScheme =
darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
)
private val LightColorScheme =
lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
)
// [ANCHOR:HomeboxLensTheme:Function]
// [RELATION:DEPENDS_ON:Typography]
// [RELATION:DEPENDS_ON:Color]
// [CONTRACT:HomeboxLensTheme]
// [PURPOSE] The main theme for the Homebox Lens application.
// [PARAM:darkTheme:Boolean] Whether the theme should be dark or light.
// [PARAM:dynamicColor:Boolean] Whether to use dynamic color (on Android 12+).
// [PARAM:content:(@Composable () -> Unit)] The content to be displayed within the theme.
// [SIDE_EFFECT] Sets the status bar color based on the theme.
// [END_CONTRACT:HomeboxLensTheme]
@Composable
fun HomeboxLensTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit,
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) {
Timber.i("[INFO][THEME][dynamic_dark_theme]", "Applying dynamic dark theme")
dynamicDarkColorScheme(context)
} else {
Timber.i("[INFO][THEME][dynamic_light_theme]", "Applying dynamic light theme")
dynamicLightColorScheme(context)
}
}
darkTheme -> {
Timber.i("[INFO][THEME][dark_theme]", "Applying static dark theme")
DarkColorScheme
}
else -> {
Timber.i("[INFO][THEME][light_theme]", "Applying static light theme")
LightColorScheme
}
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
Timber.i("[INFO][THEME][status_bar_color]", "Setting status bar color", "color", colorScheme.primary.toArgb())
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content,
)
}
// [END_ANCHOR:HomeboxLensTheme]
// [END_FILE_Theme.kt]

View File

@@ -0,0 +1,30 @@
// [FILE] Typography.kt
// [SEMANTICS] ui, theme, typography
package com.homebox.lens.feature.dashboard.ui.theme
// [IMPORTS]
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// [END_IMPORTS]
// [ANCHOR:Typography:DataStructure]
// [CONTRACT:Typography]
// [PURPOSE] Defines the typography for the application.
// [END_CONTRACT:Typography]
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp,
),
)
// [END_ANCHOR:Typography]
// [END_FILE_Typography.kt]

View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Dashboard Screen -->
<string name="dashboard_title">Главная</string>
@@ -16,10 +15,7 @@
<!-- Common -->
<string name="items_not_found">Элементы не найдены</string>
<string name="no_location">Нет локации</string>
<string name="error_loading_failed">Не удалось загрузить данные. Пожалуйста, попробуйте еще раз.</string>
<!-- Content Descriptions -->
<string name="cd_scan_qr_code">Сканировать QR/штрих-код</string>
<string name="cd_search">Поиск</string>
</resources>