feat: implement photo-based item creation and product-to-list functionality with bottom sheet UI
- Add camera/gallery photo capture for custom shopping list items with name and description fields - Implement `addCustomItemWithImage` in `ListDetailViewModel` to create items with attached photos - Add "Add to list" button in `ResultScreen` with list picker bottom sheet for scanned products - Extend `ResultViewModel` with `addToList` method to save scanned products to shopping lists - Add `PhotoSource
This commit is contained in:
parent
8a19d46949
commit
4ac951cf6e
@ -33,6 +33,7 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.AutoAwesome
|
import androidx.compose.material.icons.filled.AutoAwesome
|
||||||
import androidx.compose.material.icons.filled.Camera
|
import androidx.compose.material.icons.filled.Camera
|
||||||
|
import androidx.compose.material.icons.filled.CameraAlt
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
@ -76,22 +77,28 @@ 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.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import java.io.File
|
||||||
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
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
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.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.safebite.app.R
|
||||||
import com.safebite.app.domain.engine.CatalogProvider
|
import com.safebite.app.domain.engine.CatalogProvider
|
||||||
|
import com.safebite.app.presentation.common.components.PrimaryButton
|
||||||
import com.safebite.app.presentation.theme.LocalDimens
|
import com.safebite.app.presentation.theme.LocalDimens
|
||||||
import com.safebite.app.presentation.theme.LocalStatusColors
|
import com.safebite.app.presentation.theme.LocalStatusColors
|
||||||
|
|
||||||
@ -132,6 +139,37 @@ fun ListDetailScreen(
|
|||||||
var recentlyExpanded by remember { mutableStateOf(true) }
|
var recentlyExpanded by remember { mutableStateOf(true) }
|
||||||
val expandedCategories = remember { mutableStateMapOf<String, Boolean>() }
|
val expandedCategories = remember { mutableStateMapOf<String, Boolean>() }
|
||||||
|
|
||||||
|
var showPhotoPicker by remember { mutableStateOf(false) }
|
||||||
|
var showDescriptionDialog by remember { mutableStateOf(false) }
|
||||||
|
var selectedImageUri by remember { mutableStateOf<String?>(null) }
|
||||||
|
var itemName by remember { mutableStateOf("") }
|
||||||
|
var itemDescription by remember { mutableStateOf("") }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val takePictureLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.TakePicturePreview()
|
||||||
|
) { bitmap: Bitmap? ->
|
||||||
|
bitmap?.let {
|
||||||
|
val file = File(context.cacheDir, "sb_photo_${System.currentTimeMillis()}.jpg")
|
||||||
|
file.outputStream().use { out ->
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||||
|
}
|
||||||
|
selectedImageUri = Uri.fromFile(file).toString()
|
||||||
|
showDescriptionDialog = true
|
||||||
|
}
|
||||||
|
showPhotoPicker = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val pickImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri: Uri? ->
|
||||||
|
uri?.let {
|
||||||
|
selectedImageUri = it.toString()
|
||||||
|
showDescriptionDialog = true
|
||||||
|
}
|
||||||
|
showPhotoPicker = false
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = {
|
topBar = {
|
||||||
@ -151,6 +189,12 @@ fun ListDetailScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
|
IconButton(onClick = { showPhotoPicker = true }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.CameraAlt,
|
||||||
|
contentDescription = stringResource(R.string.list_add_photo)
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(onClick = onOpenScanner) {
|
IconButton(onClick = onOpenScanner) {
|
||||||
Icon(Icons.Filled.Camera, contentDescription = "Scanner")
|
Icon(Icons.Filled.Camera, contentDescription = "Scanner")
|
||||||
}
|
}
|
||||||
@ -271,6 +315,38 @@ fun ListDetailScreen(
|
|||||||
onOpenProduct = selected.barcode?.let { bc -> { onOpenProduct(bc) } }
|
onOpenProduct = selected.barcode?.let { bc -> { onOpenProduct(bc) } }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bottom sheet choix photo / galerie
|
||||||
|
if (showPhotoPicker) {
|
||||||
|
PhotoSourceBottomSheet(
|
||||||
|
onTakePhoto = { takePictureLauncher.launch(null) },
|
||||||
|
onPickGallery = { pickImageLauncher.launch("image/*") },
|
||||||
|
onDismiss = { showPhotoPicker = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialog saisie nom + description pour nouvel item photo
|
||||||
|
if (showDescriptionDialog) {
|
||||||
|
AddPhotoItemDialog(
|
||||||
|
name = itemName,
|
||||||
|
onNameChange = { itemName = it },
|
||||||
|
description = itemDescription,
|
||||||
|
onDescriptionChange = { itemDescription = it },
|
||||||
|
onConfirm = {
|
||||||
|
viewModel.addCustomItemWithImage(itemName, itemDescription, selectedImageUri)
|
||||||
|
itemName = ""
|
||||||
|
itemDescription = ""
|
||||||
|
selectedImageUri = null
|
||||||
|
showDescriptionDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
itemName = ""
|
||||||
|
itemDescription = ""
|
||||||
|
selectedImageUri = null
|
||||||
|
showDescriptionDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -1276,3 +1352,112 @@ private fun ParameterButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun PhotoSourceBottomSheet(
|
||||||
|
onTakePhoto: () -> Unit,
|
||||||
|
onPickGallery: () -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.list_add_photo),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
ParameterButton(
|
||||||
|
icon = Icons.Filled.Camera,
|
||||||
|
label = stringResource(R.string.list_take_photo),
|
||||||
|
onClick = onTakePhoto,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
ParameterButton(
|
||||||
|
icon = Icons.Filled.Add,
|
||||||
|
label = stringResource(R.string.list_pick_gallery),
|
||||||
|
onClick = onPickGallery,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun AddPhotoItemDialog(
|
||||||
|
name: String,
|
||||||
|
onNameChange: (String) -> Unit,
|
||||||
|
description: String,
|
||||||
|
onDescriptionChange: (String) -> Unit,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 16.dp)
|
||||||
|
.navigationBarsPadding()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.list_add_photo),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = onNameChange,
|
||||||
|
label = { Text(stringResource(R.string.list_item_name)) },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = description,
|
||||||
|
onValueChange = onDescriptionChange,
|
||||||
|
label = { Text(stringResource(R.string.list_item_description)) },
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 3,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(20.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
PrimaryButton(
|
||||||
|
text = stringResource(R.string.list_add_item_confirm),
|
||||||
|
onClick = onConfirm,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
enabled = name.isNotBlank()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -256,6 +256,27 @@ class ListDetailViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Crée un item avec photo et description. */
|
||||||
|
fun addCustomItemWithImage(name: String, note: String?, imageUri: String?) {
|
||||||
|
val trimmedName = name.trim()
|
||||||
|
if (trimmedName.isEmpty()) return
|
||||||
|
viewModelScope.launch {
|
||||||
|
val listId = _listIdFlow.value
|
||||||
|
val category = categoryEngine.detectCategory(trimmedName)
|
||||||
|
manageListUseCase.addItemToList(
|
||||||
|
listId,
|
||||||
|
ShoppingListItemEntity(
|
||||||
|
listId = listId,
|
||||||
|
productName = trimmedName,
|
||||||
|
category = category,
|
||||||
|
note = note?.trim()?.ifEmpty { null },
|
||||||
|
imageUrl = imageUri,
|
||||||
|
isChecked = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tap sur un article actif → marque comme acheté (déplace dans Recently Used).
|
* Tap sur un article actif → marque comme acheté (déplace dans Recently Used).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import android.net.Uri
|
|||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -34,8 +35,10 @@ import androidx.compose.material3.HorizontalDivider
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -44,6 +47,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@ -92,6 +96,8 @@ fun ResultScreen(
|
|||||||
viewModel: ResultViewModel = hiltViewModel()
|
viewModel: ResultViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val lists by viewModel.lists.collectAsStateWithLifecycle()
|
||||||
|
var showListPicker by remember { mutableStateOf(false) }
|
||||||
LaunchedEffect(barcode, fromOcr, ocrText) {
|
LaunchedEffect(barcode, fromOcr, ocrText) {
|
||||||
if (fromOcr && !ocrText.isNullOrBlank()) {
|
if (fromOcr && !ocrText.isNullOrBlank()) {
|
||||||
viewModel.analyzeOcrText(ocrText)
|
viewModel.analyzeOcrText(ocrText)
|
||||||
@ -145,7 +151,19 @@ fun ResultScreen(
|
|||||||
is UiState.Success -> ResultContent(
|
is UiState.Success -> ResultContent(
|
||||||
result = s.data,
|
result = s.data,
|
||||||
onScanAgain = onScanAgain,
|
onScanAgain = onScanAgain,
|
||||||
onOcr = onOcr
|
onOcr = onOcr,
|
||||||
|
onAddToList = { showListPicker = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showListPicker) {
|
||||||
|
ListPickerBottomSheet(
|
||||||
|
lists = lists,
|
||||||
|
onSelect = { listId ->
|
||||||
|
viewModel.addToList(listId)
|
||||||
|
showListPicker = false
|
||||||
|
},
|
||||||
|
onDismiss = { showListPicker = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +175,8 @@ fun ResultScreen(
|
|||||||
private fun ResultContent(
|
private fun ResultContent(
|
||||||
result: ScanResult,
|
result: ScanResult,
|
||||||
onScanAgain: () -> Unit,
|
onScanAgain: () -> Unit,
|
||||||
onOcr: () -> Unit
|
onOcr: () -> Unit,
|
||||||
|
onAddToList: () -> Unit
|
||||||
) {
|
) {
|
||||||
var ingredientsExpanded by remember { mutableStateOf(false) }
|
var ingredientsExpanded by remember { mutableStateOf(false) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -284,6 +303,12 @@ private fun ResultContent(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
OutlinedActionButton(
|
||||||
|
text = stringResource(R.string.result_add_to_list),
|
||||||
|
onClick = onAddToList,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
OutlinedActionButton(
|
OutlinedActionButton(
|
||||||
text = stringResource(R.string.action_read_ingredients),
|
text = stringResource(R.string.action_read_ingredients),
|
||||||
@ -618,3 +643,59 @@ private fun EcoScoreBadge(grade: String) {
|
|||||||
Text("🌿$upper", color = Color.White, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
|
Text("🌿$upper", color = Color.White, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ListPickerBottomSheet(
|
||||||
|
lists: List<com.safebite.app.data.local.database.entity.ShoppingListEntity>,
|
||||||
|
onSelect: (Long) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.result_choose_list),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
if (lists.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Aucune liste disponible",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
lists.forEach { list ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable { onSelect(list.id) }
|
||||||
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = list.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,15 +8,20 @@ import com.safebite.app.domain.model.UserProfile
|
|||||||
import com.safebite.app.domain.repository.ProductFetchResult
|
import com.safebite.app.domain.repository.ProductFetchResult
|
||||||
import com.safebite.app.domain.usecase.AnalyzeIngredientsTextUseCase
|
import com.safebite.app.domain.usecase.AnalyzeIngredientsTextUseCase
|
||||||
import com.safebite.app.domain.usecase.AnalyzeProductUseCase
|
import com.safebite.app.domain.usecase.AnalyzeProductUseCase
|
||||||
|
import com.safebite.app.data.local.database.entity.ShoppingListItemEntity
|
||||||
import com.safebite.app.domain.usecase.FetchProductUseCase
|
import com.safebite.app.domain.usecase.FetchProductUseCase
|
||||||
|
import com.safebite.app.domain.usecase.GetShoppingListsUseCase
|
||||||
import com.safebite.app.domain.usecase.ManageProfileUseCase
|
import com.safebite.app.domain.usecase.ManageProfileUseCase
|
||||||
|
import com.safebite.app.domain.usecase.ManageShoppingListUseCase
|
||||||
import com.safebite.app.domain.usecase.SaveScanUseCase
|
import com.safebite.app.domain.usecase.SaveScanUseCase
|
||||||
import com.safebite.app.presentation.common.util.UiState
|
import com.safebite.app.presentation.common.util.UiState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -26,12 +31,17 @@ class ResultViewModel @Inject constructor(
|
|||||||
private val analyzeProduct: AnalyzeProductUseCase,
|
private val analyzeProduct: AnalyzeProductUseCase,
|
||||||
private val analyzeText: AnalyzeIngredientsTextUseCase,
|
private val analyzeText: AnalyzeIngredientsTextUseCase,
|
||||||
private val manageProfile: ManageProfileUseCase,
|
private val manageProfile: ManageProfileUseCase,
|
||||||
private val saveScan: SaveScanUseCase
|
private val saveScan: SaveScanUseCase,
|
||||||
|
private val getLists: GetShoppingListsUseCase,
|
||||||
|
private val manageList: ManageShoppingListUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow<UiState<ScanResult>>(UiState.Idle)
|
private val _state = MutableStateFlow<UiState<ScanResult>>(UiState.Idle)
|
||||||
val state: StateFlow<UiState<ScanResult>> = _state.asStateFlow()
|
val state: StateFlow<UiState<ScanResult>> = _state.asStateFlow()
|
||||||
|
|
||||||
|
val lists = getLists.observeActive()
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
fun analyzeBarcode(barcode: String) = viewModelScope.launch {
|
fun analyzeBarcode(barcode: String) = viewModelScope.launch {
|
||||||
_state.value = UiState.Loading
|
_state.value = UiState.Loading
|
||||||
val profiles = resolveProfiles()
|
val profiles = resolveProfiles()
|
||||||
@ -71,4 +81,21 @@ class ResultViewModel @Inject constructor(
|
|||||||
else -> all.filter { it.isDefault }.ifEmpty { all.take(1) }
|
else -> all.filter { it.isDefault }.ifEmpty { all.take(1) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addToList(listId: Long) = viewModelScope.launch {
|
||||||
|
val currentState = _state.value
|
||||||
|
if (currentState !is UiState.Success) return@launch
|
||||||
|
val result = currentState.data
|
||||||
|
val entity = ShoppingListItemEntity(
|
||||||
|
listId = listId,
|
||||||
|
barcode = result.product.barcode,
|
||||||
|
productName = result.product.name ?: result.product.barcode,
|
||||||
|
brand = result.product.brand,
|
||||||
|
imageUrl = result.product.imageUrl,
|
||||||
|
isChecked = false,
|
||||||
|
safetyStatus = result.safetyStatus.name,
|
||||||
|
allergenWarning = result.detectedAllergens.firstOrNull()?.allergenType?.displayNameFr
|
||||||
|
)
|
||||||
|
manageList.addItemToList(listId, entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -193,4 +193,15 @@
|
|||||||
<string name="allergen_lupin">Lupin</string>
|
<string name="allergen_lupin">Lupin</string>
|
||||||
<string name="allergen_molluscs">Molluscs</string>
|
<string name="allergen_molluscs">Molluscs</string>
|
||||||
<string name="allergen_celery">Celery</string>
|
<string name="allergen_celery">Celery</string>
|
||||||
|
|
||||||
|
<!-- List & Photo additions -->
|
||||||
|
<string name="result_add_to_list">Add to a list</string>
|
||||||
|
<string name="result_choose_list">Choose a list</string>
|
||||||
|
<string name="result_added_to_list">Product added to list</string>
|
||||||
|
<string name="list_add_photo">Add item</string>
|
||||||
|
<string name="list_take_photo">Take a photo</string>
|
||||||
|
<string name="list_pick_gallery">Pick from gallery</string>
|
||||||
|
<string name="list_item_name">Name</string>
|
||||||
|
<string name="list_item_description">Description</string>
|
||||||
|
<string name="list_add_item_confirm">Add</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -306,4 +306,15 @@
|
|||||||
<string name="a11y_verdict_safe">Verdict : produit sûr pour tous les profils</string>
|
<string name="a11y_verdict_safe">Verdict : produit sûr pour tous les profils</string>
|
||||||
<string name="a11y_verdict_warning">Verdict : attention, traces d\'allergènes possibles</string>
|
<string name="a11y_verdict_warning">Verdict : attention, traces d\'allergènes possibles</string>
|
||||||
<string name="a11y_verdict_danger">Verdict : danger, ne pas consommer pour %1$s</string>
|
<string name="a11y_verdict_danger">Verdict : danger, ne pas consommer pour %1$s</string>
|
||||||
|
|
||||||
|
<!-- List & Photo additions -->
|
||||||
|
<string name="result_add_to_list">Ajouter à une liste</string>
|
||||||
|
<string name="result_choose_list">Choisir une liste</string>
|
||||||
|
<string name="result_added_to_list">Produit ajouté à la liste</string>
|
||||||
|
<string name="list_add_photo">Ajouter un élément</string>
|
||||||
|
<string name="list_take_photo">Prendre une photo</string>
|
||||||
|
<string name="list_pick_gallery">Choisir dans la galerie</string>
|
||||||
|
<string name="list_item_name">Nom</string>
|
||||||
|
<string name="list_item_description">Description</string>
|
||||||
|
<string name="list_add_item_confirm">Ajouter</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -1,4 +1,4 @@
|
|||||||
MAJOR=1
|
MAJOR=1
|
||||||
MINOR=7
|
MINOR=8
|
||||||
PATCH=1
|
PATCH=0
|
||||||
CODE=9
|
CODE=10
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user