Commit 33b43604 by Aleksandr

Pass events of render / upload to consumers

parent 83f125fd
......@@ -40,6 +40,8 @@ import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.c23.ui.screen.details.component.DetailsV1
import com.isidroid.c23.ui.screen.details.component.DetailsV2
import com.isidroid.c23.ui.screen.details.component.PrintCodeComponent
import com.isidroid.c23.ui.screen.map.MapContract
import com.isidroid.c23.ui.screen.map._components.TPMapComponent
......@@ -48,7 +50,6 @@ import com.isidroid.ui.maps.model.MapMarker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JobDetailsScreen(
state: State<JobDetailsContract.State>,
......@@ -58,153 +59,7 @@ fun JobDetailsScreen(
onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is JobDetailsContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
BackHandler { onEventSent(JobDetailsContract.Event.ToBack) }
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.print_job_details),
colors = TopAppBarDefaults.topAppBarColors(),
onNavigationClick = { onEventSent(JobDetailsContract.Event.ToBack) }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.consumeWindowInsets(paddingValues)
.padding(top = paddingValues.calculateTopPadding())
.fillMaxSize(),
contentAlignment = Alignment.TopStart
) {
when {
state.value.isLoading -> LoadingComponent()
}
InformationContent(state, spotResultStateFlow, onEventSent)
DisplayMapRouteConfirmation(state, onEventSent)
}
}
}
@Composable
private fun LoadingComponent(modifier: Modifier = Modifier) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.startapp))
LottieAnimation(
composition = composition,
iterations = LottieConstants.IterateForever,
modifier = modifier
.fillMaxSize()
.padding(48.dp)
)
DetailsV1(state, effectFlow, spotResultStateFlow, onEventSent, onNavigationRequested, modifier)
// DetailsV2(state, effectFlow, spotResultStateFlow, onEventSent, onNavigationRequested, modifier)
}
@Composable
private fun InformationContent(
state: State<JobDetailsContract.State>,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit,
modifier: Modifier = Modifier
) {
val information = state.value.printJob ?: return
ConstraintLayout(modifier = modifier.fillMaxSize()) {
val (codeView, statusView, mapView) = createRefs()
PrintCodeComponent(
code = information.accessCode,
modifier = Modifier
.constrainAs(codeView) {
top.linkTo(parent.top)
linkTo(start = parent.start, end = parent.end)
}
)
Text(
text = information.statusName,
fontSize = 14.sp,
fontWeight = FontWeight.W200,
letterSpacing = .76.sp,
color = Color.White,
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(information.statusColor)
.padding(horizontal = 6.dp, vertical = 2.dp)
.constrainAs(statusView) {
top.linkTo(codeView.bottom, 12.dp)
end.linkTo(parent.end, 12.dp)
}
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.constrainAs(mapView) {
linkTo(top = statusView.bottom, bottom = parent.bottom)
height = Dimension.fillToConstraints
},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
TPMapComponent(
modifier = Modifier,
onEventSent = { event ->
when (event) {
is MapContract.Event.ClickOnMarker -> onEventSent(JobDetailsContract.Event.OpenConfirmationMapRoute)
else -> {}
}
},
mapMarkersStateFlow = spotResultStateFlow,
lat = state.value.lat,
lng = state.value.lng
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DisplayMapRouteConfirmation(
state: State<JobDetailsContract.State>,
onEventSent: (event: JobDetailsContract.Event) -> Unit
) {
if (!state.value.routeConfirmationVisible) return
val sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
onDismissRequest = { onEventSent(JobDetailsContract.Event.DismissBuildRouteConfirmation) },
sheetState = sheetState,
windowInsets = WindowInsets.statusBars,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 120.dp),
) {
Text(
text = stringResource(id = R.string.app_navigation_explanation),
modifier = Modifier.padding(horizontal = 16.dp),
textAlign = TextAlign.Center
)
TextButton(
onClick = { onEventSent(JobDetailsContract.Event.OpenNavigationApp) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 18.dp)
) {
Text(
text = "Open in Google Maps",
)
}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.details.component
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.LottieConstants
import com.airbnb.lottie.compose.rememberLottieComposition
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.c23.ui.screen.details.JobDetailsContract
import com.isidroid.c23.ui.screen.map.MapContract
import com.isidroid.c23.ui.screen.map._components.TPMapComponent
import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.ui.maps.model.MapMarker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailsV1(
state: State<JobDetailsContract.State>,
effectFlow: Flow<JobDetailsContract.Effect>?,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier,
){
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is JobDetailsContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
BackHandler { onEventSent(JobDetailsContract.Event.ToBack) }
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.print_job_details),
colors = TopAppBarDefaults.topAppBarColors(),
onNavigationClick = { onEventSent(JobDetailsContract.Event.ToBack) }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.consumeWindowInsets(paddingValues)
.padding(top = paddingValues.calculateTopPadding())
.fillMaxSize(),
contentAlignment = Alignment.TopStart
) {
when {
state.value.isLoading -> LoadingComponent()
}
InformationContent(state, spotResultStateFlow, onEventSent)
DisplayMapRouteConfirmation(state, onEventSent)
}
}
}
@Composable
private fun LoadingComponent(modifier: Modifier = Modifier) {
val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.startapp))
LottieAnimation(
composition = composition,
iterations = LottieConstants.IterateForever,
modifier = modifier
.fillMaxSize()
.padding(48.dp)
)
}
@Composable
private fun InformationContent(
state: State<JobDetailsContract.State>,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit,
modifier: Modifier = Modifier
) {
val information = state.value.printJob ?: return
ConstraintLayout(modifier = modifier.fillMaxSize()) {
val (codeView, statusView, mapView) = createRefs()
PrintCodeComponent(
code = information.accessCode,
modifier = Modifier
.constrainAs(codeView) {
top.linkTo(parent.top)
linkTo(start = parent.start, end = parent.end)
}
)
Text(
text = information.statusName,
fontSize = 14.sp,
fontWeight = FontWeight.W200,
letterSpacing = .76.sp,
color = Color.White,
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(information.statusColor)
.padding(horizontal = 6.dp, vertical = 2.dp)
.constrainAs(statusView) {
top.linkTo(codeView.bottom, 12.dp)
end.linkTo(parent.end, 12.dp)
}
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp)
.constrainAs(mapView) {
linkTo(top = statusView.bottom, bottom = parent.bottom)
height = Dimension.fillToConstraints
},
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp, bottomStart = 0.dp, bottomEnd = 0.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 12.dp)
) {
TPMapComponent(
modifier = Modifier,
onEventSent = { event ->
when (event) {
is MapContract.Event.ClickOnMarker -> onEventSent(JobDetailsContract.Event.OpenConfirmationMapRoute)
else -> {}
}
},
mapMarkersStateFlow = spotResultStateFlow,
lat = state.value.lat,
lng = state.value.lng
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DisplayMapRouteConfirmation(
state: State<JobDetailsContract.State>,
onEventSent: (event: JobDetailsContract.Event) -> Unit
) {
if (!state.value.routeConfirmationVisible) return
val sheetState = rememberModalBottomSheetState()
ModalBottomSheet(
onDismissRequest = { onEventSent(JobDetailsContract.Event.DismissBuildRouteConfirmation) },
sheetState = sheetState,
windowInsets = WindowInsets.statusBars,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 120.dp),
) {
Text(
text = stringResource(id = R.string.app_navigation_explanation),
modifier = Modifier.padding(horizontal = 16.dp),
textAlign = TextAlign.Center
)
TextButton(
onClick = { onEventSent(JobDetailsContract.Event.OpenNavigationApp) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 18.dp)
) {
Text(
text = "Open in Google Maps",
)
}
}
}
\ No newline at end of file
package com.isidroid.c23.ui.screen.details.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import com.isidroid.c23.R
import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.c23.ui.screen.details.JobDetailsContract
import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.utils.asCost
import com.isidroid.utils.spaceInCenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import java.text.DateFormat
import java.text.SimpleDateFormat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailsV2(
state: State<JobDetailsContract.State>,
effectFlow: Flow<JobDetailsContract.Effect>?,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier,
) {
val printJob = state.value.printJob ?: return
Scaffold(modifier = modifier.fillMaxSize(), topBar = {
TopAppBarComponent(
text = stringResource(id = R.string.print_job_details),
colors = TopAppBarDefaults.topAppBarColors(),
onNavigationClick = { onEventSent(JobDetailsContract.Event.ToBack) }
)
}) { paddingValues ->
Column(
modifier = Modifier
.fillMaxWidth()
.consumeWindowInsets(paddingValues)
.padding(top = paddingValues.calculateTopPadding())
.padding(horizontal = 16.dp)
) {
val textStrings = arrayOf(
"PIN code: ${printJob.accessCode.spaceInCenter()}",
"${printJob.cost.asCost()} | Unpaid",
DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT).format(printJob.createdAt)
)
for (text in textStrings) {
Text(text = text)
Spacer(modifier = Modifier.height(8.dp))
}
Text(
text = printJob.statusName,
fontWeight = FontWeight.W200,
letterSpacing = .76.sp,
color = Color.White,
modifier = Modifier
.clip(RoundedCornerShape(6.dp))
.background(printJob.statusColor)
.padding(horizontal = 6.dp, vertical = 2.dp)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
.background(Color.Black)
)
Row(modifier = Modifier.fillMaxWidth()) {
}
}
}
}
\ No newline at end of file
......@@ -42,4 +42,6 @@
<string name="app_navigation_explanation">To navigate to this location, please use an external navigation app. Tap the button below to open your preferred navigation app and plan your route.</string>
<string name="print_job_status_rendered">Render complete</string>
<string name="print_job_status_sending">Sending</string>
<string name="pin_code">Pin code</string>
<string name="price">Price</string>
</resources>
\ No newline at end of file
......@@ -61,3 +61,15 @@ fun Float.asCost(lang: String? = null, country: String? = null): String {
val currencyFormat = NumberFormat.getCurrencyInstance(locale)
return currencyFormat.format(this)
}
fun String.spaceInCenter(): String {
val mid = length / 2
val left = substring(0, mid)
val right = substring(mid, length)
return buildString {
append(left)
append(" ")
append(right)
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.isidroid.job.constant.JobStatus
import com.isidroid.job.constant.SendJobStatus
import com.isidroid.job.constant.sendJobStatusName
import com.isidroid.job.data.source.local.JobLocalSource
import com.isidroid.job.data.source.remote.JobNetworkSource
import com.isidroid.job.data.source.remote.api.ApiJob
......@@ -97,8 +98,9 @@ class UploadJobsTest {
delay(delay)
deferred.cancel()
val actualEvent1Progress = (events[0] as? JobSenderResult.UploadProgress)?.position
val actualEvent2Progress = (events[1] as? JobSenderResult.UploadProgress)?.position
val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }
Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
......@@ -128,12 +130,13 @@ class UploadJobsTest {
deferred.cancel()
val actualEventTotalProgressCount = events.filterIsInstance<JobSenderResult.UploadProgress>().distinctBy { it.total }.let { if (it.size == 1) it[0].total else -1 }
val actualEvent1Progress = (events[0] as? JobSenderResult.UploadProgress)?.position
val actualEvent2Progress = (events[1] as? JobSenderResult.UploadProgress)?.position
val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }
Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
Assert.assertEquals("invalid progress for last progress event", 1, actualEvent2Progress)
Assert.assertEquals("invalid progress for last progress event ${actualEvent1Progress?.sendJobStatusName}", 1, actualEvent2Progress)
Assert.assertEquals("incorrect count of rendered print jobs ready to be sent", 1, actualCountUploadStatus)
Assert.assertEquals("incorrect count of total jobs", 2, actualEventTotalProgressCount)
}
......@@ -159,8 +162,9 @@ class UploadJobsTest {
delay(delay)
deferred.cancel()
val actualEvent1Progress = (events[0] as? JobSenderResult.UploadProgress)?.position
val actualEvent2Progress = (events[2] as? JobSenderResult.UploadProgress)?.position
val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }
val actualEventJobErrorsCount = events.filterIsInstance<JobSenderResult.Error>().count()
......@@ -197,8 +201,9 @@ class UploadJobsTest {
deferred.cancel()
val actualEventTotalProgressCount = events.filterIsInstance<JobSenderResult.UploadProgress>().distinctBy { it.total }.let { if (it.size == 1) it[0].total else -1 }
val actualEvent1Progress = (events[0] as? JobSenderResult.UploadProgress)?.position
val actualEvent2Progress = (events[1] as? JobSenderResult.UploadProgress)?.position
val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }
Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
......@@ -241,8 +246,9 @@ class UploadJobsTest {
deferred.cancel()
val actualEventTotalProgressCount = events.filterIsInstance<JobSenderResult.UploadProgress>().distinctBy { it.total }.let { if (it.size == 1) it[0].total else -1 }
val actualEvent1Progress = (events[0] as? JobSenderResult.UploadProgress)?.position
val actualEvent2Progress = (events[1] as? JobSenderResult.UploadProgress)?.position
val uploadProgressEvents = events.filterIsInstance<JobSenderResult.UploadProgress>()
val actualEvent1Progress = uploadProgressEvents.getOrNull(0)?.position
val actualEvent2Progress = uploadProgressEvents.getOrNull(1)?.position
val actualCountUploadStatus = events.filterIsInstance<JobSenderResult.Statuses>().count { it.status == JobStatus.RENDER_UPLOAD }
Assert.assertEquals("invalid progress for first event", 0, actualEvent1Progress)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment