Añadir la opcion de la lista de la compra que por articulo se pueda actualizar el precio
This commit is contained in:
@@ -49,6 +49,7 @@ dependencies {
|
|||||||
implementation(libs.androidx.camera.lifecycle)
|
implementation(libs.androidx.camera.lifecycle)
|
||||||
implementation(libs.androidx.camera.view)
|
implementation(libs.androidx.camera.view)
|
||||||
implementation(libs.barcode.scanning)
|
implementation(libs.barcode.scanning)
|
||||||
|
implementation(libs.text.recognition)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
implementation(libs.androidx.work.runtime)
|
implementation(libs.androidx.work.runtime)
|
||||||
|
|||||||
@@ -99,6 +99,25 @@ class PantryRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateProductPrice(productId: Int, newPrice: Double, userName: String): Result<Boolean> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val connection = getConnection()
|
||||||
|
val sql = "UPDATE productos SET precio = ?, modificado_por = ? WHERE id = ?"
|
||||||
|
val statement = connection.prepareStatement(sql)
|
||||||
|
statement.setDouble(1, newPrice)
|
||||||
|
statement.setString(2, userName)
|
||||||
|
statement.setInt(3, productId)
|
||||||
|
|
||||||
|
val rowsUpdated = statement.executeUpdate()
|
||||||
|
connection.close()
|
||||||
|
Result.success(rowsUpdated > 0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Result.failure(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun updateProductFull(
|
suspend fun updateProductFull(
|
||||||
productId: Int,
|
productId: Int,
|
||||||
nombre: String,
|
nombre: String,
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package com.example.despensapp.ui.navigation
|
package com.example.despensapp.ui.navigation
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.NavType
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import com.example.despensapp.ui.screens.LoginScreen
|
|
||||||
import com.example.despensapp.ui.screens.PantryScreen
|
|
||||||
import com.example.despensapp.ui.screens.RegisterScreen
|
|
||||||
import com.example.despensapp.ui.screens.AddProductScreen
|
|
||||||
import com.example.despensapp.ui.screens.ProductDetailScreen
|
|
||||||
import com.example.despensapp.ui.screens.SettingsScreen
|
|
||||||
import androidx.navigation.NavType
|
|
||||||
import androidx.navigation.navArgument
|
import androidx.navigation.navArgument
|
||||||
|
import com.example.despensapp.data.repository.PantryRepository
|
||||||
|
import com.example.despensapp.data.session.UserSession
|
||||||
|
import com.example.despensapp.ui.screens.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
sealed class Screen(val route: String) {
|
sealed class Screen(val route: String) {
|
||||||
object Login : Screen("login")
|
object Login : Screen("login")
|
||||||
@@ -19,6 +19,9 @@ sealed class Screen(val route: String) {
|
|||||||
object Pantry : Screen("pantry")
|
object Pantry : Screen("pantry")
|
||||||
object AddProduct : Screen("add_product")
|
object AddProduct : Screen("add_product")
|
||||||
object Settings : Screen("settings")
|
object Settings : Screen("settings")
|
||||||
|
object PriceScanner : Screen("price_scanner/{productId}") {
|
||||||
|
fun createRoute(productId: Int) = "price_scanner/$productId"
|
||||||
|
}
|
||||||
object ProductDetail : Screen("product_detail/{productId}") {
|
object ProductDetail : Screen("product_detail/{productId}") {
|
||||||
fun createRoute(productId: Int) = "product_detail/$productId"
|
fun createRoute(productId: Int) = "product_detail/$productId"
|
||||||
}
|
}
|
||||||
@@ -65,6 +68,9 @@ fun NavGraph(navController: NavHostController) {
|
|||||||
onNavigateToSettings = {
|
onNavigateToSettings = {
|
||||||
navController.navigate(Screen.Settings.route)
|
navController.navigate(Screen.Settings.route)
|
||||||
},
|
},
|
||||||
|
onNavigateToPriceScanner = { productId ->
|
||||||
|
navController.navigate(Screen.PriceScanner.createRoute(productId))
|
||||||
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
navController.navigate(Screen.Login.route) {
|
navController.navigate(Screen.Login.route) {
|
||||||
popUpTo(Screen.Pantry.route) { inclusive = true }
|
popUpTo(Screen.Pantry.route) { inclusive = true }
|
||||||
@@ -82,6 +88,27 @@ fun NavGraph(navController: NavHostController) {
|
|||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
composable(
|
||||||
|
route = Screen.PriceScanner.route,
|
||||||
|
arguments = listOf(navArgument("productId") { type = NavType.IntType })
|
||||||
|
) { backStackEntry ->
|
||||||
|
val productId = backStackEntry.arguments?.getInt("productId") ?: 0
|
||||||
|
val repository = PantryRepository()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
PriceScannerScreen(
|
||||||
|
onPriceScanned = { price ->
|
||||||
|
scope.launch {
|
||||||
|
val userName = UserSession.getUserName(context)
|
||||||
|
repository.updateProductPrice(productId, price, userName).onSuccess {
|
||||||
|
navController.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBack = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
composable(
|
composable(
|
||||||
route = Screen.ProductDetail.route,
|
route = Screen.ProductDetail.route,
|
||||||
arguments = listOf(navArgument("productId") { type = NavType.IntType })
|
arguments = listOf(navArgument("productId") { type = NavType.IntType })
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ fun PantryScreen(
|
|||||||
onNavigateToAdd: () -> Unit,
|
onNavigateToAdd: () -> Unit,
|
||||||
onNavigateToDetail: (Int) -> Unit,
|
onNavigateToDetail: (Int) -> Unit,
|
||||||
onNavigateToSettings: () -> Unit,
|
onNavigateToSettings: () -> Unit,
|
||||||
|
onNavigateToPriceScanner: (Int) -> Unit,
|
||||||
onLogout: () -> Unit
|
onLogout: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -323,6 +324,8 @@ fun PantryScreen(
|
|||||||
ProductItem(
|
ProductItem(
|
||||||
product = product,
|
product = product,
|
||||||
onItemClick = { onNavigateToDetail(product.id) },
|
onItemClick = { onNavigateToDetail(product.id) },
|
||||||
|
showPriceUpdater = showShoppingList,
|
||||||
|
onUpdatePrice = { onNavigateToPriceScanner(product.id) },
|
||||||
onQuantityChange = { newQty ->
|
onQuantityChange = { newQty ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val success = repository.updateProductQuantity(product.id, newQty, userName)
|
val success = repository.updateProductQuantity(product.id, newQty, userName)
|
||||||
@@ -397,7 +400,13 @@ fun isNearExpiry(dateStr: String?): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProductItem(product: Product, onItemClick: () -> Unit, onQuantityChange: (Int) -> Unit) {
|
fun ProductItem(
|
||||||
|
product: Product,
|
||||||
|
onItemClick: () -> Unit,
|
||||||
|
showPriceUpdater: Boolean = false,
|
||||||
|
onUpdatePrice: () -> Unit = {},
|
||||||
|
onQuantityChange: (Int) -> Unit
|
||||||
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -454,6 +463,12 @@ fun ProductItem(product: Product, onItemClick: () -> Unit, onQuantityChange: (In
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = product.name,
|
text = product.name,
|
||||||
@@ -466,6 +481,25 @@ fun ProductItem(product: Product, onItemClick: () -> Unit, onQuantityChange: (In
|
|||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
color = Color.Gray
|
color = Color.Gray
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botones de acción arriba a la derecha en columna (+ arriba, - abajo)
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
FilledIconButton(
|
||||||
|
onClick = { onQuantityChange(product.quantity + 1) },
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_product_more), modifier = Modifier.size(18.dp))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = { if (product.quantity > 0) onQuantityChange(product.quantity - 1) },
|
||||||
|
modifier = Modifier.size(36.dp)
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Remove, contentDescription = stringResource(R.string.add_product_less), modifier = Modifier.size(18.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Información de Fechas y Usuario (organizada en columna para evitar solapamientos)
|
// Información de Fechas y Usuario (organizada en columna para evitar solapamientos)
|
||||||
Column(modifier = Modifier.padding(top = 4.dp)) {
|
Column(modifier = Modifier.padding(top = 4.dp)) {
|
||||||
@@ -533,10 +567,11 @@ fun ProductItem(product: Product, onItemClick: () -> Unit, onQuantityChange: (In
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
) {
|
||||||
|
// Recuadro de Unidades y Precio con peso flexible
|
||||||
Surface(
|
Surface(
|
||||||
|
modifier = Modifier.weight(1f, fill = false),
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
color = if (product.isLowStock) MaterialTheme.colorScheme.errorContainer
|
color = if (product.isLowStock) MaterialTheme.colorScheme.errorContainer
|
||||||
else MaterialTheme.colorScheme.secondaryContainer,
|
else MaterialTheme.colorScheme.secondaryContainer,
|
||||||
@@ -561,19 +596,18 @@ fun ProductItem(product: Product, onItemClick: () -> Unit, onQuantityChange: (In
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
// Botón de OCR Precio (Solo en modo Lista Compra)
|
||||||
FilledTonalIconButton(
|
if (showPriceUpdater) {
|
||||||
onClick = { if (product.quantity > 0) onQuantityChange(product.quantity - 1) },
|
|
||||||
modifier = Modifier.size(32.dp)
|
|
||||||
) {
|
|
||||||
Icon(Icons.Default.Remove, contentDescription = stringResource(R.string.add_product_less), modifier = Modifier.size(18.dp))
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
FilledIconButton(
|
IconButton(
|
||||||
onClick = { onQuantityChange(product.quantity + 1) },
|
onClick = onUpdatePrice,
|
||||||
modifier = Modifier.size(32.dp)
|
modifier = Modifier.size(32.dp)
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_product_more), modifier = Modifier.size(18.dp))
|
Icon(
|
||||||
|
Icons.Default.QrCodeScanner,
|
||||||
|
contentDescription = stringResource(R.string.pantry_update_price),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -587,6 +621,6 @@ fun ProductItem(product: Product, onItemClick: () -> Unit, onQuantityChange: (In
|
|||||||
@Composable
|
@Composable
|
||||||
fun PantryScreenPreview() {
|
fun PantryScreenPreview() {
|
||||||
DespensappTheme {
|
DespensappTheme {
|
||||||
PantryScreen(onNavigateToAdd = {}, onNavigateToDetail = {}, onNavigateToSettings = {}, onLogout = {})
|
PantryScreen(onNavigateToAdd = {}, onNavigateToDetail = {}, onNavigateToSettings = {}, onNavigateToPriceScanner = {}, onLogout = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package com.example.despensapp.ui.screens
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.camera.core.*
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.FlashOff
|
||||||
|
import androidx.compose.material.icons.filled.FlashOn
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.example.despensapp.R
|
||||||
|
import com.google.mlkit.vision.common.InputImage
|
||||||
|
import com.google.mlkit.vision.text.TextRecognition
|
||||||
|
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun PriceScannerScreen(onPriceScanned: (Double) -> Unit, onBack: () -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
|
||||||
|
|
||||||
|
var torchEnabled by remember { mutableStateOf(false) }
|
||||||
|
var detectedText by remember { mutableStateOf("") }
|
||||||
|
var detectedPrice by remember { mutableStateOf<Double?>(null) }
|
||||||
|
|
||||||
|
val recognizer = remember { TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.pantry_scan_price)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { torchEnabled = !torchEnabled }) {
|
||||||
|
Icon(
|
||||||
|
if (torchEnabled) Icons.Default.FlashOn else Icons.Default.FlashOff,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (torchEnabled) Color.Yellow else LocalContentColor.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
val previewView = PreviewView(ctx)
|
||||||
|
val executor = ContextCompat.getMainExecutor(ctx)
|
||||||
|
cameraProviderFuture.addListener({
|
||||||
|
val cameraProvider = cameraProviderFuture.get()
|
||||||
|
val preview = Preview.Builder().build().also {
|
||||||
|
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
val imageAnalysis = ImageAnalysis.Builder()
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
imageAnalysis.setAnalyzer(executor) { imageProxy ->
|
||||||
|
@androidx.camera.core.ExperimentalGetImage
|
||||||
|
val mediaImage = imageProxy.image
|
||||||
|
if (mediaImage != null) {
|
||||||
|
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||||
|
recognizer.process(image)
|
||||||
|
.addOnSuccessListener { visionText ->
|
||||||
|
val fullText = visionText.text
|
||||||
|
// Lógica simple para extraer precio: buscar algo como X,XX o X.XX
|
||||||
|
val priceRegex = """(\d+[,.]\d{2})""".toRegex()
|
||||||
|
val match = priceRegex.find(fullText)
|
||||||
|
if (match != null) {
|
||||||
|
val priceStr = match.value.replace(",", ".")
|
||||||
|
detectedPrice = priceStr.toDoubleOrNull()
|
||||||
|
detectedText = match.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.addOnCompleteListener { imageProxy.close() }
|
||||||
|
} else {
|
||||||
|
imageProxy.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||||
|
try {
|
||||||
|
cameraProvider.unbindAll()
|
||||||
|
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||||
|
camera.cameraControl.enableTorch(torchEnabled)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("PriceScanner", "Binding failed", e)
|
||||||
|
}
|
||||||
|
}, executor)
|
||||||
|
previewView
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marco de enfoque
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(250.dp, 100.dp)
|
||||||
|
.border(2.dp, Color.White.copy(alpha = 0.5f), RoundedCornerShape(12.dp))
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.pantry_ocr_instruction),
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 32.dp)
|
||||||
|
.background(Color.Black.copy(alpha = 0.5f), RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panel inferior con el precio detectado
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (detectedText.isNotEmpty()) "Precio detectado: $detectedText €" else "Buscando precio...",
|
||||||
|
style = MaterialTheme.typography.headlineSmall
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { detectedPrice?.let { onPriceScanned(it) } },
|
||||||
|
enabled = detectedPrice != null,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.save))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,10 @@
|
|||||||
<string name="pantry_expiry_date">Caduca: %1$s</string>
|
<string name="pantry_expiry_date">Caduca: %1$s</string>
|
||||||
<string name="pantry_added_date">Añadido: %1$s</string>
|
<string name="pantry_added_date">Añadido: %1$s</string>
|
||||||
<string name="pantry_last_modified_by">Por: %1$s</string>
|
<string name="pantry_last_modified_by">Por: %1$s</string>
|
||||||
|
<string name="pantry_update_price">Actualizar Precio</string>
|
||||||
|
<string name="pantry_scan_price">Escanear Precio</string>
|
||||||
|
<string name="pantry_ocr_instruction">Apunta al precio de la etiqueta</string>
|
||||||
|
<string name="pantry_price_updated">Precio actualizado a %1$.2f €</string>
|
||||||
|
|
||||||
<!-- Add Product -->
|
<!-- Add Product -->
|
||||||
<string name="add_product_title">Añadir Producto</string>
|
<string name="add_product_title">Añadir Producto</string>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ okhttp = "4.12.0"
|
|||||||
mariadbJavaClient = "3.5.10"
|
mariadbJavaClient = "3.5.10"
|
||||||
camerax = "1.4.1"
|
camerax = "1.4.1"
|
||||||
barcodeScanning = "17.3.0"
|
barcodeScanning = "17.3.0"
|
||||||
|
textRecognition = "16.0.1"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
workManager = "2.10.0"
|
workManager = "2.10.0"
|
||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
@@ -44,6 +45,7 @@ androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2",
|
|||||||
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
|
androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
|
||||||
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
|
androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
|
||||||
barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "barcodeScanning" }
|
barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "barcodeScanning" }
|
||||||
|
text-recognition = { group = "com.google.mlkit", name = "text-recognition", version.ref = "textRecognition" }
|
||||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||||
androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" }
|
androidx-work-runtime = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workManager" }
|
||||||
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
|
|||||||
Reference in New Issue
Block a user