From 6066527dc056cb4feb4c16343da86c078e3bc242 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 1 Aug 2026 00:32:05 +0200 Subject: [PATCH] =?UTF-8?q?Przyspiesz=20wy=C5=9Bwietlanie=20dodawanych=20z?= =?UTF-8?q?dj=C4=99=C4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/pl/firmatpp/kierowca/ui/DriverApp.kt | 133 +++++++++++++++--- .../firmatpp/kierowca/ui/DriverViewModel.kt | 73 +++++++--- 2 files changed, 174 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt index 72a5388..dab2d3d 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -1202,9 +1202,22 @@ private fun RoutesScreen( val context = LocalContext.current val coroutineScope = rememberCoroutineScope() var cameraUri by remember { mutableStateOf(null) } + var cameraLocationMetadata by remember { mutableStateOf(PhotoUploadMetadata()) } + var cameraMetadataGeneration by remember { mutableStateOf(0L) } var showPreciseLocationPermissionDialog by remember { mutableStateOf(false) } var showRouteDatePicker by remember { mutableStateOf(false) } var showTachographConfirmation by remember { mutableStateOf(false) } + val prepareCameraMetadata = { + val generation = cameraMetadataGeneration + 1 + cameraMetadataGeneration = generation + cameraLocationMetadata = PhotoUploadMetadata() + coroutineScope.launch { + val metadata = cameraPhotoMetadata(context).copy(takenAt = null) + if (cameraMetadataGeneration == generation) { + cameraLocationMetadata = metadata + } + } + } val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> val capturedUri = cameraUri if ( @@ -1215,10 +1228,13 @@ private fun RoutesScreen( hasFineLocation = hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION), ) ) { - coroutineScope.launch { - onDispatchSheetUpload(capturedUri, "camera", cameraPhotoMetadata(context)) - } + onDispatchSheetUpload( + capturedUri, + "camera", + cameraLocationMetadata.copy(takenAt = Instant.now().toString()), + ) } + cameraMetadataGeneration += 1 cameraUri = null } val cameraPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants -> @@ -1228,10 +1244,14 @@ private fun RoutesScreen( hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) if (!canLaunchCameraWithLocationPolicy(state.requirePreciseLocationForPhotos, fineLocationGranted)) { showPreciseLocationPermissionDialog = true + cameraMetadataGeneration += 1 cameraUri = null return@rememberLauncherForActivityResult } - if (cameraGranted && pendingUri != null) cameraLauncher.launch(pendingUri) + if (cameraGranted && pendingUri != null) { + prepareCameraMetadata() + cameraLauncher.launch(pendingUri) + } } LaunchedEffect(state.routeDayLiveUpdateMessage) { @@ -1339,6 +1359,7 @@ private fun RoutesScreen( requirePreciseLocation = state.requirePreciseLocationForPhotos, ) if (missingPermissions.isEmpty()) { + prepareCameraMetadata() cameraLauncher.launch(newUri) } else { cameraPermissionLauncher.launch(missingPermissions) @@ -4449,13 +4470,20 @@ private fun RouteStageScreen( var voiceDictationActive by remember(stage) { mutableStateOf(false) } var cameraAttachmentTarget by remember(stage) { mutableStateOf(null) } var pickerAttachmentTarget by remember(stage) { mutableStateOf(null) } + var singlePhotoLocationMetadata by remember(stage) { mutableStateOf(PhotoUploadMetadata()) } + var singlePhotoMetadataGeneration by remember(stage) { mutableStateOf(0L) } val route = state.displaySelectedRoute val stagePhotos = route?.photos?.let { routePhotosForStage(it, stage) }.orEmpty() val stageUploads = state.photoUploads.filter { normalizedRoutePhotoStage(it.stage) == stage } + val stagePreparations = state.preparingPhotoUploads.filter { normalizedRoutePhotoStage(it.stage) == stage } val usableStageUploads = routeStageUsableUploads(stageUploads) val cargoStagePhotos = stagePhotos.filter { it.workflowDocumentId.isNullOrBlank() } val cargoStageUploads = usableStageUploads.filter { it.workflowDocumentId.isNullOrBlank() } - val attachmentCount = visiblePhotoAttachmentCount(cargoStagePhotos.size, cargoStageUploads.size) + val cargoStagePreparations = stagePreparations.filter { it.workflowDocumentId.isNullOrBlank() } + val attachmentCount = visiblePhotoAttachmentCount( + cargoStagePhotos.size, + cargoStageUploads.size + cargoStagePreparations.size, + ) val photoRequirement = if (stage == "loading") state.loadingPhotoRequirement else state.unloadingPhotoRequirement val weightRequirement = if (stage == "loading") state.loadingWeightRequirement else state.unloadingWeightRequirement val submitBlocker = if (state.routeWorkflow != null) { @@ -4494,9 +4522,21 @@ private fun RouteStageScreen( submitBlocker } val weightBlocker = submitBlocker?.takeIf { it.contains("wag", ignoreCase = true) || it.contains("popraw", ignoreCase = true) } + val prepareSinglePhotoMetadata = { + val generation = singlePhotoMetadataGeneration + 1 + singlePhotoMetadataGeneration = generation + singlePhotoLocationMetadata = PhotoUploadMetadata() + coroutineScope.launch { + val metadata = cameraPhotoMetadata(context).copy(takenAt = null) + if (singlePhotoMetadataGeneration == generation) { + singlePhotoLocationMetadata = metadata + } + } + } val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { ok -> val capturedUri = cameraUri + val attachmentTarget = cameraAttachmentTarget if ( ok && capturedUri != null && @@ -4509,12 +4549,13 @@ private fun RouteStageScreen( onUpload( capturedUri, "camera", - cameraPhotoMetadata(context), - cameraAttachmentTarget?.stepId, - cameraAttachmentTarget?.documentId, + singlePhotoLocationMetadata.copy(takenAt = Instant.now().toString()), + attachmentTarget?.stepId, + attachmentTarget?.documentId, ) } } + singlePhotoMetadataGeneration += 1 cameraUri = null cameraAttachmentTarget = null } @@ -4533,6 +4574,7 @@ private fun RouteStageScreen( hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) if (!canLaunchCameraWithLocationPolicy(state.requirePreciseLocationForPhotos, fineLocationGranted)) { showPreciseLocationPermissionDialog = true + singlePhotoMetadataGeneration += 1 cameraUri = null pendingCameraMode = null return@rememberLauncherForActivityResult @@ -4540,7 +4582,10 @@ private fun RouteStageScreen( if (cameraGranted) { when (requestedMode) { RouteStageCameraMode.Single -> { - if (pendingUri != null) cameraLauncher.launch(pendingUri) + if (pendingUri != null) { + prepareSinglePhotoMetadata() + cameraLauncher.launch(pendingUri) + } } RouteStageCameraMode.Multiple -> { openMultiplePhotoCamera() @@ -4553,28 +4598,30 @@ private fun RouteStageScreen( pendingCameraMode = null } val pickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + val attachmentTarget = pickerAttachmentTarget if (uri != null) { coroutineScope.launch { onUpload( uri, "gallery", PhotoUploadMetadata(), - pickerAttachmentTarget?.stepId, - pickerAttachmentTarget?.documentId, + attachmentTarget?.stepId, + attachmentTarget?.documentId, ) } } pickerAttachmentTarget = null } val filePickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + val attachmentTarget = pickerAttachmentTarget if (uri != null) { coroutineScope.launch { onUpload( uri, "file", PhotoUploadMetadata(), - pickerAttachmentTarget?.stepId, - pickerAttachmentTarget?.documentId, + attachmentTarget?.stepId, + attachmentTarget?.documentId, ) } } @@ -4629,6 +4676,7 @@ private fun RouteStageScreen( weightText = state.routeStageWeightText, serverPhotos = stagePhotos, localUploads = usableStageUploads, + preparingUploads = stagePreparations, onStepToggle = onStepToggle, onTakeDocumentPhoto = { stepId, documentId -> cameraAttachmentTarget = RouteWorkflowAttachmentTarget(stepId, documentId) @@ -4641,6 +4689,7 @@ private fun RouteStageScreen( ) if (missingPermissions.isEmpty()) { pendingCameraMode = null + prepareSinglePhotoMetadata() cameraLauncher.launch(newUri) } else { cameraPermissionLauncher.launch(missingPermissions) @@ -4726,6 +4775,7 @@ private fun RouteStageScreen( ) if (missingPermissions.isEmpty()) { pendingCameraMode = null + prepareSinglePhotoMetadata() cameraLauncher.launch(newUri) } else { cameraPermissionLauncher.launch(missingPermissions) @@ -4761,6 +4811,7 @@ private fun RouteStageScreen( PhotoGrid( photos = cargoStagePhotos, uploads = stageUploads.filter { it.workflowDocumentId.isNullOrBlank() }, + preparations = cargoStagePreparations, deletingPhotoIds = state.deletingPhotoIds, imageAuthHeader = state.imageAuthHeader, onPhoto = onPhoto, @@ -4816,6 +4867,7 @@ private fun RouteWorkflowStepsCard( weightText: String, serverPhotos: List, localUploads: List, + preparingUploads: List, onStepToggle: (String, Boolean) -> Unit, onTakeDocumentPhoto: (String, String) -> Unit, onPickDocumentPhoto: (String, String) -> Unit, @@ -4902,7 +4954,10 @@ private fun RouteWorkflowStepsCard( it.status != PhotoUploadStatus.Cancelled.storageValue && it.status != PhotoUploadStatus.FailedPermanent.storageValue } - val count = documentServerPhotos.size + documentUploads.size + val documentPreparations = preparingUploads.count { + it.workflowStepId == step.id && it.workflowDocumentId == document.id + } + val count = documentServerPhotos.size + documentUploads.size + documentPreparations Column( Modifier .fillMaxWidth() @@ -5951,6 +6006,7 @@ private fun CargoActionButton(label: String, icon: ImageVector, color: Color, on private fun PhotoGrid( photos: List, uploads: List, + preparations: List = emptyList(), deletingPhotoIds: Set, imageAuthHeader: String?, onPhoto: (RoutePhotoDto) -> Unit, @@ -5960,9 +6016,13 @@ private fun PhotoGrid( readOnly: Boolean = false, ) { var pendingDelete by remember { mutableStateOf(null) } - val items = (uploads.map { PhotoGridItem.Upload(it) } + photos.map { PhotoGridItem.Server(it) }) + val items = ( + preparations.map { PhotoGridItem.Preparing(it) } + + uploads.map { PhotoGridItem.Upload(it) } + + photos.map { PhotoGridItem.Server(it) } + ) .sortedByDescending { it.sortEpochMillis } - val visibleAttachmentCount = visiblePhotoAttachmentCount(photos.size, uploads.size) + val visibleAttachmentCount = visiblePhotoAttachmentCount(photos.size, uploads.size + preparations.size) Card( colors = CardDefaults.cardColors(containerColor = TppTheme.colors.card), @@ -6009,6 +6069,10 @@ private fun PhotoGrid( readOnly = readOnly, modifier = Modifier.weight(1f), ) + is PhotoGridItem.Preparing -> PreparingPhotoTile( + preparation = item.preparation, + modifier = Modifier.weight(1f), + ) } } if (rowItems.size == 1) { @@ -6055,6 +6119,9 @@ private sealed class PhotoGridItem(open val sortEpochMillis: Long) { ) data class Upload(val upload: PhotoUploadEntity) : PhotoGridItem(upload.createdAtEpochMillis) + + data class Preparing(val preparation: PreparingPhotoUpload) : + PhotoGridItem(preparation.createdAtEpochMillis) } private sealed class PhotoDeleteTarget { @@ -6288,6 +6355,40 @@ private fun PendingPhotoTile( } } +@Composable +private fun PreparingPhotoTile( + preparation: PreparingPhotoUpload, + modifier: Modifier = Modifier, +) { + Box(modifier.aspectRatio(1f).background(TppTheme.colors.panel, RoundedCornerShape(4.dp))) { + AsyncImage( + model = preparation.uri, + contentDescription = "Przygotowywane zdjęcie", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + ) + Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.24f))) + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center).size(30.dp), + color = Color.White, + trackColor = Color.White.copy(alpha = 0.3f), + strokeWidth = 2.5.dp, + ) + Text( + "Przygotowuję zdjęcie", + color = TppTheme.colors.ink, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.labelMedium, + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .background(Color.White.copy(alpha = 0.94f)) + .padding(10.dp), + ) + } +} + @Composable private fun PhotoTileLoadingOverlay() { Box( diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt index fa7fc2e..80585df 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverViewModel.kt @@ -73,6 +73,16 @@ enum class DriverScreen { Initializing, Phone, Otp, Routes, Profile, Diagnostics enum class ServerConnectionState { Unknown, Reachable, Degraded } +data class PreparingPhotoUpload( + val id: String, + val routeId: String, + val uri: Uri, + val stage: String, + val workflowStepId: String?, + val workflowDocumentId: String?, + val createdAtEpochMillis: Long = System.currentTimeMillis(), +) + data class DriverUiState( val screen: DriverScreen = DriverScreen.Initializing, val loading: Boolean = true, @@ -111,6 +121,7 @@ data class DriverUiState( val routeActions: List = emptyList(), val visibleRouteActions: List = emptyList(), val photoUploads: List = emptyList(), + val preparingPhotoUploads: List = emptyList(), val queuedPhotoUploads: List = emptyList(), val dispatchSheetReminder: DispatchSheetReminderDto? = null, val tachographReminder: TachographReminderDto? = null, @@ -1496,23 +1507,46 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) _state.update { it.copy(error = "Zdjęcia możesz dodać tylko w zaplanowanym zakresie kursu.") } return null } + val preparation = PreparingPhotoUpload( + id = UUID.randomUUID().toString(), + routeId = route.id, + uri = uri, + stage = stage, + workflowStepId = workflowStepId, + workflowDocumentId = workflowDocumentId, + ) + _state.update { + it.copy( + preparingPhotoUploads = it.preparingPhotoUploads + preparation, + error = null, + ) + } return viewModelScope.launch { - _state.update { it.copy(error = null) } - runCatching { - photoUploadOutbox.enqueue( - routeId = route.id, - uri = uri, - source = source, - metadata = metadata, - stage = stage, - workflowVersion = snapshot.routeWorkflow?.version, - workflowStepId = workflowStepId, - workflowDocumentId = workflowDocumentId, - ) - } - .onFailure { throwable -> - _state.update { it.withApiError(throwable) } + try { + runCatching { + photoUploadOutbox.enqueue( + routeId = route.id, + uri = uri, + source = source, + metadata = metadata, + stage = stage, + workflowVersion = snapshot.routeWorkflow?.version, + workflowStepId = workflowStepId, + workflowDocumentId = workflowDocumentId, + ) } + .onFailure { throwable -> + _state.update { it.withApiError(throwable) } + } + } finally { + _state.update { + it.copy( + preparingPhotoUploads = it.preparingPhotoUploads.filterNot { item -> + item.id == preparation.id + }, + ) + } + } } } @@ -2017,7 +2051,14 @@ class DriverViewModel(application: Application) : AndroidViewModel(application) DriverScreen.Detail -> { photoUploadsJob?.cancel() routeActionsJob?.cancel() - it.copy(screen = DriverScreen.Routes, selectedRoute = null, routeActions = emptyList(), photoUploads = emptyList(), feedback = null) + it.copy( + screen = DriverScreen.Routes, + selectedRoute = null, + routeActions = emptyList(), + photoUploads = emptyList(), + preparingPhotoUploads = emptyList(), + feedback = null, + ) } DriverScreen.Profile -> it.copy(screen = DriverScreen.Routes) DriverScreen.Diagnostics -> it.copy(screen = DriverScreen.Profile)