Add route detail photo tile loaders
This commit is contained in:
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId = "pl.firmatpp.kierowca"
|
applicationId = "pl.firmatpp.kierowca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 45
|
versionCode = 46
|
||||||
versionName = "1.0.43"
|
versionName = "1.0.44"
|
||||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -2490,13 +2490,44 @@ private fun PhotoTile(
|
|||||||
onDeletePhoto: (RoutePhotoDto) -> Unit,
|
onDeletePhoto: (RoutePhotoDto) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier.aspectRatio(1f)) {
|
val hasPhoto = photo.url.isNotBlank()
|
||||||
|
var imageLoading by remember(photo.url) { mutableStateOf(hasPhoto) }
|
||||||
|
var imageError by remember(photo.url) { mutableStateOf(!hasPhoto) }
|
||||||
|
Box(modifier.aspectRatio(1f).background(TppTheme.colors.panel, RoundedCornerShape(4.dp))) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageRequest(photo.url, imageAuthHeader),
|
model = imageRequest(photo.url, imageAuthHeader),
|
||||||
contentDescription = "Zdjecie ladunku",
|
contentDescription = "Zdjecie ladunku",
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
|
onLoading = {
|
||||||
|
imageLoading = true
|
||||||
|
imageError = false
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
imageLoading = false
|
||||||
|
imageError = false
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
imageLoading = false
|
||||||
|
imageError = true
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxSize().clickable { onPhoto(photo) },
|
modifier = Modifier.fillMaxSize().clickable { onPhoto(photo) },
|
||||||
)
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = shouldShowPhotoTileLoadingState(hasPhoto, imageLoading, imageError),
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
) {
|
||||||
|
PhotoTileLoadingOverlay()
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = shouldShowPhotoTileErrorState(hasPhoto, imageError),
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
) {
|
||||||
|
PhotoTileErrorOverlay()
|
||||||
|
}
|
||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
||||||
@@ -2546,14 +2577,45 @@ private fun PendingPhotoTile(
|
|||||||
onDismiss = { showFailureDetails = false },
|
onDismiss = { showFailureDetails = false },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val hasPhoto = upload.localPath.isNotBlank()
|
||||||
|
var imageLoading by remember(upload.localPath) { mutableStateOf(hasPhoto) }
|
||||||
|
var imageError by remember(upload.localPath) { mutableStateOf(!hasPhoto) }
|
||||||
Box(modifier.aspectRatio(1f).background(TppTheme.colors.panel, RoundedCornerShape(4.dp))) {
|
Box(modifier.aspectRatio(1f).background(TppTheme.colors.panel, RoundedCornerShape(4.dp))) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = File(upload.localPath),
|
model = File(upload.localPath),
|
||||||
contentDescription = "Zdjęcie oczekujące na zapis",
|
contentDescription = "Zdjęcie oczekujące na zapis",
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
|
onLoading = {
|
||||||
|
imageLoading = true
|
||||||
|
imageError = false
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
imageLoading = false
|
||||||
|
imageError = false
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
imageLoading = false
|
||||||
|
imageError = true
|
||||||
|
},
|
||||||
modifier = Modifier.fillMaxSize().clickable { onPhoto(upload) },
|
modifier = Modifier.fillMaxSize().clickable { onPhoto(upload) },
|
||||||
)
|
)
|
||||||
Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.34f)))
|
Box(Modifier.fillMaxSize().background(Color.Black.copy(alpha = 0.34f)))
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = shouldShowPhotoTileLoadingState(hasPhoto, imageLoading, imageError),
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
) {
|
||||||
|
PhotoTileLoadingOverlay()
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = shouldShowPhotoTileErrorState(hasPhoto, imageError),
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
) {
|
||||||
|
PhotoTileErrorOverlay()
|
||||||
|
}
|
||||||
if (isDeleting) {
|
if (isDeleting) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
||||||
@@ -2642,6 +2704,50 @@ private fun PendingPhotoTile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PhotoTileLoadingOverlay() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(54.dp)
|
||||||
|
.background(TppTheme.colors.card.copy(alpha = 0.94f), RoundedCornerShape(8.dp))
|
||||||
|
.border(1.dp, TppTheme.colors.outline.copy(alpha = 0.7f), RoundedCornerShape(8.dp)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
color = TppTheme.colors.forest,
|
||||||
|
trackColor = TppTheme.colors.outline.copy(alpha = 0.32f),
|
||||||
|
strokeWidth = 2.5.dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PhotoTileErrorOverlay() {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(TppTheme.colors.card.copy(alpha = 0.96f), RoundedCornerShape(8.dp))
|
||||||
|
.border(1.dp, TppTheme.colors.error.copy(alpha = 0.36f), RoundedCornerShape(8.dp))
|
||||||
|
.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Błąd",
|
||||||
|
color = TppTheme.colors.error,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"miniatury",
|
||||||
|
color = TppTheme.colors.muted,
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun EmptyPhotoState() {
|
private fun EmptyPhotoState() {
|
||||||
Box(
|
Box(
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ fun shouldShowPhotoPreviewLoadingState(hasPhoto: Boolean, isLoading: Boolean, is
|
|||||||
fun shouldShowPhotoPreviewErrorState(hasPhoto: Boolean, isError: Boolean): Boolean =
|
fun shouldShowPhotoPreviewErrorState(hasPhoto: Boolean, isError: Boolean): Boolean =
|
||||||
hasPhoto && isError
|
hasPhoto && isError
|
||||||
|
|
||||||
|
fun shouldShowPhotoTileLoadingState(hasPhoto: Boolean, isLoading: Boolean, isError: Boolean): Boolean =
|
||||||
|
hasPhoto && isLoading && !isError
|
||||||
|
|
||||||
|
fun shouldShowPhotoTileErrorState(hasPhoto: Boolean, isError: Boolean): Boolean =
|
||||||
|
hasPhoto && isError
|
||||||
|
|
||||||
fun authBrandBannerHeightDp(screenHeightDp: Int): Int =
|
fun authBrandBannerHeightDp(screenHeightDp: Int): Int =
|
||||||
when {
|
when {
|
||||||
screenHeightDp < 640 -> 118
|
screenHeightDp < 640 -> 118
|
||||||
|
|||||||
@@ -73,6 +73,18 @@ class DriverUiRulesTest {
|
|||||||
assertFalse(shouldShowPhotoPreviewErrorState(hasPhoto = false, isError = true))
|
assertFalse(shouldShowPhotoPreviewErrorState(hasPhoto = false, isError = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun keepsPhotoTileFeedbackVisibleUntilThumbnailLoadsOrFails() {
|
||||||
|
assertTrue(shouldShowPhotoTileLoadingState(hasPhoto = true, isLoading = true, isError = false))
|
||||||
|
assertFalse(shouldShowPhotoTileLoadingState(hasPhoto = true, isLoading = false, isError = false))
|
||||||
|
assertFalse(shouldShowPhotoTileLoadingState(hasPhoto = true, isLoading = true, isError = true))
|
||||||
|
assertFalse(shouldShowPhotoTileLoadingState(hasPhoto = false, isLoading = true, isError = false))
|
||||||
|
|
||||||
|
assertTrue(shouldShowPhotoTileErrorState(hasPhoto = true, isError = true))
|
||||||
|
assertFalse(shouldShowPhotoTileErrorState(hasPhoto = true, isError = false))
|
||||||
|
assertFalse(shouldShowPhotoTileErrorState(hasPhoto = false, isError = true))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun usesShorterChromeOnCompactScreens() {
|
fun usesShorterChromeOnCompactScreens() {
|
||||||
assertEquals(118, authBrandBannerHeightDp(600))
|
assertEquals(118, authBrandBannerHeightDp(600))
|
||||||
|
|||||||
Reference in New Issue
Block a user