Compare commits
2
Commits
v1.0.41-43
...
v1.0.43-45
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c0469669 | ||
|
|
3d6e1624e9 |
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId = "pl.firmatpp.kierowca"
|
applicationId = "pl.firmatpp.kierowca"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 43
|
versionCode = 45
|
||||||
versionName = "1.0.41"
|
versionName = "1.0.43"
|
||||||
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
setProperty("archivesBaseName", "pl.firmatpp.kierowca")
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import android.location.LocationManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
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.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.PickVisualMediaRequest
|
import androidx.activity.result.PickVisualMediaRequest
|
||||||
@@ -2675,22 +2678,116 @@ private fun DriverRouteDto.distanceLabel(): String =
|
|||||||
@Composable
|
@Composable
|
||||||
private fun PhotoScreen(state: DriverUiState, onBack: () -> Unit) {
|
private fun PhotoScreen(state: DriverUiState, onBack: () -> Unit) {
|
||||||
val photo = state.selectedPhoto
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.statusBarsPadding(),
|
modifier = Modifier.statusBarsPadding(),
|
||||||
title = { Text("Podglad zdjecia") },
|
title = { Text("Podglad zdjecia") },
|
||||||
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.Outlined.ArrowBack, contentDescription = "Wstecz") } },
|
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 ->
|
) { padding ->
|
||||||
AsyncImage(
|
Box(Modifier.fillMaxSize().padding(padding).background(viewerBackground)) {
|
||||||
model = imageRequest(photo?.url, state.imageAuthHeader),
|
AsyncImage(
|
||||||
contentDescription = "Zdjecie ladunku",
|
model = imageRequest(photo?.url, state.imageAuthHeader),
|
||||||
contentScale = ContentScale.Fit,
|
contentDescription = "Zdjecie ladunku",
|
||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
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,12 @@ private fun polishWeekdayPhrase(day: DayOfWeek): String =
|
|||||||
fun inlinePhotoGridRows(photoCount: Int): Int =
|
fun inlinePhotoGridRows(photoCount: Int): Int =
|
||||||
if (photoCount <= 0) 0 else (photoCount + 1) / 2
|
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 =
|
fun authBrandBannerHeightDp(screenHeightDp: Int): Int =
|
||||||
when {
|
when {
|
||||||
screenHeightDp < 640 -> 118
|
screenHeightDp < 640 -> 118
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ private fun DriverUiState.withApiError(throwable: Throwable): DriverUiState {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun DriverUiState.withLoadedLeaveRequests(
|
||||||
|
requests: List<DriverLeaveRequestDto>,
|
||||||
|
navigateToList: Boolean,
|
||||||
|
): DriverUiState {
|
||||||
|
val refreshedSelectedRequest = selectedLeaveRequest?.let { selected ->
|
||||||
|
requests.firstOrNull { request -> request.id == selected.id } ?: selected
|
||||||
|
}
|
||||||
|
return copy(
|
||||||
|
screen = if (navigateToList) DriverScreen.LeaveRequests else screen,
|
||||||
|
leaveRequests = requests,
|
||||||
|
selectedLeaveRequest = refreshedSelectedRequest,
|
||||||
|
error = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
class DriverViewModel(application: Application) : AndroidViewModel(application) {
|
class DriverViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val appPreferencesStore = AppPreferencesStore(application)
|
private val appPreferencesStore = AppPreferencesStore(application)
|
||||||
private val repository = DriverRepository(application)
|
private val repository = DriverRepository(application)
|
||||||
@@ -346,27 +361,20 @@ class DriverViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
|
|
||||||
fun openLeaveRequests() {
|
fun openLeaveRequests() {
|
||||||
if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return
|
if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return
|
||||||
loadLeaveRequests(showLoading = true)
|
loadLeaveRequests(showLoading = true, navigateToList = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshLeaveRequests() {
|
fun refreshLeaveRequests() {
|
||||||
if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return
|
if (!DriverLeaveRequestUiRules.isFeatureVisible(_state.value.leaveRequestsConfig)) return
|
||||||
loadLeaveRequests(showLoading = false)
|
loadLeaveRequests(showLoading = false, navigateToList = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadLeaveRequests(showLoading: Boolean) {
|
private fun loadLeaveRequests(showLoading: Boolean, navigateToList: Boolean) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update { it.copy(loading = showLoading, refreshing = !showLoading, error = null, feedback = null) }
|
_state.update { it.copy(loading = showLoading, refreshing = !showLoading, error = null, feedback = null) }
|
||||||
runCatching { repository.leaveRequests() }
|
runCatching { repository.leaveRequests() }
|
||||||
.onSuccess { requests ->
|
.onSuccess { requests ->
|
||||||
_state.update {
|
_state.update { it.withLoadedLeaveRequests(requests, navigateToList) }
|
||||||
it.copy(
|
|
||||||
screen = DriverScreen.LeaveRequests,
|
|
||||||
leaveRequests = requests,
|
|
||||||
selectedLeaveRequest = requests.firstOrNull { request -> request.id == it.selectedLeaveRequest?.id } ?: it.selectedLeaveRequest,
|
|
||||||
error = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.onFailure { throwable -> _state.update { it.withApiError(throwable) } }
|
.onFailure { throwable -> _state.update { it.withApiError(throwable) } }
|
||||||
_state.update { it.copy(loading = false, refreshing = false) }
|
_state.update { it.copy(loading = false, refreshing = false) }
|
||||||
|
|||||||
@@ -61,6 +61,18 @@ class DriverUiRulesTest {
|
|||||||
assertEquals(3, inlinePhotoGridRows(5))
|
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
|
@Test
|
||||||
fun usesShorterChromeOnCompactScreens() {
|
fun usesShorterChromeOnCompactScreens() {
|
||||||
assertEquals(118, authBrandBannerHeightDp(600))
|
assertEquals(118, authBrandBannerHeightDp(600))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package pl.firmatpp.kierowca.ui
|
|||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import pl.firmatpp.kierowca.data.model.DriverLeaveRequestDto
|
||||||
import pl.firmatpp.kierowca.ui.theme.AppThemeMode
|
import pl.firmatpp.kierowca.ui.theme.AppThemeMode
|
||||||
|
|
||||||
class DriverUiStateTest {
|
class DriverUiStateTest {
|
||||||
@@ -20,4 +21,42 @@ class DriverUiStateTest {
|
|||||||
|
|
||||||
assertEquals(AppThemeMode.Material3, state.themeMode)
|
assertEquals(AppThemeMode.Material3, state.themeMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun refreshingLeaveRequestsKeepsDetailScreenOpenAndUpdatesSelectedRequest() {
|
||||||
|
val state = DriverUiState(
|
||||||
|
screen = DriverScreen.LeaveRequestDetail,
|
||||||
|
selectedLeaveRequest = leaveRequest(id = "leave-1", status = "pending"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val refreshed = state.withLoadedLeaveRequests(
|
||||||
|
requests = listOf(leaveRequest(id = "leave-1", status = "approved")),
|
||||||
|
navigateToList = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(DriverScreen.LeaveRequestDetail, refreshed.screen)
|
||||||
|
assertEquals("approved", refreshed.selectedLeaveRequest?.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun openingLeaveRequestsNavigatesToList() {
|
||||||
|
val state = DriverUiState(screen = DriverScreen.Routes)
|
||||||
|
|
||||||
|
val refreshed = state.withLoadedLeaveRequests(
|
||||||
|
requests = listOf(leaveRequest(id = "leave-1", status = "pending")),
|
||||||
|
navigateToList = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(DriverScreen.LeaveRequests, refreshed.screen)
|
||||||
|
assertEquals(1, refreshed.leaveRequests.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun leaveRequest(id: String, status: String): DriverLeaveRequestDto =
|
||||||
|
DriverLeaveRequestDto(
|
||||||
|
id = id,
|
||||||
|
dateFrom = "2026-07-10",
|
||||||
|
dateTo = "2026-07-12",
|
||||||
|
type = "URLOP",
|
||||||
|
status = status,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user