feat: add tag system to shopping list items, implement dashboard quick access cards, and enhance item detail sheet with photo picker
- Add `tag` field to `ShoppingListItemEntity` for visual tags (urgent, offre, whenever) - Increment database version to 5 - Implement dashboard quick access cards showing shopping lists with remaining item counts - Add tag selection buttons in item detail sheet with toggle functionality - Display tag badges on item tiles with color-coded styling (danger for urgent, safe
This commit is contained in:
parent
b68212b99c
commit
8a19d46949
@ -21,7 +21,7 @@ import com.safebite.app.data.local.database.entity.UserProfileEntity
|
||||
ShoppingListEntity::class,
|
||||
ShoppingListItemEntity::class
|
||||
],
|
||||
version = 4,
|
||||
version = 5,
|
||||
exportSchema = false
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
||||
@ -99,5 +99,6 @@ data class ShoppingListItemEntity(
|
||||
val allergenWarning: String? = null, // Allergène détecté pour alerte
|
||||
val note: String? = null, // Quantité / description libre (ex: "2 kg")
|
||||
val customEmoji: String? = null, // Emoji personnalisé choisi par l'utilisateur
|
||||
val tag: String? = null, // Tag visuel : "urgent", "offre", "whenever"
|
||||
val addedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@ -4,33 +4,32 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.safebite.app.R
|
||||
import com.safebite.app.presentation.common.components.OutlinedActionButton
|
||||
import com.safebite.app.presentation.common.components.PrimaryButton
|
||||
import com.safebite.app.presentation.common.components.SafeBiteTopAppBar
|
||||
import com.safebite.app.presentation.common.components.StandardCard
|
||||
import com.safebite.app.presentation.common.components.CardVariant
|
||||
|
||||
/**
|
||||
* Dashboard contextuel (spec UX §5.3).
|
||||
@ -40,26 +39,17 @@ import com.safebite.app.presentation.common.components.SafeBiteTopAppBar
|
||||
* - store_mode : détecté via géolocalisation/heure
|
||||
* - home_mode : mode par défaut
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
onScan: () -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onOpenList: (Long, String) -> Unit,
|
||||
onOpenHistoryItem: (String) -> Unit
|
||||
onOpenHistoryItem: (String) -> Unit,
|
||||
viewModel: DashboardViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
actions = {
|
||||
IconButton(onClick = onOpenSettings) {
|
||||
Icon(Icons.Filled.Settings, stringResource(R.string.nav_settings))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@ -71,7 +61,7 @@ fun DashboardScreen(
|
||||
) {
|
||||
// Greeting
|
||||
Text(
|
||||
text = stringResource(R.string.dashboard_greeting, "Sophie"),
|
||||
text = stringResource(R.string.dashboard_greeting, state.greetingName),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
@ -82,11 +72,48 @@ fun DashboardScreen(
|
||||
onClick = onScan,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
OutlinedActionButton(
|
||||
text = stringResource(R.string.dashboard_lists_button),
|
||||
onClick = { onOpenList(0, "Ma liste") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// Shopping lists quick access
|
||||
if (state.lists.isNotEmpty()) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
state.lists.forEach { list ->
|
||||
StandardCard(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(72.dp),
|
||||
variant = CardVariant.Filled,
|
||||
onClick = { onOpenList(list.id, list.name) },
|
||||
contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = list.name,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.dashboard_remaining,
|
||||
list.remaining
|
||||
),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weekly stats placeholder
|
||||
Card(
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
package com.safebite.app.presentation.screen.dashboard
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.domain.model.UserProfile
|
||||
import com.safebite.app.domain.usecase.GetShoppingListsUseCase
|
||||
import com.safebite.app.domain.usecase.ManageProfileUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import javax.inject.Inject
|
||||
|
||||
data class DashboardUiState(
|
||||
val greetingName: String = "",
|
||||
val lists: List<ListSummary> = emptyList()
|
||||
)
|
||||
|
||||
data class ListSummary(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val remaining: Int
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
private val manageProfile: ManageProfileUseCase,
|
||||
private val getShoppingLists: GetShoppingListsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val state: StateFlow<DashboardUiState> = combine(
|
||||
manageProfile.observe(),
|
||||
manageProfile.observeActiveIds()
|
||||
) { profiles, activeIds ->
|
||||
profiles to activeIds
|
||||
}.flatMapLatest { (profiles, activeIds) ->
|
||||
val greetingName = resolveGreetingName(profiles, activeIds)
|
||||
observeListsWithStats(greetingName)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5000),
|
||||
initialValue = DashboardUiState()
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun observeListsWithStats(greetingName: String): Flow<DashboardUiState> {
|
||||
return getShoppingLists.observeActive().flatMapLatest { lists ->
|
||||
val sortedLists = lists.sortedBy { it.createdAt }.take(4)
|
||||
if (sortedLists.isEmpty()) {
|
||||
flowOf(DashboardUiState(greetingName = greetingName, lists = emptyList()))
|
||||
} else {
|
||||
val listFlows = sortedLists.map { list ->
|
||||
combine(
|
||||
getShoppingLists.observeItemCount(list.id),
|
||||
getShoppingLists.observeCheckedCount(list.id)
|
||||
) { total, checked ->
|
||||
ListSummary(list.id, list.name, total - checked)
|
||||
}
|
||||
}
|
||||
combine(listFlows) { array ->
|
||||
DashboardUiState(greetingName, array.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveGreetingName(profiles: List<UserProfile>, activeIds: Set<Long>): String {
|
||||
return when {
|
||||
activeIds.isNotEmpty() -> profiles.filter { it.id in activeIds }.firstOrNull()?.name
|
||||
else -> profiles.filter { it.isDefault }.firstOrNull()?.name ?: profiles.firstOrNull()?.name
|
||||
} ?: ""
|
||||
}
|
||||
}
|
||||
@ -76,6 +76,9 @@ import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -261,6 +264,8 @@ fun ListDetailScreen(
|
||||
onUpdateNote = { note -> viewModel.updateItemNote(selected.id, note) },
|
||||
onUpdateCategory = { cat -> viewModel.updateItemCategory(selected.id, cat) },
|
||||
onUpdateEmoji = { emoji -> viewModel.updateItemEmoji(selected.id, emoji) },
|
||||
onUpdateTag = { tag -> viewModel.updateItemTag(selected.id, tag) },
|
||||
onUpdateImage = { imageUrl -> viewModel.updateItemImageUrl(selected.id, imageUrl) },
|
||||
onMoveTo = { targetListId -> viewModel.moveItemToList(selected.id, targetListId) },
|
||||
onDelete = { viewModel.deleteItem(selected.id) },
|
||||
onOpenProduct = selected.barcode?.let { bc -> { onOpenProduct(bc) } }
|
||||
@ -289,6 +294,15 @@ private fun ListDetailContent(
|
||||
) {
|
||||
val dimens = LocalDimens.current
|
||||
|
||||
val filteredCatalogCounts = remember(ready.activeItems, ready.recentlyUsed) {
|
||||
catalog.categories.associateWith { category ->
|
||||
catalog.itemsForCategory(category).count { catItem ->
|
||||
ready.activeItems.none { it.productName.equals(catItem.name, ignoreCase = true) } &&
|
||||
ready.recentlyUsed.none { it.productName.equals(catItem.name, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
@ -315,7 +329,8 @@ private fun ListDetailContent(
|
||||
emoji = it.emoji,
|
||||
imageUrl = it.imageUrl,
|
||||
tone = TileTone.Active,
|
||||
badgeWarning = !it.allergenWarning.isNullOrBlank()
|
||||
badgeWarning = !it.allergenWarning.isNullOrBlank(),
|
||||
tag = it.tag
|
||||
)
|
||||
},
|
||||
onTap = onTapActive,
|
||||
@ -345,7 +360,8 @@ private fun ListDetailContent(
|
||||
emoji = it.emoji,
|
||||
imageUrl = it.imageUrl,
|
||||
tone = TileTone.Recent,
|
||||
badgeWarning = false
|
||||
badgeWarning = false,
|
||||
tag = it.tag
|
||||
)
|
||||
},
|
||||
onTap = onTapRecent,
|
||||
@ -373,14 +389,17 @@ private fun ListDetailContent(
|
||||
item(key = "header-$category") {
|
||||
CollapsibleHeader(
|
||||
title = category,
|
||||
count = catalog.itemsForCategory(category).size,
|
||||
count = filteredCatalogCounts[category] ?: 0,
|
||||
expanded = expanded,
|
||||
onToggle = { onToggleCategory(category) }
|
||||
)
|
||||
}
|
||||
if (expanded) {
|
||||
item(key = "grid-$category") {
|
||||
val items = catalog.itemsForCategory(category)
|
||||
val items = catalog.itemsForCategory(category).filter { catItem ->
|
||||
ready.activeItems.none { it.productName.equals(catItem.name, ignoreCase = true) } &&
|
||||
ready.recentlyUsed.none { it.productName.equals(catItem.name, ignoreCase = true) }
|
||||
}
|
||||
TileGrid(
|
||||
items = items.map {
|
||||
TileData(
|
||||
@ -418,7 +437,8 @@ private data class TileData(
|
||||
val emoji: String,
|
||||
val imageUrl: String?,
|
||||
val tone: TileTone,
|
||||
val badgeWarning: Boolean
|
||||
val badgeWarning: Boolean,
|
||||
val tag: String? = null
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@ -528,6 +548,23 @@ private fun Tile(
|
||||
.size(16.dp)
|
||||
)
|
||||
}
|
||||
if (!data.tag.isNullOrBlank()) {
|
||||
val tagColor = when (data.tag.lowercase()) {
|
||||
"urgent" -> statusColors.danger
|
||||
"offre" -> statusColors.safe
|
||||
else -> MaterialTheme.colorScheme.tertiary
|
||||
}
|
||||
Text(
|
||||
text = data.tag.uppercase(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.background(tagColor, RoundedCornerShape(4.dp))
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -776,17 +813,26 @@ private fun ItemDetailSheet(
|
||||
onUpdateNote: (String) -> Unit,
|
||||
onUpdateCategory: (String) -> Unit,
|
||||
onUpdateEmoji: (String?) -> Unit,
|
||||
onUpdateTag: (String?) -> Unit,
|
||||
onUpdateImage: (String?) -> Unit,
|
||||
onMoveTo: (Long) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
onOpenProduct: (() -> Unit)?
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
var note by remember(item.id) { mutableStateOf(item.note.orEmpty()) }
|
||||
var currentTag by remember(item.id) { mutableStateOf(item.tag) }
|
||||
var showCategoryPicker by remember { mutableStateOf(false) }
|
||||
var showIconPicker by remember { mutableStateOf(false) }
|
||||
var showMovePicker by remember { mutableStateOf(false) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val photoPickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let { onUpdateImage(it.toString()) }
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
// Persiste la note avant fermeture si elle a changé.
|
||||
@ -853,22 +899,34 @@ private fun ItemDetailSheet(
|
||||
DetailTagButton(
|
||||
icon = Icons.Filled.AutoAwesome,
|
||||
label = "Urgent",
|
||||
selected = false,
|
||||
onClick = { /* TODO */ },
|
||||
selected = currentTag == "urgent",
|
||||
onClick = {
|
||||
val newTag = if (currentTag == "urgent") null else "urgent"
|
||||
currentTag = newTag
|
||||
onUpdateTag(newTag)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
DetailTagButton(
|
||||
icon = Icons.Filled.Done,
|
||||
label = "Offre",
|
||||
selected = false,
|
||||
onClick = { /* TODO */ },
|
||||
selected = currentTag == "offre",
|
||||
onClick = {
|
||||
val newTag = if (currentTag == "offre") null else "offre"
|
||||
currentTag = newTag
|
||||
onUpdateTag(newTag)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
DetailTagButton(
|
||||
icon = Icons.Filled.History,
|
||||
label = "Quand cela convient",
|
||||
selected = false,
|
||||
onClick = { /* TODO */ },
|
||||
selected = currentTag == "whenever",
|
||||
onClick = {
|
||||
val newTag = if (currentTag == "whenever") null else "whenever"
|
||||
currentTag = newTag
|
||||
onUpdateTag(newTag)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
@ -894,7 +952,7 @@ private fun ItemDetailSheet(
|
||||
ParameterButton(
|
||||
icon = Icons.Filled.Camera,
|
||||
label = "Ajouter une photo",
|
||||
onClick = { /* TODO: Photo picker */ },
|
||||
onClick = { photoPickerLauncher.launch("image/*") },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
@ -64,7 +64,8 @@ class ListDetailViewModel @Inject constructor(
|
||||
val safetyStatus: String?,
|
||||
val allergenWarning: String?,
|
||||
val note: String?,
|
||||
val emoji: String
|
||||
val emoji: String,
|
||||
val tag: String?
|
||||
)
|
||||
|
||||
/**
|
||||
@ -322,6 +323,24 @@ class ListDetailViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/** Change le tag visuel d'un article. */
|
||||
fun updateItemTag(id: Long, tag: String?) {
|
||||
viewModelScope.launch {
|
||||
val listId = _listIdFlow.value
|
||||
val item = manageListUseCase.getItems(listId).firstOrNull { it.id == id } ?: return@launch
|
||||
manageListUseCase.updateItem(item.copy(tag = tag))
|
||||
}
|
||||
}
|
||||
|
||||
/** Change l'image (URL/URI) d'un article. */
|
||||
fun updateItemImageUrl(id: Long, imageUrl: String?) {
|
||||
viewModelScope.launch {
|
||||
val listId = _listIdFlow.value
|
||||
val item = manageListUseCase.getItems(listId).firstOrNull { it.id == id } ?: return@launch
|
||||
manageListUseCase.updateItem(item.copy(imageUrl = imageUrl))
|
||||
}
|
||||
}
|
||||
|
||||
/** Change l'emoji personnalisé d'un article. */
|
||||
fun updateItemEmoji(id: Long, emoji: String?) {
|
||||
viewModelScope.launch {
|
||||
@ -402,7 +421,8 @@ class ListDetailViewModel @Inject constructor(
|
||||
safetyStatus = safetyStatus,
|
||||
allergenWarning = allergenWarning,
|
||||
note = note,
|
||||
emoji = customEmoji ?: catalog.emojiFor(productName, category)
|
||||
emoji = customEmoji ?: catalog.emojiFor(productName, category),
|
||||
tag = tag
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@ -5,9 +5,12 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.safebite.app.data.local.database.entity.ShoppingListEntity
|
||||
import com.safebite.app.domain.usecase.GetShoppingListsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@ -35,23 +38,23 @@ class ListsViewModel @Inject constructor(
|
||||
val checkedCount: Int
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val state: StateFlow<UiState> = getShoppingListsUseCase.observeActive()
|
||||
.map { lists ->
|
||||
.flatMapLatest { lists ->
|
||||
if (lists.isEmpty()) {
|
||||
UiState.Empty("Aucune liste de courses. Créez votre première liste !")
|
||||
flowOf(UiState.Empty("Aucune liste de courses. Créez votre première liste !"))
|
||||
} else {
|
||||
// Pour chaque liste, on récupère les stats
|
||||
// Note: Dans une implémentation complète, on utiliserait combine
|
||||
// pour observer les stats en temps réel
|
||||
UiState.Success(
|
||||
lists.map { list ->
|
||||
ShoppingListWithStats(
|
||||
list = list,
|
||||
itemCount = 0, // Sera mis à jour par le Flow
|
||||
checkedCount = 0
|
||||
)
|
||||
val statsFlows = lists.map { list ->
|
||||
combine(
|
||||
getShoppingListsUseCase.observeItemCount(list.id),
|
||||
getShoppingListsUseCase.observeCheckedCount(list.id)
|
||||
) { itemCount, checkedCount ->
|
||||
ShoppingListWithStats(list, itemCount, checkedCount)
|
||||
}
|
||||
)
|
||||
}
|
||||
combine(statsFlows) { array ->
|
||||
UiState.Success(array.toList())
|
||||
}
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
|
||||
@ -13,10 +13,12 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
@ -79,7 +81,15 @@ fun MainScreen(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.app_name)) }
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
actions = {
|
||||
IconButton(onClick = onOpenSettings) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Settings,
|
||||
contentDescription = stringResource(R.string.nav_settings)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@ -108,7 +118,6 @@ fun MainScreen(
|
||||
composable(Screen.Dashboard.route) {
|
||||
DashboardScreen(
|
||||
onScan = onOpenScanner,
|
||||
onOpenSettings = onOpenSettings,
|
||||
onOpenList = onOpenListDetail,
|
||||
onOpenHistoryItem = onOpenHistoryItem
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
MAJOR=1
|
||||
MINOR=7
|
||||
PATCH=0
|
||||
CODE=8
|
||||
PATCH=1
|
||||
CODE=9
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user