Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1834763f19 | ||
|
|
62c0469669 |
@@ -34,8 +34,8 @@ android {
|
||||
applicationId = "pl.firmatpp.kierowca"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 44
|
||||
versionName = "1.0.42"
|
||||
versionCode = 46
|
||||
versionName = "1.0.44"
|
||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import android.location.LocationManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
@@ -2487,13 +2490,44 @@ private fun PhotoTile(
|
||||
onDeletePhoto: (RoutePhotoDto) -> Unit,
|
||||
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(
|
||||
model = imageRequest(photo.url, imageAuthHeader),
|
||||
contentDescription = "Zdjecie ladunku",
|
||||
contentScale = ContentScale.Crop,
|
||||
onLoading = {
|
||||
imageLoading = true
|
||||
imageError = false
|
||||
},
|
||||
onSuccess = {
|
||||
imageLoading = false
|
||||
imageError = false
|
||||
},
|
||||
onError = {
|
||||
imageLoading = false
|
||||
imageError = true
|
||||
},
|
||||
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) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
||||
@@ -2543,14 +2577,45 @@ private fun PendingPhotoTile(
|
||||
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))) {
|
||||
AsyncImage(
|
||||
model = File(upload.localPath),
|
||||
contentDescription = "Zdjęcie oczekujące na zapis",
|
||||
contentScale = ContentScale.Crop,
|
||||
onLoading = {
|
||||
imageLoading = true
|
||||
imageError = false
|
||||
},
|
||||
onSuccess = {
|
||||
imageLoading = false
|
||||
imageError = false
|
||||
},
|
||||
onError = {
|
||||
imageLoading = false
|
||||
imageError = true
|
||||
},
|
||||
modifier = Modifier.fillMaxSize().clickable { onPhoto(upload) },
|
||||
)
|
||||
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) {
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(Color.White.copy(alpha = 0.72f)),
|
||||
@@ -2639,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
|
||||
private fun EmptyPhotoState() {
|
||||
Box(
|
||||
@@ -2675,22 +2784,116 @@ private fun DriverRouteDto.distanceLabel(): String =
|
||||
@Composable
|
||||
private fun PhotoScreen(state: DriverUiState, onBack: () -> Unit) {
|
||||
val photo = state.selectedPhoto
|
||||
val hasPhoto = !photo?.url.isNullOrBlank()
|
||||
var imageLoading by remember(photo?.url) { mutableStateOf(hasPhoto) }
|
||||
var imageError by remember(photo?.url) { mutableStateOf(!hasPhoto) }
|
||||
val viewerBackground = Color(0xFF050805)
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
title = { Text("Podglad zdjecia") },
|
||||
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz") } },
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Black, titleContentColor = Color.White, navigationIconContentColor = Color.White),
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = viewerBackground,
|
||||
titleContentColor = Color.White,
|
||||
navigationIconContentColor = Color.White,
|
||||
),
|
||||
)
|
||||
},
|
||||
containerColor = Color.Black,
|
||||
containerColor = viewerBackground,
|
||||
) { padding ->
|
||||
AsyncImage(
|
||||
model = imageRequest(photo?.url, state.imageAuthHeader),
|
||||
contentDescription = "Zdjecie ladunku",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize().padding(padding),
|
||||
Box(Modifier.fillMaxSize().padding(padding).background(viewerBackground)) {
|
||||
AsyncImage(
|
||||
model = imageRequest(photo?.url, state.imageAuthHeader),
|
||||
contentDescription = "Zdjecie ladunku",
|
||||
contentScale = ContentScale.Fit,
|
||||
onLoading = {
|
||||
imageLoading = true
|
||||
imageError = false
|
||||
},
|
||||
onSuccess = {
|
||||
imageLoading = false
|
||||
imageError = false
|
||||
},
|
||||
onError = {
|
||||
imageLoading = false
|
||||
imageError = true
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = shouldShowPhotoPreviewLoadingState(hasPhoto, imageLoading, imageError),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
) {
|
||||
PhotoPreviewLoadingOverlay()
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = shouldShowPhotoPreviewErrorState(hasPhoto, imageError),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
) {
|
||||
PhotoPreviewErrorOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PhotoPreviewLoadingOverlay() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(Color(0xFF111710).copy(alpha = 0.94f), RoundedCornerShape(8.dp))
|
||||
.border(1.dp, Color(0xFF609830).copy(alpha = 0.42f), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 24.dp, vertical = 22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(36.dp),
|
||||
color = Color(0xFF8EC45D),
|
||||
trackColor = Color.White.copy(alpha = 0.12f),
|
||||
strokeWidth = 3.dp,
|
||||
)
|
||||
Text(
|
||||
"Ładuję zdjęcie",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
"Pobieram plik z serwera",
|
||||
color = Color.White.copy(alpha = 0.72f),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PhotoPreviewErrorOverlay() {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(Color(0xFF171111).copy(alpha = 0.94f), RoundedCornerShape(8.dp))
|
||||
.border(1.dp, TppTheme.colors.error.copy(alpha = 0.5f), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 24.dp, vertical = 22.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
"Nie udało się wczytać zdjęcia",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
"Wróć i spróbuj ponownie",
|
||||
color = Color.White.copy(alpha = 0.72f),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,18 @@ private fun polishWeekdayPhrase(day: DayOfWeek): String =
|
||||
fun inlinePhotoGridRows(photoCount: Int): Int =
|
||||
if (photoCount <= 0) 0 else (photoCount + 1) / 2
|
||||
|
||||
fun shouldShowPhotoPreviewLoadingState(hasPhoto: Boolean, isLoading: Boolean, isError: Boolean): Boolean =
|
||||
hasPhoto && isLoading && !isError
|
||||
|
||||
fun shouldShowPhotoPreviewErrorState(hasPhoto: Boolean, isError: Boolean): Boolean =
|
||||
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 =
|
||||
when {
|
||||
screenHeightDp < 640 -> 118
|
||||
|
||||
@@ -61,6 +61,30 @@ class DriverUiRulesTest {
|
||||
assertEquals(3, inlinePhotoGridRows(5))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepsPhotoPreviewFeedbackVisibleUntilImageLoadsOrFails() {
|
||||
assertTrue(shouldShowPhotoPreviewLoadingState(hasPhoto = true, isLoading = true, isError = false))
|
||||
assertFalse(shouldShowPhotoPreviewLoadingState(hasPhoto = true, isLoading = false, isError = false))
|
||||
assertFalse(shouldShowPhotoPreviewLoadingState(hasPhoto = true, isLoading = true, isError = true))
|
||||
assertFalse(shouldShowPhotoPreviewLoadingState(hasPhoto = false, isLoading = true, isError = false))
|
||||
|
||||
assertTrue(shouldShowPhotoPreviewErrorState(hasPhoto = true, isError = true))
|
||||
assertFalse(shouldShowPhotoPreviewErrorState(hasPhoto = true, isError = false))
|
||||
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
|
||||
fun usesShorterChromeOnCompactScreens() {
|
||||
assertEquals(118, authBrandBannerHeightDp(600))
|
||||
|
||||
Reference in New Issue
Block a user