Compare commits

...
Author SHA1 Message Date
admin 90ca9775ef Wydaj aplikację kierowcy 1.0.77 (130) 2026-07-30 13:30:22 +02:00
admin 0e33564b95 Popraw zapis i usuwanie zdjęć kierowcy 2026-07-30 13:24:19 +02:00
5 changed files with 137 additions and 49 deletions
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId = "pl.firmatpp.kierowca" applicationId = "pl.firmatpp.kierowca"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 129 versionCode = 130
versionName = "1.0.76" versionName = "1.0.77"
setProperty("archivesBaseName", "pl.firmatpp.kierowca") setProperty("archivesBaseName", "pl.firmatpp.kierowca")
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -13,7 +13,9 @@ import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
import pl.firmatpp.kierowca.data.PhotoUploadMetadata import pl.firmatpp.kierowca.data.PhotoUploadMetadata
import pl.firmatpp.kierowca.data.model.DriverRouteDto import pl.firmatpp.kierowca.data.model.DriverRouteDto
import pl.firmatpp.kierowca.data.model.RoutePhotoDto import pl.firmatpp.kierowca.data.model.RoutePhotoDto
@@ -29,7 +31,13 @@ class PhotoUploadOutbox(
fun observeQueuedUploads(): Flow<List<PhotoUploadEntity>> = fun observeQueuedUploads(): Flow<List<PhotoUploadEntity>> =
dao.observeQueuedUploads() dao.observeQueuedUploads()
suspend fun enqueue(routeId: String, uri: Uri, source: String, metadata: PhotoUploadMetadata, stage: String = "other"): PhotoUploadEntity { suspend fun enqueue(
routeId: String,
uri: Uri,
source: String,
metadata: PhotoUploadMetadata,
stage: String = "other",
): PhotoUploadEntity = withContext(Dispatchers.IO) {
val clientRequestId = UUID.randomUUID().toString() val clientRequestId = UUID.randomUUID().toString()
val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg" val mimeType = context.contentResolver.getType(uri) ?: "image/jpeg"
val extension = when (mimeType) { val extension = when (mimeType) {
@@ -58,7 +66,7 @@ class PhotoUploadOutbox(
dao.upsert(upload) dao.upsert(upload)
enqueueWorker(clientRequestId) enqueueWorker(clientRequestId)
return upload upload
} }
suspend fun retry(clientRequestId: String) { suspend fun retry(clientRequestId: String) {
@@ -62,7 +62,6 @@ import androidx.compose.material.icons.outlined.CalendarToday
import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.CloudUpload import androidx.compose.material.icons.outlined.CloudUpload
import androidx.compose.material.icons.outlined.BugReport import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.ContentCopy import androidx.compose.material.icons.outlined.ContentCopy
import androidx.compose.material.icons.outlined.Factory import androidx.compose.material.icons.outlined.Factory
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
@@ -470,6 +469,9 @@ fun DriverApp(
locationPermissionGranted = locationPermissionGranted, locationPermissionGranted = locationPermissionGranted,
onBack = viewModel::back, onBack = viewModel::back,
onPhoto = viewModel::openPhoto, onPhoto = viewModel::openPhoto,
onDeletePhoto = viewModel::deletePhoto,
onDeleteUpload = viewModel::deletePhotoUpload,
onRetryUpload = viewModel::retryPhotoUpload,
onRefresh = viewModel::refreshSelectedRoute, onRefresh = viewModel::refreshSelectedRoute,
onStartRoute = openStartRoute, onStartRoute = openStartRoute,
onFinishRoute = viewModel::openFinishRoute, onFinishRoute = viewModel::openFinishRoute,
@@ -489,8 +491,9 @@ fun DriverApp(
onBack = viewModel::back, onBack = viewModel::back,
onWeightChange = viewModel::updateRouteStageWeight, onWeightChange = viewModel::updateRouteStageWeight,
onNotesChange = viewModel::updateRouteStageNotes, onNotesChange = viewModel::updateRouteStageNotes,
onUpload = { uri, source, metadata -> viewModel.uploadPhoto(uri, source, metadata, "loading") }, onUpload = { uri, source, metadata -> viewModel.uploadPhoto(uri, source, metadata, "loading")?.join() },
onPhoto = viewModel::openPhoto, onPhoto = viewModel::openPhoto,
onDeletePhoto = viewModel::deletePhoto,
onDeleteUpload = viewModel::deletePhotoUpload, onDeleteUpload = viewModel::deletePhotoUpload,
onRetryUpload = viewModel::retryPhotoUpload, onRetryUpload = viewModel::retryPhotoUpload,
onSubmit = if (routeStageRequirementIsVisible(state.loadingWeightRequirement)) viewModel::openLoadingWeight else viewModel::submitStartRoute, onSubmit = if (routeStageRequirementIsVisible(state.loadingWeightRequirement)) viewModel::openLoadingWeight else viewModel::submitStartRoute,
@@ -510,6 +513,7 @@ fun DriverApp(
onNotesChange = viewModel::updateRouteStageNotes, onNotesChange = viewModel::updateRouteStageNotes,
onUpload = { _, _, _ -> }, onUpload = { _, _, _ -> },
onPhoto = viewModel::openPhoto, onPhoto = viewModel::openPhoto,
onDeletePhoto = viewModel::deletePhoto,
onDeleteUpload = viewModel::deletePhotoUpload, onDeleteUpload = viewModel::deletePhotoUpload,
onRetryUpload = viewModel::retryPhotoUpload, onRetryUpload = viewModel::retryPhotoUpload,
onSubmit = viewModel::submitStartRoute, onSubmit = viewModel::submitStartRoute,
@@ -527,8 +531,9 @@ fun DriverApp(
onBack = viewModel::back, onBack = viewModel::back,
onWeightChange = viewModel::updateRouteStageWeight, onWeightChange = viewModel::updateRouteStageWeight,
onNotesChange = viewModel::updateRouteStageNotes, onNotesChange = viewModel::updateRouteStageNotes,
onUpload = { uri, source, metadata -> viewModel.uploadPhoto(uri, source, metadata, "unloading") }, onUpload = { uri, source, metadata -> viewModel.uploadPhoto(uri, source, metadata, "unloading")?.join() },
onPhoto = viewModel::openPhoto, onPhoto = viewModel::openPhoto,
onDeletePhoto = viewModel::deletePhoto,
onDeleteUpload = viewModel::deletePhotoUpload, onDeleteUpload = viewModel::deletePhotoUpload,
onRetryUpload = viewModel::retryPhotoUpload, onRetryUpload = viewModel::retryPhotoUpload,
onSubmit = viewModel::submitFinishRoute, onSubmit = viewModel::submitFinishRoute,
@@ -4422,8 +4427,9 @@ private fun RouteStageScreen(
onBack: () -> Unit, onBack: () -> Unit,
onWeightChange: (String) -> Unit, onWeightChange: (String) -> Unit,
onNotesChange: (String) -> Unit, onNotesChange: (String) -> Unit,
onUpload: (Uri, String, PhotoUploadMetadata) -> Unit, onUpload: suspend (Uri, String, PhotoUploadMetadata) -> Unit,
onPhoto: (RoutePhotoDto) -> Unit, onPhoto: (RoutePhotoDto) -> Unit,
onDeletePhoto: (RoutePhotoDto) -> Unit,
onDeleteUpload: (PhotoUploadEntity) -> Unit, onDeleteUpload: (PhotoUploadEntity) -> Unit,
onRetryUpload: (PhotoUploadEntity) -> Unit, onRetryUpload: (PhotoUploadEntity) -> Unit,
onSubmit: () -> Unit, onSubmit: () -> Unit,
@@ -4433,6 +4439,7 @@ private fun RouteStageScreen(
var cameraUri by remember { mutableStateOf<Uri?>(null) } var cameraUri by remember { mutableStateOf<Uri?>(null) }
var pendingCameraMode by remember { mutableStateOf<RouteStageCameraMode?>(null) } var pendingCameraMode by remember { mutableStateOf<RouteStageCameraMode?>(null) }
var showMultiplePhotoCamera by remember(stage) { mutableStateOf(false) } var showMultiplePhotoCamera by remember(stage) { mutableStateOf(false) }
var multiplePhotoLocationMetadata by remember(stage) { mutableStateOf(PhotoUploadMetadata()) }
var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) } var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) }
var voiceDictationActive by remember(stage) { mutableStateOf(false) } var voiceDictationActive by remember(stage) { mutableStateOf(false) }
val route = state.displaySelectedRoute val route = state.displaySelectedRoute
@@ -4486,6 +4493,13 @@ private fun RouteStageScreen(
} }
cameraUri = null cameraUri = null
} }
val openMultiplePhotoCamera = {
multiplePhotoLocationMetadata = PhotoUploadMetadata()
showMultiplePhotoCamera = true
coroutineScope.launch {
multiplePhotoLocationMetadata = cameraPhotoMetadata(context).copy(takenAt = null)
}
}
val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants -> val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants ->
val requestedMode = pendingCameraMode val requestedMode = pendingCameraMode
val pendingUri = cameraUri val pendingUri = cameraUri
@@ -4504,7 +4518,7 @@ private fun RouteStageScreen(
if (pendingUri != null) cameraLauncher.launch(pendingUri) if (pendingUri != null) cameraLauncher.launch(pendingUri)
} }
RouteStageCameraMode.Multiple -> { RouteStageCameraMode.Multiple -> {
showMultiplePhotoCamera = true openMultiplePhotoCamera()
} }
null -> Unit null -> Unit
} }
@@ -4514,16 +4528,22 @@ private fun RouteStageScreen(
pendingCameraMode = null pendingCameraMode = null
} }
val pickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> val pickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
if (uri != null) onUpload(uri, "gallery", PhotoUploadMetadata()) if (uri != null) {
coroutineScope.launch {
onUpload(uri, "gallery", PhotoUploadMetadata())
}
}
} }
if (showMultiplePhotoCamera) { if (showMultiplePhotoCamera) {
MultiplePhotoCameraScreen( MultiplePhotoCameraScreen(
stage = stage, stage = stage,
onPhotoCaptured = { uri -> onPhotoCaptured = { uri ->
coroutineScope.launch { onUpload(
onUpload(uri, "camera", cameraPhotoMetadata(context)) uri,
} "camera",
multiplePhotoLocationMetadata.copy(takenAt = Instant.now().toString()),
)
}, },
onClose = { onClose = {
showMultiplePhotoCamera = false showMultiplePhotoCamera = false
@@ -4636,7 +4656,7 @@ private fun RouteStageScreen(
) )
if (missingPermissions.isEmpty()) { if (missingPermissions.isEmpty()) {
pendingCameraMode = null pendingCameraMode = null
showMultiplePhotoCamera = true openMultiplePhotoCamera()
} else { } else {
cameraPermissionLauncher.launch(missingPermissions) cameraPermissionLauncher.launch(missingPermissions)
} }
@@ -4658,7 +4678,7 @@ private fun RouteStageScreen(
deletingPhotoIds = state.deletingPhotoIds, deletingPhotoIds = state.deletingPhotoIds,
imageAuthHeader = state.imageAuthHeader, imageAuthHeader = state.imageAuthHeader,
onPhoto = onPhoto, onPhoto = onPhoto,
onDeletePhoto = {}, onDeletePhoto = onDeletePhoto,
onDeleteUpload = onDeleteUpload, onDeleteUpload = onDeleteUpload,
onRetryUpload = onRetryUpload, onRetryUpload = onRetryUpload,
) )
@@ -4865,6 +4885,9 @@ private fun DetailScreen(
locationPermissionGranted: Boolean, locationPermissionGranted: Boolean,
onBack: () -> Unit, onBack: () -> Unit,
onPhoto: (RoutePhotoDto) -> Unit, onPhoto: (RoutePhotoDto) -> Unit,
onDeletePhoto: (RoutePhotoDto) -> Unit,
onDeleteUpload: (PhotoUploadEntity) -> Unit,
onRetryUpload: (PhotoUploadEntity) -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onStartRoute: () -> Unit, onStartRoute: () -> Unit,
onFinishRoute: () -> Unit, onFinishRoute: () -> Unit,
@@ -4923,6 +4946,9 @@ private fun DetailScreen(
loadingWeightPending = projection.loadingWeightPending, loadingWeightPending = projection.loadingWeightPending,
unloadingWeightPending = projection.unloadingWeightPending, unloadingWeightPending = projection.unloadingWeightPending,
onPhoto = onPhoto, onPhoto = onPhoto,
onDeletePhoto = onDeletePhoto,
onDeleteUpload = onDeleteUpload,
onRetryUpload = onRetryUpload,
) )
} }
item { ErrorText(state.error) } item { ErrorText(state.error) }
@@ -5391,6 +5417,9 @@ private fun CargoDocumentationSection(
loadingWeightPending: Boolean, loadingWeightPending: Boolean,
unloadingWeightPending: Boolean, unloadingWeightPending: Boolean,
onPhoto: (RoutePhotoDto) -> Unit, onPhoto: (RoutePhotoDto) -> Unit,
onDeletePhoto: (RoutePhotoDto) -> Unit,
onDeleteUpload: (PhotoUploadEntity) -> Unit,
onRetryUpload: (PhotoUploadEntity) -> Unit,
) { ) {
val loadingPhotos = routePhotosForStage(route.photos, "loading") val loadingPhotos = routePhotosForStage(route.photos, "loading")
val unloadingPhotos = routePhotosForStage(route.photos, "unloading") val unloadingPhotos = routePhotosForStage(route.photos, "unloading")
@@ -5415,6 +5444,9 @@ private fun CargoDocumentationSection(
imageAuthHeader = imageAuthHeader, imageAuthHeader = imageAuthHeader,
weightPending = loadingWeightPending, weightPending = loadingWeightPending,
onPhoto = onPhoto, onPhoto = onPhoto,
onDeletePhoto = onDeletePhoto,
onDeleteUpload = onDeleteUpload,
onRetryUpload = onRetryUpload,
) )
RouteStageDocumentationBlock( RouteStageDocumentationBlock(
title = "Rozładunek", title = "Rozładunek",
@@ -5426,6 +5458,9 @@ private fun CargoDocumentationSection(
imageAuthHeader = imageAuthHeader, imageAuthHeader = imageAuthHeader,
weightPending = unloadingWeightPending, weightPending = unloadingWeightPending,
onPhoto = onPhoto, onPhoto = onPhoto,
onDeletePhoto = onDeletePhoto,
onDeleteUpload = onDeleteUpload,
onRetryUpload = onRetryUpload,
) )
if (otherPhotos.isNotEmpty() || otherUploads.isNotEmpty()) { if (otherPhotos.isNotEmpty() || otherUploads.isNotEmpty()) {
RouteStageDocumentationBlock( RouteStageDocumentationBlock(
@@ -5438,6 +5473,9 @@ private fun CargoDocumentationSection(
imageAuthHeader = imageAuthHeader, imageAuthHeader = imageAuthHeader,
weightPending = false, weightPending = false,
onPhoto = onPhoto, onPhoto = onPhoto,
onDeletePhoto = onDeletePhoto,
onDeleteUpload = onDeleteUpload,
onRetryUpload = onRetryUpload,
) )
} }
} }
@@ -5454,6 +5492,9 @@ private fun RouteStageDocumentationBlock(
imageAuthHeader: String?, imageAuthHeader: String?,
weightPending: Boolean = false, weightPending: Boolean = false,
onPhoto: (RoutePhotoDto) -> Unit, onPhoto: (RoutePhotoDto) -> Unit,
onDeletePhoto: (RoutePhotoDto) -> Unit,
onDeleteUpload: (PhotoUploadEntity) -> Unit,
onRetryUpload: (PhotoUploadEntity) -> Unit,
) { ) {
val count = visiblePhotoAttachmentCount(photos.size, uploads.size) val count = visiblePhotoAttachmentCount(photos.size, uploads.size)
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
@@ -5536,10 +5577,9 @@ private fun RouteStageDocumentationBlock(
deletingPhotoIds = deletingPhotoIds, deletingPhotoIds = deletingPhotoIds,
imageAuthHeader = imageAuthHeader, imageAuthHeader = imageAuthHeader,
onPhoto = onPhoto, onPhoto = onPhoto,
onDeletePhoto = {}, onDeletePhoto = onDeletePhoto,
onDeleteUpload = {}, onDeleteUpload = onDeleteUpload,
onRetryUpload = {}, onRetryUpload = onRetryUpload,
readOnly = true,
) )
} }
} }
@@ -5569,8 +5609,8 @@ private fun RouteStageCameraActions(
) )
RouteStageCameraActionButton( RouteStageCameraActionButton(
label = "Wiele zdjęć", label = "Wiele zdjęć",
icon = Icons.Outlined.Collections, icon = Icons.Outlined.CameraAlt,
color = TppTheme.colors.navy, color = TppTheme.colors.forest,
compact = true, compact = true,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = onMultiplePhotos, onClick = onMultiplePhotos,
@@ -5588,8 +5628,8 @@ private fun RouteStageCameraActions(
) )
RouteStageCameraActionButton( RouteStageCameraActionButton(
label = "Wiele zdjęć", label = "Wiele zdjęć",
icon = Icons.Outlined.Collections, icon = Icons.Outlined.CameraAlt,
color = TppTheme.colors.navy, color = TppTheme.colors.forest,
compact = false, compact = false,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = onMultiplePhotos, onClick = onMultiplePhotos,
@@ -1478,14 +1478,19 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
} }
} }
fun uploadPhoto(uri: Uri, source: String, metadata: PhotoUploadMetadata = PhotoUploadMetadata(), stage: String = "other") { fun uploadPhoto(
uri: Uri,
source: String,
metadata: PhotoUploadMetadata = PhotoUploadMetadata(),
stage: String = "other",
): Job? {
val snapshot = _state.value val snapshot = _state.value
val route = snapshot.selectedRoute ?: return val route = snapshot.selectedRoute ?: return null
if (!routeIsActiveToday(route, snapshot.selectedDate)) { if (!routeIsActiveToday(route, snapshot.selectedDate)) {
_state.update { it.copy(error = "Zdjęcia możesz dodać tylko w zaplanowanym zakresie kursu.") } _state.update { it.copy(error = "Zdjęcia możesz dodać tylko w zaplanowanym zakresie kursu.") }
return return null
} }
viewModelScope.launch { return viewModelScope.launch {
_state.update { it.copy(error = null) } _state.update { it.copy(error = null) }
runCatching { photoUploadOutbox.enqueue(route.id, uri, source, metadata, stage) } runCatching { photoUploadOutbox.enqueue(route.id, uri, source, metadata, stage) }
.onFailure { throwable -> .onFailure { throwable ->
@@ -12,14 +12,16 @@ import androidx.compose.foundation.background
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
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -39,28 +41,32 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
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.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import java.io.File import java.io.File
import kotlinx.coroutines.launch
import pl.firmatpp.kierowca.ui.theme.TppTheme import pl.firmatpp.kierowca.ui.theme.TppTheme
@Composable @Composable
internal fun MultiplePhotoCameraScreen( internal fun MultiplePhotoCameraScreen(
stage: String, stage: String,
onPhotoCaptured: (Uri) -> Unit, onPhotoCaptured: suspend (Uri) -> Unit,
onClose: () -> Unit, onClose: () -> Unit,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
val currentOnPhotoCaptured by rememberUpdatedState(onPhotoCaptured) val currentOnPhotoCaptured by rememberUpdatedState(onPhotoCaptured)
val cameraController = remember(context) { val cameraController = remember(context) {
LifecycleCameraController(context).apply { LifecycleCameraController(context).apply {
@@ -69,6 +75,7 @@ internal fun MultiplePhotoCameraScreen(
} }
} }
var capturedCount by remember { mutableIntStateOf(0) } var capturedCount by remember { mutableIntStateOf(0) }
var pendingUploads by remember { mutableIntStateOf(0) }
var isCapturing by remember { mutableStateOf(false) } var isCapturing by remember { mutableStateOf(false) }
var cameraUnavailable by remember { mutableStateOf(false) } var cameraUnavailable by remember { mutableStateOf(false) }
var cameraError by remember { mutableStateOf<String?>(null) } var cameraError by remember { mutableStateOf<String?>(null) }
@@ -87,7 +94,7 @@ internal fun MultiplePhotoCameraScreen(
} }
BackHandler { BackHandler {
if (!isCapturing) onClose() if (!isCapturing && pendingUploads == 0) onClose()
} }
Box( Box(
@@ -117,7 +124,7 @@ internal fun MultiplePhotoCameraScreen(
) { ) {
IconButton( IconButton(
onClick = onClose, onClick = onClose,
enabled = !isCapturing, enabled = !isCapturing && pendingUploads == 0,
) { ) {
Icon( Icon(
Icons.AutoMirrored.Outlined.ArrowBack, Icons.AutoMirrored.Outlined.ArrowBack,
@@ -132,7 +139,11 @@ internal fun MultiplePhotoCameraScreen(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
) )
Text( Text(
text = "Wykonano: $capturedCount", text = if (pendingUploads > 0) {
"Wykonano: $capturedCount · zapisuję: $pendingUploads"
} else {
"Wykonano: $capturedCount"
},
color = Color.White.copy(alpha = 0.78f), color = Color.White.copy(alpha = 0.78f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
@@ -161,12 +172,11 @@ internal fun MultiplePhotoCameraScreen(
) )
} }
Row( Box(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
verticalAlignment = Alignment.CenterVertically, .fillMaxWidth()
horizontalArrangement = Arrangement.SpaceBetween, .height(72.dp),
) { ) {
Spacer(Modifier.size(112.dp, 52.dp))
Button( Button(
onClick = { onClick = {
if (isCapturing || cameraUnavailable) return@Button if (isCapturing || cameraUnavailable) return@Button
@@ -182,7 +192,14 @@ internal fun MultiplePhotoCameraScreen(
isCapturing = false isCapturing = false
cameraError = null cameraError = null
capturedCount += 1 capturedCount += 1
currentOnPhotoCaptured(cameraFileUri(context, photoFile)) pendingUploads += 1
coroutineScope.launch {
try {
currentOnPhotoCaptured(cameraFileUri(context, photoFile))
} finally {
pendingUploads = (pendingUploads - 1).coerceAtLeast(0)
}
}
} }
override fun onError(exception: ImageCaptureException) { override fun onError(exception: ImageCaptureException) {
@@ -194,7 +211,9 @@ internal fun MultiplePhotoCameraScreen(
) )
}, },
enabled = !isCapturing && !cameraUnavailable, enabled = !isCapturing && !cameraUnavailable,
modifier = Modifier.size(78.dp), modifier = Modifier
.align(Alignment.Center)
.size(72.dp),
shape = CircleShape, shape = CircleShape,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.White, containerColor = Color.White,
@@ -219,23 +238,39 @@ internal fun MultiplePhotoCameraScreen(
} }
Button( Button(
onClick = onClose, onClick = onClose,
enabled = !isCapturing, enabled = !isCapturing && pendingUploads == 0,
modifier = Modifier.size(width = 112.dp, height = 52.dp), modifier = Modifier
.align(Alignment.CenterEnd)
.width(104.dp)
.height(52.dp),
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = TppTheme.colors.forest, containerColor = TppTheme.colors.forest,
), ),
shape = RoundedCornerShape(6.dp), shape = RoundedCornerShape(6.dp),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
) { ) {
Icon( if (pendingUploads > 0) {
Icons.Outlined.CheckCircle, CircularProgressIndicator(
contentDescription = null, modifier = Modifier.size(16.dp),
tint = Color.White, color = Color.White,
modifier = Modifier.size(20.dp), strokeWidth = 2.dp,
) )
} else {
Icon(
Icons.Outlined.CheckCircle,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(18.dp),
)
}
Text( Text(
text = "Gotowe", text = if (pendingUploads > 0) "Zapisuję" else "Gotowe",
color = Color.White, color = Color.White,
modifier = Modifier.padding(start = 6.dp), style = MaterialTheme.typography.labelMedium,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Clip,
modifier = Modifier.padding(start = 4.dp),
) )
} }
} }