diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b451c5..712824b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -34,8 +34,8 @@ android { applicationId = "pl.firmatpp.kierowca" minSdk = 26 targetSdk = 35 - versionCode = 44 - versionName = "1.0.42" + versionCode = 45 + versionName = "1.0.43" setProperty("archivesBaseName", "pl.firmatpp.kierowca") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 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 345a715..c764aad 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverApp.kt @@ -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 @@ -2675,22 +2678,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, ) } } diff --git a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt index e5f96e1..8585456 100644 --- a/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt +++ b/app/src/main/java/pl/firmatpp/kierowca/ui/DriverUiRules.kt @@ -59,6 +59,12 @@ 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 authBrandBannerHeightDp(screenHeightDp: Int): Int = when { screenHeightDp < 640 -> 118 diff --git a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt index b42a393..fb1bb0c 100644 --- a/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt +++ b/app/src/test/java/pl/firmatpp/kierowca/ui/DriverUiRulesTest.kt @@ -61,6 +61,18 @@ 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 usesShorterChromeOnCompactScreens() { assertEquals(118, authBrandBannerHeightDp(600))