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:
Bruno Charest 2026-04-26 15:23:26 -04:00
parent b68212b99c
commit 8a19d46949
9 changed files with 259 additions and 61 deletions

View File

@ -21,7 +21,7 @@ import com.safebite.app.data.local.database.entity.UserProfileEntity
ShoppingListEntity::class, ShoppingListEntity::class,
ShoppingListItemEntity::class ShoppingListItemEntity::class
], ],
version = 4, version = 5,
exportSchema = false exportSchema = false
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)

View File

@ -99,5 +99,6 @@ data class ShoppingListItemEntity(
val allergenWarning: String? = null, // Allergène détecté pour alerte val allergenWarning: String? = null, // Allergène détecté pour alerte
val note: String? = null, // Quantité / description libre (ex: "2 kg") val note: String? = null, // Quantité / description libre (ex: "2 kg")
val customEmoji: String? = null, // Emoji personnalisé choisi par l'utilisateur val customEmoji: String? = null, // Emoji personnalisé choisi par l'utilisateur
val tag: String? = null, // Tag visuel : "urgent", "offre", "whenever"
val addedAt: Long = System.currentTimeMillis() val addedAt: Long = System.currentTimeMillis()
) )

View File

@ -4,33 +4,32 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll 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.Card
import androidx.compose.material3.CardDefaults 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.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp 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.R
import com.safebite.app.presentation.common.components.OutlinedActionButton
import com.safebite.app.presentation.common.components.PrimaryButton 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). * 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 * - store_mode : détecté via géolocalisation/heure
* - home_mode : mode par défaut * - home_mode : mode par défaut
*/ */
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DashboardScreen( fun DashboardScreen(
onScan: () -> Unit, onScan: () -> Unit,
onOpenSettings: () -> Unit,
onOpenList: (Long, String) -> Unit, onOpenList: (Long, String) -> Unit,
onOpenHistoryItem: (String) -> Unit onOpenHistoryItem: (String) -> Unit,
viewModel: DashboardViewModel = hiltViewModel()
) { ) {
val state by viewModel.state.collectAsStateWithLifecycle()
Scaffold( Scaffold(
containerColor = MaterialTheme.colorScheme.background, 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))
}
}
)
}
) { padding -> ) { padding ->
Column( Column(
modifier = Modifier modifier = Modifier
@ -71,7 +61,7 @@ fun DashboardScreen(
) { ) {
// Greeting // Greeting
Text( Text(
text = stringResource(R.string.dashboard_greeting, "Sophie"), text = stringResource(R.string.dashboard_greeting, state.greetingName),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
@ -82,11 +72,48 @@ fun DashboardScreen(
onClick = onScan, onClick = onScan,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
OutlinedActionButton(
text = stringResource(R.string.dashboard_lists_button), // Shopping lists quick access
onClick = { onOpenList(0, "Ma liste") }, if (state.lists.isNotEmpty()) {
modifier = Modifier.fillMaxWidth() 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 // Weekly stats placeholder
Card( Card(

View File

@ -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
} ?: ""
}
}

View File

@ -76,6 +76,9 @@ import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -261,6 +264,8 @@ fun ListDetailScreen(
onUpdateNote = { note -> viewModel.updateItemNote(selected.id, note) }, onUpdateNote = { note -> viewModel.updateItemNote(selected.id, note) },
onUpdateCategory = { cat -> viewModel.updateItemCategory(selected.id, cat) }, onUpdateCategory = { cat -> viewModel.updateItemCategory(selected.id, cat) },
onUpdateEmoji = { emoji -> viewModel.updateItemEmoji(selected.id, emoji) }, 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) }, onMoveTo = { targetListId -> viewModel.moveItemToList(selected.id, targetListId) },
onDelete = { viewModel.deleteItem(selected.id) }, onDelete = { viewModel.deleteItem(selected.id) },
onOpenProduct = selected.barcode?.let { bc -> { onOpenProduct(bc) } } onOpenProduct = selected.barcode?.let { bc -> { onOpenProduct(bc) } }
@ -289,6 +294,15 @@ private fun ListDetailContent(
) { ) {
val dimens = LocalDimens.current 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( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues( contentPadding = PaddingValues(
@ -315,7 +329,8 @@ private fun ListDetailContent(
emoji = it.emoji, emoji = it.emoji,
imageUrl = it.imageUrl, imageUrl = it.imageUrl,
tone = TileTone.Active, tone = TileTone.Active,
badgeWarning = !it.allergenWarning.isNullOrBlank() badgeWarning = !it.allergenWarning.isNullOrBlank(),
tag = it.tag
) )
}, },
onTap = onTapActive, onTap = onTapActive,
@ -345,7 +360,8 @@ private fun ListDetailContent(
emoji = it.emoji, emoji = it.emoji,
imageUrl = it.imageUrl, imageUrl = it.imageUrl,
tone = TileTone.Recent, tone = TileTone.Recent,
badgeWarning = false badgeWarning = false,
tag = it.tag
) )
}, },
onTap = onTapRecent, onTap = onTapRecent,
@ -373,14 +389,17 @@ private fun ListDetailContent(
item(key = "header-$category") { item(key = "header-$category") {
CollapsibleHeader( CollapsibleHeader(
title = category, title = category,
count = catalog.itemsForCategory(category).size, count = filteredCatalogCounts[category] ?: 0,
expanded = expanded, expanded = expanded,
onToggle = { onToggleCategory(category) } onToggle = { onToggleCategory(category) }
) )
} }
if (expanded) { if (expanded) {
item(key = "grid-$category") { 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( TileGrid(
items = items.map { items = items.map {
TileData( TileData(
@ -418,7 +437,8 @@ private data class TileData(
val emoji: String, val emoji: String,
val imageUrl: String?, val imageUrl: String?,
val tone: TileTone, val tone: TileTone,
val badgeWarning: Boolean val badgeWarning: Boolean,
val tag: String? = null
) )
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ -528,6 +548,23 @@ private fun Tile(
.size(16.dp) .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, onUpdateNote: (String) -> Unit,
onUpdateCategory: (String) -> Unit, onUpdateCategory: (String) -> Unit,
onUpdateEmoji: (String?) -> Unit, onUpdateEmoji: (String?) -> Unit,
onUpdateTag: (String?) -> Unit,
onUpdateImage: (String?) -> Unit,
onMoveTo: (Long) -> Unit, onMoveTo: (Long) -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,
onOpenProduct: (() -> Unit)? onOpenProduct: (() -> Unit)?
) { ) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var note by remember(item.id) { mutableStateOf(item.note.orEmpty()) } 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 showCategoryPicker by remember { mutableStateOf(false) }
var showIconPicker by remember { mutableStateOf(false) } var showIconPicker by remember { mutableStateOf(false) }
var showMovePicker by remember { mutableStateOf(false) } var showMovePicker by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val photoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent()
) { uri: Uri? ->
uri?.let { onUpdateImage(it.toString()) }
}
ModalBottomSheet( ModalBottomSheet(
onDismissRequest = { onDismissRequest = {
// Persiste la note avant fermeture si elle a changé. // Persiste la note avant fermeture si elle a changé.
@ -853,22 +899,34 @@ private fun ItemDetailSheet(
DetailTagButton( DetailTagButton(
icon = Icons.Filled.AutoAwesome, icon = Icons.Filled.AutoAwesome,
label = "Urgent", label = "Urgent",
selected = false, selected = currentTag == "urgent",
onClick = { /* TODO */ }, onClick = {
val newTag = if (currentTag == "urgent") null else "urgent"
currentTag = newTag
onUpdateTag(newTag)
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
DetailTagButton( DetailTagButton(
icon = Icons.Filled.Done, icon = Icons.Filled.Done,
label = "Offre", label = "Offre",
selected = false, selected = currentTag == "offre",
onClick = { /* TODO */ }, onClick = {
val newTag = if (currentTag == "offre") null else "offre"
currentTag = newTag
onUpdateTag(newTag)
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
DetailTagButton( DetailTagButton(
icon = Icons.Filled.History, icon = Icons.Filled.History,
label = "Quand cela convient", label = "Quand cela convient",
selected = false, selected = currentTag == "whenever",
onClick = { /* TODO */ }, onClick = {
val newTag = if (currentTag == "whenever") null else "whenever"
currentTag = newTag
onUpdateTag(newTag)
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
@ -894,7 +952,7 @@ private fun ItemDetailSheet(
ParameterButton( ParameterButton(
icon = Icons.Filled.Camera, icon = Icons.Filled.Camera,
label = "Ajouter une photo", label = "Ajouter une photo",
onClick = { /* TODO: Photo picker */ }, onClick = { photoPickerLauncher.launch("image/*") },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }

View File

@ -64,7 +64,8 @@ class ListDetailViewModel @Inject constructor(
val safetyStatus: String?, val safetyStatus: String?,
val allergenWarning: String?, val allergenWarning: String?,
val note: 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. */ /** Change l'emoji personnalisé d'un article. */
fun updateItemEmoji(id: Long, emoji: String?) { fun updateItemEmoji(id: Long, emoji: String?) {
viewModelScope.launch { viewModelScope.launch {
@ -402,7 +421,8 @@ class ListDetailViewModel @Inject constructor(
safetyStatus = safetyStatus, safetyStatus = safetyStatus,
allergenWarning = allergenWarning, allergenWarning = allergenWarning,
note = note, note = note,
emoji = customEmoji ?: catalog.emojiFor(productName, category) emoji = customEmoji ?: catalog.emojiFor(productName, category),
tag = tag
) )
companion object { companion object {

View File

@ -5,9 +5,12 @@ import androidx.lifecycle.viewModelScope
import com.safebite.app.data.local.database.entity.ShoppingListEntity import com.safebite.app.data.local.database.entity.ShoppingListEntity
import com.safebite.app.domain.usecase.GetShoppingListsUseCase import com.safebite.app.domain.usecase.GetShoppingListsUseCase
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow 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.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -35,23 +38,23 @@ class ListsViewModel @Inject constructor(
val checkedCount: Int val checkedCount: Int
) )
@OptIn(ExperimentalCoroutinesApi::class)
val state: StateFlow<UiState> = getShoppingListsUseCase.observeActive() val state: StateFlow<UiState> = getShoppingListsUseCase.observeActive()
.map { lists -> .flatMapLatest { lists ->
if (lists.isEmpty()) { 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 { } else {
// Pour chaque liste, on récupère les stats val statsFlows = lists.map { list ->
// Note: Dans une implémentation complète, on utiliserait combine combine(
// pour observer les stats en temps réel getShoppingListsUseCase.observeItemCount(list.id),
UiState.Success( getShoppingListsUseCase.observeCheckedCount(list.id)
lists.map { list -> ) { itemCount, checkedCount ->
ShoppingListWithStats( ShoppingListWithStats(list, itemCount, checkedCount)
list = list, }
itemCount = 0, // Sera mis à jour par le Flow }
checkedCount = 0 combine(statsFlows) { array ->
) UiState.Success(array.toList())
} }
)
} }
} }
.stateIn( .stateIn(

View File

@ -13,10 +13,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FabPosition import androidx.compose.material3.FabPosition
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
@ -79,7 +81,15 @@ fun MainScreen(
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
topBar = { topBar = {
TopAppBar( 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 = { bottomBar = {
@ -108,7 +118,6 @@ fun MainScreen(
composable(Screen.Dashboard.route) { composable(Screen.Dashboard.route) {
DashboardScreen( DashboardScreen(
onScan = onOpenScanner, onScan = onOpenScanner,
onOpenSettings = onOpenSettings,
onOpenList = onOpenListDetail, onOpenList = onOpenListDetail,
onOpenHistoryItem = onOpenHistoryItem onOpenHistoryItem = onOpenHistoryItem
) )

View File

@ -1,4 +1,4 @@
MAJOR=1 MAJOR=1
MINOR=7 MINOR=7
PATCH=0 PATCH=1
CODE=8 CODE=9