feat: add interactive image cropping with bottom sheet UI for shopping list item photos
- Extract `ImageCropBottomSheet` component for reusable crop interface - Remove inline `saveCroppedImage` helper in favor of centralized cropping flow - Add tap-to-recrop functionality on existing item photos in detail sheet - Implement crop state management with `cropBitmap`, `cropForNewItem`, and `cropForItemId` flags - Pass `onRequestCrop` callback to `ItemDetailSheet` for both camera/gallery and existing
This commit is contained in:
parent
48a9266942
commit
c8dff8df40
@ -0,0 +1,345 @@
|
|||||||
|
package com.safebite.app.presentation.common.components
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bottom sheet permettant de recadrer une image avec zoom/pan.
|
||||||
|
* Affiche un cadre blanc centré (type scanner) et un overlay sombre.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ImageCropBottomSheet(
|
||||||
|
bitmap: Bitmap,
|
||||||
|
onCropComplete: (String?) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var containerSize by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
|
||||||
|
val bmpW = bitmap.width.toFloat()
|
||||||
|
val bmpH = bitmap.height.toFloat()
|
||||||
|
|
||||||
|
val containerW = containerSize.width.toFloat().coerceAtLeast(1f)
|
||||||
|
val containerH = containerSize.height.toFloat().coerceAtLeast(1f)
|
||||||
|
|
||||||
|
val fitScale = remember(containerW, containerH, bmpW, bmpH) {
|
||||||
|
min(containerW / bmpW, containerH / bmpH)
|
||||||
|
}
|
||||||
|
|
||||||
|
val initialOffsetX = (containerW - bmpW * fitScale) / 2f
|
||||||
|
val initialOffsetY = (containerH - bmpH * fitScale) / 2f
|
||||||
|
|
||||||
|
val totalScale = fitScale * scale
|
||||||
|
val displayW = bmpW * totalScale
|
||||||
|
val displayH = bmpH * totalScale
|
||||||
|
|
||||||
|
val frameSize = min(containerW, containerH) * 0.8f
|
||||||
|
val frameLeft = (containerW - frameSize) / 2f
|
||||||
|
val frameTop = (containerH - frameSize) / 2f
|
||||||
|
val frameRight = frameLeft + frameSize
|
||||||
|
val frameBottom = frameTop + frameSize
|
||||||
|
|
||||||
|
// Contraintes de pan pour garder le cadre rempli
|
||||||
|
val minOffsetX = if (displayW > frameSize) {
|
||||||
|
frameLeft - initialOffsetX - displayW
|
||||||
|
} else {
|
||||||
|
(containerW - displayW) / 2f - initialOffsetX
|
||||||
|
}
|
||||||
|
val maxOffsetX = if (displayW > frameSize) {
|
||||||
|
frameRight - initialOffsetX
|
||||||
|
} else {
|
||||||
|
(containerW - displayW) / 2f - initialOffsetX
|
||||||
|
}
|
||||||
|
val minOffsetY = if (displayH > frameSize) {
|
||||||
|
frameTop - initialOffsetY - displayH
|
||||||
|
} else {
|
||||||
|
(containerH - displayH) / 2f - initialOffsetY
|
||||||
|
}
|
||||||
|
val maxOffsetY = if (displayH > frameSize) {
|
||||||
|
frameBottom - initialOffsetY
|
||||||
|
} else {
|
||||||
|
(containerH - displayH) / 2f - initialOffsetY
|
||||||
|
}
|
||||||
|
|
||||||
|
val totalOffsetX = initialOffsetX + offsetX
|
||||||
|
val totalOffsetY = initialOffsetY + offsetY
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
sheetState = sheetState,
|
||||||
|
dragHandle = null
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(520.dp)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Ajuster le cadrage",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onSizeChanged { containerSize = it }
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, _ ->
|
||||||
|
val newScale = (scale * zoom).coerceIn(1f, 5f)
|
||||||
|
val scaleChange = newScale / scale
|
||||||
|
scale = newScale
|
||||||
|
|
||||||
|
// Ajuste l'offset pour zoomer autour du centre du cadre
|
||||||
|
val focusX = containerW / 2f
|
||||||
|
val focusY = containerH / 2f
|
||||||
|
val relFocusX = focusX - totalOffsetX
|
||||||
|
val relFocusY = focusY - totalOffsetY
|
||||||
|
|
||||||
|
offsetX = (offsetX + pan.x - relFocusX * (scaleChange - 1f))
|
||||||
|
.coerceIn(minOffsetX, maxOffsetX)
|
||||||
|
offsetY = (offsetY + pan.y - relFocusY * (scaleChange - 1f))
|
||||||
|
.coerceIn(minOffsetY, maxOffsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Image zoomable/pannable
|
||||||
|
Image(
|
||||||
|
bitmap = bitmap.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.None,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer {
|
||||||
|
this.scaleX = totalScale
|
||||||
|
this.scaleY = totalScale
|
||||||
|
this.translationX = totalOffsetX
|
||||||
|
this.translationY = totalOffsetY
|
||||||
|
transformOrigin = androidx.compose.ui.graphics.TransformOrigin(0f, 0f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Overlay sombre + cadre blanc
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val fSize = min(size.width, size.height) * 0.8f
|
||||||
|
val fLeft = (size.width - fSize) / 2f
|
||||||
|
val fTop = (size.height - fSize) / 2f
|
||||||
|
val fRight = fLeft + fSize
|
||||||
|
val fBottom = fTop + fSize
|
||||||
|
|
||||||
|
// Top
|
||||||
|
drawRect(
|
||||||
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
|
topLeft = Offset(0f, 0f),
|
||||||
|
size = Size(size.width, fTop)
|
||||||
|
)
|
||||||
|
// Bottom
|
||||||
|
drawRect(
|
||||||
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
|
topLeft = Offset(0f, fBottom),
|
||||||
|
size = Size(size.width, size.height - fBottom)
|
||||||
|
)
|
||||||
|
// Left
|
||||||
|
drawRect(
|
||||||
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
|
topLeft = Offset(0f, fTop),
|
||||||
|
size = Size(fLeft, fSize)
|
||||||
|
)
|
||||||
|
// Right
|
||||||
|
drawRect(
|
||||||
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
|
topLeft = Offset(fRight, fTop),
|
||||||
|
size = Size(size.width - fRight, fSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Coins du cadre (type scanner)
|
||||||
|
val cornerLength = fSize * 0.15f
|
||||||
|
val strokeWidth = 3f
|
||||||
|
val cornerColor = Color.White
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fLeft, fTop),
|
||||||
|
Offset(fLeft + cornerLength, fTop),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fLeft, fTop),
|
||||||
|
Offset(fLeft, fTop + cornerLength),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// Top-right
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fRight, fTop),
|
||||||
|
Offset(fRight - cornerLength, fTop),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fRight, fTop),
|
||||||
|
Offset(fRight, fTop + cornerLength),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bottom-left
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fLeft, fBottom),
|
||||||
|
Offset(fLeft + cornerLength, fBottom),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fLeft, fBottom),
|
||||||
|
Offset(fLeft, fBottom - cornerLength),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bottom-right
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fRight, fBottom),
|
||||||
|
Offset(fRight - cornerLength, fBottom),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
cornerColor,
|
||||||
|
Offset(fRight, fBottom),
|
||||||
|
Offset(fRight, fBottom - cornerLength),
|
||||||
|
strokeWidth = strokeWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.navigationBarsPadding(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text("Annuler")
|
||||||
|
}
|
||||||
|
PrimaryButton(
|
||||||
|
text = "OK",
|
||||||
|
onClick = {
|
||||||
|
val cropped = cropAndSaveBitmap(
|
||||||
|
bitmap,
|
||||||
|
containerW, containerH,
|
||||||
|
frameLeft, frameTop, frameSize,
|
||||||
|
totalOffsetX, totalOffsetY, totalScale,
|
||||||
|
context.cacheDir
|
||||||
|
)
|
||||||
|
onCropComplete(cropped)
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
large = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cropAndSaveBitmap(
|
||||||
|
bitmap: Bitmap,
|
||||||
|
containerW: Float,
|
||||||
|
containerH: Float,
|
||||||
|
frameLeft: Float,
|
||||||
|
frameTop: Float,
|
||||||
|
frameSize: Float,
|
||||||
|
totalOffsetX: Float,
|
||||||
|
totalOffsetY: Float,
|
||||||
|
totalScale: Float,
|
||||||
|
cacheDir: File
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
val cropX = ((frameLeft - totalOffsetX) / totalScale).toInt().coerceIn(0, bitmap.width)
|
||||||
|
val cropY = ((frameTop - totalOffsetY) / totalScale).toInt().coerceIn(0, bitmap.height)
|
||||||
|
val cropW = (frameSize / totalScale).toInt().coerceIn(1, bitmap.width - cropX)
|
||||||
|
val cropH = (frameSize / totalScale).toInt().coerceIn(1, bitmap.height - cropY)
|
||||||
|
|
||||||
|
val cropped = Bitmap.createBitmap(bitmap, cropX, cropY, cropW, cropH)
|
||||||
|
val maxSize = 512
|
||||||
|
val scaled = if (cropW > maxSize || cropH > maxSize) {
|
||||||
|
val ratio = maxSize.toFloat() / maxOf(cropW, cropH)
|
||||||
|
val newW = (cropW * ratio).toInt()
|
||||||
|
val newH = (cropH * ratio).toInt()
|
||||||
|
Bitmap.createScaledBitmap(cropped, newW, newH, true).also {
|
||||||
|
if (it != cropped) cropped.recycle()
|
||||||
|
}
|
||||||
|
} else cropped
|
||||||
|
|
||||||
|
val file = File(cacheDir, "item_${System.currentTimeMillis()}.jpg")
|
||||||
|
file.outputStream().use { out ->
|
||||||
|
scaled.compress(Bitmap.CompressFormat.JPEG, 85, out)
|
||||||
|
}
|
||||||
|
if (scaled != cropped && scaled != bitmap) scaled.recycle()
|
||||||
|
Uri.fromFile(file).toString()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -101,6 +101,7 @@ 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.R
|
||||||
import com.safebite.app.domain.engine.CatalogProvider
|
import com.safebite.app.domain.engine.CatalogProvider
|
||||||
|
import com.safebite.app.presentation.common.components.ImageCropBottomSheet
|
||||||
import com.safebite.app.presentation.common.components.PrimaryButton
|
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
|
||||||
@ -149,12 +150,17 @@ fun ListDetailScreen(
|
|||||||
var itemDescription by remember { mutableStateOf("") }
|
var itemDescription by remember { mutableStateOf("") }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var cropBitmap by remember { mutableStateOf<Bitmap?>(null) }
|
||||||
|
var cropForNewItem by remember { mutableStateOf(false) }
|
||||||
|
var cropForItemId by remember { mutableStateOf<Long?>(null) }
|
||||||
|
|
||||||
val takePictureLauncher = rememberLauncherForActivityResult(
|
val takePictureLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.TakePicturePreview()
|
ActivityResultContracts.TakePicturePreview()
|
||||||
) { bitmap: Bitmap? ->
|
) { bitmap: Bitmap? ->
|
||||||
bitmap?.let {
|
bitmap?.let {
|
||||||
selectedImageUri = saveCroppedImage(context, it)
|
cropBitmap = it
|
||||||
showDescriptionDialog = true
|
cropForNewItem = true
|
||||||
|
cropForItemId = null
|
||||||
}
|
}
|
||||||
showPhotoPicker = false
|
showPhotoPicker = false
|
||||||
}
|
}
|
||||||
@ -167,8 +173,9 @@ fun ListDetailScreen(
|
|||||||
BitmapFactory.decodeStream(stream)
|
BitmapFactory.decodeStream(stream)
|
||||||
}
|
}
|
||||||
bmp?.let { bitmap ->
|
bmp?.let { bitmap ->
|
||||||
selectedImageUri = saveCroppedImage(context, bitmap)
|
cropBitmap = bitmap
|
||||||
showDescriptionDialog = true
|
cropForNewItem = true
|
||||||
|
cropForItemId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showPhotoPicker = false
|
showPhotoPicker = false
|
||||||
@ -316,7 +323,35 @@ fun ListDetailScreen(
|
|||||||
onUpdateImage = { imageUrl -> viewModel.updateItemImageUrl(selected.id, imageUrl) },
|
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) } },
|
||||||
|
onRequestCrop = { bitmap ->
|
||||||
|
cropBitmap = bitmap
|
||||||
|
cropForNewItem = false
|
||||||
|
cropForItemId = selected.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom sheet recadrage image
|
||||||
|
cropBitmap?.let { bmp ->
|
||||||
|
ImageCropBottomSheet(
|
||||||
|
bitmap = bmp,
|
||||||
|
onCropComplete = { croppedUri ->
|
||||||
|
cropBitmap = null
|
||||||
|
if (cropForNewItem) {
|
||||||
|
selectedImageUri = croppedUri
|
||||||
|
showDescriptionDialog = true
|
||||||
|
} else {
|
||||||
|
cropForItemId?.let { id -> viewModel.updateItemImageUrl(id, croppedUri) }
|
||||||
|
}
|
||||||
|
cropForNewItem = false
|
||||||
|
cropForItemId = null
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
cropBitmap = null
|
||||||
|
cropForNewItem = false
|
||||||
|
cropForItemId = null
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -353,26 +388,6 @@ fun ListDetailScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Croppe au centre en carré, redimensionne à [maxSize] et sauvegarde en JPEG dans le cache. */
|
|
||||||
private fun saveCroppedImage(context: android.content.Context, bitmap: Bitmap, maxSize: Int = 512): String? {
|
|
||||||
return try {
|
|
||||||
val w = bitmap.width
|
|
||||||
val h = bitmap.height
|
|
||||||
val side = kotlin.math.min(w, h)
|
|
||||||
val x = (w - side) / 2
|
|
||||||
val y = (h - side) / 2
|
|
||||||
val cropped = Bitmap.createBitmap(bitmap, x, y, side, side)
|
|
||||||
val scaled = Bitmap.createScaledBitmap(cropped, maxSize, maxSize, true)
|
|
||||||
val file = File(context.cacheDir, "item_${System.currentTimeMillis()}.jpg")
|
|
||||||
file.outputStream().use { out ->
|
|
||||||
scaled.compress(Bitmap.CompressFormat.JPEG, 85, out)
|
|
||||||
}
|
|
||||||
Uri.fromFile(file).toString()
|
|
||||||
} catch (_: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Contenu principal scrollable
|
// Contenu principal scrollable
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -942,7 +957,8 @@ private fun ItemDetailSheet(
|
|||||||
onUpdateImage: (String?) -> Unit,
|
onUpdateImage: (String?) -> Unit,
|
||||||
onMoveTo: (Long) -> Unit,
|
onMoveTo: (Long) -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onOpenProduct: (() -> Unit)?
|
onOpenProduct: (() -> Unit)?,
|
||||||
|
onRequestCrop: (Bitmap) -> Unit
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@ -960,9 +976,7 @@ private fun ItemDetailSheet(
|
|||||||
val bmp = context.contentResolver.openInputStream(it)?.use { stream ->
|
val bmp = context.contentResolver.openInputStream(it)?.use { stream ->
|
||||||
BitmapFactory.decodeStream(stream)
|
BitmapFactory.decodeStream(stream)
|
||||||
}
|
}
|
||||||
bmp?.let { bitmap ->
|
bmp?.let { bitmap -> onRequestCrop(bitmap) }
|
||||||
onUpdateImage(saveCroppedImage(context, bitmap))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1015,7 +1029,17 @@ private fun ItemDetailSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(180.dp)
|
.height(180.dp)
|
||||||
.clip(RoundedCornerShape(12.dp)),
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.clickable {
|
||||||
|
val bmp = try {
|
||||||
|
val uri = Uri.parse(item.imageUrl)
|
||||||
|
when {
|
||||||
|
item.imageUrl.startsWith("file://") -> BitmapFactory.decodeFile(uri.path)
|
||||||
|
else -> context.contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it) }
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
bmp?.let { onRequestCrop(it) }
|
||||||
|
},
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
MAJOR=1
|
MAJOR=1
|
||||||
MINOR=9
|
MINOR=9
|
||||||
PATCH=0
|
PATCH=2
|
||||||
CODE=11
|
CODE=13
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user