Dodaj usuwanie świeżo zapisanych zdjęć
This commit is contained in:
@@ -69,6 +69,13 @@ class PhotoUploadOutbox(
|
||||
enqueueWorker(clientRequestId)
|
||||
}
|
||||
|
||||
suspend fun discard(clientRequestId: String) {
|
||||
dao.find(clientRequestId)?.let { upload ->
|
||||
File(upload.localPath).delete()
|
||||
dao.delete(upload.clientRequestId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun discardConfirmedServerPhotos(route: DriverRouteDto) {
|
||||
val confirmedRequestIds = route.photos.mapNotNull { it.clientRequestId }.distinct()
|
||||
if (confirmedRequestIds.isEmpty()) return
|
||||
|
||||
@@ -179,7 +179,16 @@ fun DriverApp(viewModel: DriverViewModel) {
|
||||
onRoute = viewModel::openRoute,
|
||||
)
|
||||
DriverScreen.Profile -> ProfileScreen(state, viewModel::refreshRoutes, viewModel::openProfile, viewModel::logout)
|
||||
DriverScreen.Detail -> DetailScreen(state, viewModel::back, viewModel::uploadPhoto, viewModel::openPhoto, viewModel::deletePhoto, viewModel::retryPhotoUpload, viewModel::refreshSelectedRoute)
|
||||
DriverScreen.Detail -> DetailScreen(
|
||||
state,
|
||||
viewModel::back,
|
||||
viewModel::uploadPhoto,
|
||||
viewModel::openPhoto,
|
||||
viewModel::deletePhoto,
|
||||
viewModel::deleteConfirmedUpload,
|
||||
viewModel::retryPhotoUpload,
|
||||
viewModel::refreshSelectedRoute,
|
||||
)
|
||||
DriverScreen.Photo -> PhotoScreen(state, viewModel::back)
|
||||
}
|
||||
|
||||
@@ -738,6 +747,7 @@ private fun DetailScreen(
|
||||
onUpload: (Uri, String, PhotoUploadMetadata) -> Unit,
|
||||
onPhoto: (RoutePhotoDto) -> Unit,
|
||||
onDeletePhoto: (RoutePhotoDto) -> Unit,
|
||||
onDeleteUpload: (PhotoUploadEntity) -> Unit,
|
||||
onRetryUpload: (PhotoUploadEntity) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
@@ -784,6 +794,7 @@ private fun DetailScreen(
|
||||
canManagePhotos = canManagePhotos,
|
||||
onPhoto = onPhoto,
|
||||
onDeletePhoto = onDeletePhoto,
|
||||
onDeleteUpload = onDeleteUpload,
|
||||
onRetryUpload = onRetryUpload,
|
||||
onCamera = {
|
||||
val newUri = createCameraUri(context)
|
||||
@@ -975,6 +986,7 @@ private fun CargoDocumentationSection(
|
||||
canManagePhotos: Boolean,
|
||||
onPhoto: (RoutePhotoDto) -> Unit,
|
||||
onDeletePhoto: (RoutePhotoDto) -> Unit,
|
||||
onDeleteUpload: (PhotoUploadEntity) -> Unit,
|
||||
onRetryUpload: (PhotoUploadEntity) -> Unit,
|
||||
onCamera: () -> Unit,
|
||||
onGallery: () -> Unit,
|
||||
@@ -997,7 +1009,7 @@ private fun CargoDocumentationSection(
|
||||
}
|
||||
}
|
||||
}
|
||||
PhotoGrid(photos, uploads, deletingPhotoIds, imageAuthHeader, onPhoto, onDeletePhoto, onRetryUpload)
|
||||
PhotoGrid(photos, uploads, deletingPhotoIds, imageAuthHeader, onPhoto, onDeletePhoto, onDeleteUpload, onRetryUpload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1023,9 +1035,10 @@ private fun PhotoGrid(
|
||||
imageAuthHeader: String?,
|
||||
onPhoto: (RoutePhotoDto) -> Unit,
|
||||
onDeletePhoto: (RoutePhotoDto) -> Unit,
|
||||
onDeleteUpload: (PhotoUploadEntity) -> Unit,
|
||||
onRetryUpload: (PhotoUploadEntity) -> Unit,
|
||||
) {
|
||||
var photoPendingDelete by remember { mutableStateOf<RoutePhotoDto?>(null) }
|
||||
var pendingDelete by remember { mutableStateOf<PhotoDeleteTarget?>(null) }
|
||||
val items = (uploads.map { PhotoGridItem.Upload(it) } + photos.map { PhotoGridItem.Server(it) })
|
||||
.sortedByDescending { it.sortEpochMillis }
|
||||
val visibleAttachmentCount = visiblePhotoAttachmentCount(photos.size, uploads.size)
|
||||
@@ -1057,11 +1070,13 @@ private fun PhotoGrid(
|
||||
imageAuthHeader = imageAuthHeader,
|
||||
isDeleting = item.photo.id in deletingPhotoIds,
|
||||
onPhoto = onPhoto,
|
||||
onDeletePhoto = { photoPendingDelete = it },
|
||||
onDeletePhoto = { pendingDelete = PhotoDeleteTarget.Server(it) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
is PhotoGridItem.Upload -> PendingPhotoTile(
|
||||
upload = item.upload,
|
||||
isDeleting = item.upload.serverPhotoId?.let { it in deletingPhotoIds } == true,
|
||||
onDelete = { pendingDelete = PhotoDeleteTarget.Upload(it) },
|
||||
onRetry = onRetryUpload,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -1077,23 +1092,26 @@ private fun PhotoGrid(
|
||||
}
|
||||
}
|
||||
|
||||
photoPendingDelete?.let { photo ->
|
||||
pendingDelete?.let { target ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { photoPendingDelete = null },
|
||||
onDismissRequest = { pendingDelete = null },
|
||||
title = { Text("Usunąć zdjęcie?", color = TppColors.Ink, fontWeight = FontWeight.Bold) },
|
||||
text = { Text("Czy na pewno chcesz usunąć to zdjęcie?", color = TppColors.Muted) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
photoPendingDelete = null
|
||||
onDeletePhoto(photo)
|
||||
pendingDelete = null
|
||||
when (target) {
|
||||
is PhotoDeleteTarget.Server -> onDeletePhoto(target.photo)
|
||||
is PhotoDeleteTarget.Upload -> onDeleteUpload(target.upload)
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text("Usuń", color = TppColors.Error, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { photoPendingDelete = null }) {
|
||||
TextButton(onClick = { pendingDelete = null }) {
|
||||
Text("Anuluj", color = TppColors.Muted)
|
||||
}
|
||||
},
|
||||
@@ -1109,6 +1127,11 @@ private sealed class PhotoGridItem(open val sortEpochMillis: Long) {
|
||||
data class Upload(val upload: PhotoUploadEntity) : PhotoGridItem(upload.createdAtEpochMillis)
|
||||
}
|
||||
|
||||
private sealed class PhotoDeleteTarget {
|
||||
data class Server(val photo: RoutePhotoDto) : PhotoDeleteTarget()
|
||||
data class Upload(val upload: PhotoUploadEntity) : PhotoDeleteTarget()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PhotoTile(
|
||||
photo: RoutePhotoDto,
|
||||
@@ -1158,10 +1181,13 @@ private fun PhotoTile(
|
||||
@Composable
|
||||
private fun PendingPhotoTile(
|
||||
upload: PhotoUploadEntity,
|
||||
isDeleting: Boolean,
|
||||
onDelete: (PhotoUploadEntity) -> Unit,
|
||||
onRetry: (PhotoUploadEntity) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val status = upload.statusType
|
||||
val canDeleteServerPhoto = confirmedUploadServerPhotoId(upload.status, upload.serverPhotoId) != null
|
||||
Box(modifier.aspectRatio(1f).background(TppColors.Panel, RoundedCornerShape(4.dp))) {
|
||||
AsyncImage(
|
||||
model = File(upload.localPath),
|
||||
@@ -1170,6 +1196,33 @@ private fun PendingPhotoTile(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.34f)))
|
||||
if (isDeleting) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text("Usuwam", color = TppColors.Muted, fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
if (canDeleteServerPhoto) {
|
||||
IconButton(
|
||||
onClick = { onDelete(upload) },
|
||||
enabled = !isDeleting,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(8.dp)
|
||||
.size(40.dp)
|
||||
.background(TppColors.Surface.copy(alpha = 0.96f), RoundedCornerShape(20.dp))
|
||||
.border(1.dp, TppColors.Error.copy(alpha = 0.35f), RoundedCornerShape(20.dp)),
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Delete,
|
||||
contentDescription = "Usuń zdjęcie",
|
||||
tint = TppColors.Error,
|
||||
modifier = Modifier.size(19.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
Modifier.align(Alignment.BottomStart).fillMaxWidth().background(Color.White.copy(alpha = 0.94f)).padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
||||
@@ -17,6 +17,9 @@ fun routePhotoSortEpochMillis(createdAt: String?, takenAt: String?, fallback: Lo
|
||||
?: parseIsoOffsetEpochMillis(takenAt)
|
||||
?: fallback
|
||||
|
||||
fun confirmedUploadServerPhotoId(status: String, serverPhotoId: String?): String? =
|
||||
serverPhotoId?.takeIf { status == "CONFIRMED" && it.isNotBlank() }
|
||||
|
||||
private fun parseIsoOffsetEpochMillis(value: String?): Long? =
|
||||
value?.takeIf { it.isNotBlank() }?.let {
|
||||
runCatching { OffsetDateTime.parse(it).toInstant().toEpochMilli() }.getOrNull()
|
||||
|
||||
@@ -210,6 +210,31 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteConfirmedUpload(upload: PhotoUploadEntity) {
|
||||
val route = _state.value.selectedRoute ?: return
|
||||
val serverPhotoId = confirmedUploadServerPhotoId(upload.status, upload.serverPhotoId) ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds + serverPhotoId, error = null) }
|
||||
runCatching {
|
||||
repository.deletePhoto(serverPhotoId)
|
||||
photoUploadOutbox.discard(upload.clientRequestId)
|
||||
val response = repository.route(route.id)
|
||||
photoUploadOutbox.discardConfirmedServerPhotos(response.route)
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedRoute = response.route,
|
||||
routes = it.routes.map { item -> if (item.id == route.id) response.route else item },
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}.onFailure { throwable ->
|
||||
_state.update { it.copy(error = ApiErrorMapper.map(throwable).message) }
|
||||
}
|
||||
_state.update { it.copy(deletingPhotoIds = it.deletingPhotoIds - serverPhotoId) }
|
||||
}
|
||||
}
|
||||
|
||||
fun openPhoto(photo: RoutePhotoDto) {
|
||||
_state.update { it.copy(screen = DriverScreen.Photo, selectedPhoto = photo) }
|
||||
}
|
||||
|
||||
@@ -60,4 +60,12 @@ class DriverUiRulesTest {
|
||||
)
|
||||
assertEquals(fallback, routePhotoSortEpochMillis(createdAt = null, takenAt = null, fallback = fallback))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allowsDeleteIconForConfirmedLocalUploadWithServerPhotoId() {
|
||||
assertEquals("42", confirmedUploadServerPhotoId("CONFIRMED", "42"))
|
||||
assertEquals(null, confirmedUploadServerPhotoId("CONFIRMED", ""))
|
||||
assertEquals(null, confirmedUploadServerPhotoId("UPLOADING", "42"))
|
||||
assertEquals(null, confirmedUploadServerPhotoId("FAILED_RETRYABLE", "42"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user