Commit 4bf53d84 by Aleksandr

Template for Print job details

parent 33b43604
package com.isidroid.c23 package com.isidroid.c23
class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m) class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m)
class JobNotFoundException(m: String? = null): Throwable(m) class JobNotFoundException(m: String? = null): Throwable(m)
\ No newline at end of file class NoLocationPermissionException(m: String? = null): Throwable(m)
\ No newline at end of file
...@@ -3,7 +3,15 @@ package com.isidroid.c23.constant ...@@ -3,7 +3,15 @@ package com.isidroid.c23.constant
import androidx.annotation.StringDef import androidx.annotation.StringDef
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@StringDef(Argument.URI, Argument.LATITUDE, Argument.LONGITUDE, Argument.SPOT_CODE, Argument.ID, Argument.INFO, Argument.MARKER) @StringDef(
Argument.URI,
Argument.LATITUDE,
Argument.LONGITUDE,
Argument.SPOT_CODE,
Argument.ID,
Argument.INFO,
Argument.MARKER,
)
annotation class Argument { annotation class Argument {
companion object { companion object {
const val URI = "URI" const val URI = "URI"
...@@ -13,5 +21,6 @@ annotation class Argument { ...@@ -13,5 +21,6 @@ annotation class Argument {
const val ID = "ID" const val ID = "ID"
const val INFO = "INFO" const val INFO = "INFO"
const val MARKER = "MARKER" const val MARKER = "MARKER"
const val DISTANCE = "DISTANCE"
} }
} }
...@@ -15,6 +15,7 @@ fun PrintJob.createListItem(context: Context, richSpot: RichSpot?): PrintJobList ...@@ -15,6 +15,7 @@ fun PrintJob.createListItem(context: Context, richSpot: RichSpot?): PrintJobList
id = id, id = id,
spotCode = richSpot?.spot?.code, spotCode = richSpot?.spot?.code,
spotName = richSpot?.spot?.name ?: "Deleted spot", spotName = richSpot?.spot?.name ?: "Deleted spot",
spotAddress = richSpot?.spot?.address ?: "Deleted spot",
cost = cost, cost = cost,
paperInfo = printSize.printSizeName, paperInfo = printSize.printSizeName,
isColor = profile?.grayscale != true, isColor = profile?.grayscale != true,
...@@ -25,5 +26,7 @@ fun PrintJob.createListItem(context: Context, richSpot: RichSpot?): PrintJobList ...@@ -25,5 +26,7 @@ fun PrintJob.createListItem(context: Context, richSpot: RichSpot?): PrintJobList
statusName = context.getString(getPrintJobStatus(status)), statusName = context.getString(getPrintJobStatus(status)),
accessCode = accessCode.orEmpty(), accessCode = accessCode.orEmpty(),
createdAt = createdAt, createdAt = createdAt,
profileName = profile?.name,
profileCost = profile?.cost
) )
} }
\ No newline at end of file
...@@ -7,6 +7,7 @@ data class PrintJobListItem( ...@@ -7,6 +7,7 @@ data class PrintJobListItem(
val id: String, val id: String,
val spotName: String, val spotName: String,
val spotCode: String?, val spotCode: String?,
val spotAddress: String?,
val cost: Float, val cost: Float,
val paperInfo: String, val paperInfo: String,
val isColor: Boolean, val isColor: Boolean,
...@@ -16,5 +17,7 @@ data class PrintJobListItem( ...@@ -16,5 +17,7 @@ data class PrintJobListItem(
val statusColor: Color, val statusColor: Color,
val cover: String?, val cover: String?,
val accessCode: String, val accessCode: String,
val createdAt: Date val createdAt: Date,
val profileName: String?,
val profileCost: Float?
) )
\ No newline at end of file
package com.isidroid.c23.domain.use_case package com.isidroid.c23.domain.use_case
import android.Manifest
import android.content.Context import android.content.Context
import androidx.compose.ui.text.intl.Locale
import com.isidroid.c23.JobNotFoundException import com.isidroid.c23.JobNotFoundException
import com.isidroid.c23.NoLocationPermissionException
import com.isidroid.c23.constant.Argument import com.isidroid.c23.constant.Argument
import com.isidroid.c23.data.mapper.createListItem import com.isidroid.c23.data.mapper.createListItem
import com.isidroid.c23.ext.isDebug import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult import com.isidroid.core.FlowResult
import com.isidroid.job.repository.JobRepository import com.isidroid.job.repository.JobRepository
import com.isidroid.location.ext.isMileUnit
import com.isidroid.location.repository.LocationRepository import com.isidroid.location.repository.LocationRepository
import com.isidroid.spot.repository.SpotRepository import com.isidroid.spot.repository.SpotRepository
import com.isidroid.ui.maps.ext.addKilometerToLatLng import com.isidroid.ui.maps.ext.addKilometerToLatLng
import com.isidroid.ui.maps.model.MapMarker import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.utils.hasPermission
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.round
@Singleton @Singleton
class DetailsUseCase @Inject constructor( class DetailsUseCase @Inject constructor(
...@@ -26,11 +33,30 @@ class DetailsUseCase @Inject constructor( ...@@ -26,11 +33,30 @@ class DetailsUseCase @Inject constructor(
fun loadDetails(id: String) = flow { fun loadDetails(id: String) = flow {
emit(FlowResult.Loading) emit(FlowResult.Loading)
val hasLocationPermission = context.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
if (!hasLocationPermission) {
throw NoLocationPermissionException()
}
val job = jobRepository.readLocalList(ids = listOf(id)).firstOrNull() ?: throw JobNotFoundException() val job = jobRepository.readLocalList(ids = listOf(id)).firstOrNull() ?: throw JobNotFoundException()
val richSpot = spotRepository.findLocalRichSpots(ids = listOf(job.spotId))?.firstOrNull() val richSpot = spotRepository.findLocalRichSpots(ids = listOf(job.spotId))?.firstOrNull()
val result = job.createListItem(context = context, richSpot = richSpot) val result = job.createListItem(context = context, richSpot = richSpot)
val currentLocation = locationRepository.getCurrentLocation() val currentLocation = locationRepository.getCurrentLocation()
val distanceKm = locationRepository.findDistance(
currentLocation.first,
currentLocation.second,
richSpot?.spot?.lat,
richSpot?.spot?.lng
)?.let { (round(it * 100) / 100) }
val distance = buildString {
distanceKm?.let { append("$distanceKm ") }
if (Locale.current.isMileUnit)
append("miles")
else
append("km")
}
val location = if (isDebug() && richSpot?.spot != null) val location = if (isDebug() && richSpot?.spot != null)
addKilometerToLatLng(richSpot.spot.lat, richSpot.spot.lng, distance = 0.2) addKilometerToLatLng(richSpot.spot.lat, richSpot.spot.lng, distance = 0.2)
...@@ -47,7 +73,8 @@ class DetailsUseCase @Inject constructor( ...@@ -47,7 +73,8 @@ class DetailsUseCase @Inject constructor(
Argument.INFO to result, Argument.INFO to result,
Argument.LATITUDE to location.first, Argument.LATITUDE to location.first,
Argument.LONGITUDE to location.second, Argument.LONGITUDE to location.second,
Argument.MARKER to mapMarker Argument.MARKER to mapMarker,
Argument.DISTANCE to distance
) )
) )
) )
......
...@@ -11,6 +11,7 @@ class JobDetailsContract { ...@@ -11,6 +11,7 @@ class JobDetailsContract {
data object OpenConfirmationMapRoute: Event data object OpenConfirmationMapRoute: Event
data object DismissBuildRouteConfirmation: Event data object DismissBuildRouteConfirmation: Event
data object OpenNavigationApp: Event data object OpenNavigationApp: Event
data object LocationPermissionGranted: Event
} }
sealed interface Effect : ViewSideEffect { sealed interface Effect : ViewSideEffect {
...@@ -25,6 +26,8 @@ class JobDetailsContract { ...@@ -25,6 +26,8 @@ class JobDetailsContract {
val printJob: PrintJobListItem? = null, val printJob: PrintJobListItem? = null,
val lat: Double? = null, val lat: Double? = null,
val lng: Double? = null, val lng: Double? = null,
val routeConfirmationVisible: Boolean = false val routeConfirmationVisible: Boolean = false,
val requestLocationPermission: Boolean = false,
val distance: String? = null
) : ViewState ) : ViewState
} }
\ No newline at end of file
package com.isidroid.c23.ui.screen.details package com.isidroid.c23.ui.screen.details
import androidx.activity.compose.BackHandler 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.component.DetailsV1
import com.isidroid.c23.ui.screen.details.component.DetailsV2 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._components.RequestLocationPermissionComponent
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.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.ui.maps.model.MapMarker import com.isidroid.ui.maps.model.MapMarker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
...@@ -59,7 +21,21 @@ fun JobDetailsScreen( ...@@ -59,7 +21,21 @@ fun JobDetailsScreen(
onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit, onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
DetailsV1(state, effectFlow, spotResultStateFlow, onEventSent, onNavigationRequested, modifier) RequestLocationPermissionComponent(
// DetailsV2(state, effectFlow, spotResultStateFlow, onEventSent, onNavigationRequested, modifier) requestLocationPermission = state.value.requestLocationPermission,
} onGranted = { onEventSent(JobDetailsContract.Event.LocationPermissionGranted) }
)
LaunchedEffect(SIDE_EFFECTS_KEY) {
effectFlow?.collect { effect ->
when (effect) {
is JobDetailsContract.Effect.Navigation -> onNavigationRequested(effect)
}
}
}
BackHandler { onEventSent(JobDetailsContract.Event.ToBack) }
// DetailsV1(state, effectFlow, spotResultStateFlow, onEventSent, onNavigationRequested, modifier)
DetailsV2(state, spotResultStateFlow, onEventSent, modifier)
}
\ No newline at end of file
...@@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons ...@@ -4,6 +4,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.AccountCircle
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isidroid.c23.NoLocationPermissionException
import com.isidroid.c23.constant.Argument import com.isidroid.c23.constant.Argument
import com.isidroid.c23.domain.dto.PrintJobListItem import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.c23.domain.use_case.DetailsUseCase import com.isidroid.c23.domain.use_case.DetailsUseCase
...@@ -17,9 +18,9 @@ import kotlinx.coroutines.Dispatchers ...@@ -17,9 +18,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
...@@ -31,12 +32,17 @@ class JobDetailsViewModel @Inject constructor( ...@@ -31,12 +32,17 @@ class JobDetailsViewModel @Inject constructor(
private val _spotResultStateFlow = MutableStateFlow<List<MapMarker>>(emptyList()) private val _spotResultStateFlow = MutableStateFlow<List<MapMarker>>(emptyList())
val spotResultStateFlow = _spotResultStateFlow.asStateFlow() val spotResultStateFlow = _spotResultStateFlow.asStateFlow()
private lateinit var jobId: String
init { init {
viewModelScope.launch { viewModelScope.launch {
savedStateHandle.getStateFlow<String?>(Argument.ID, null) savedStateHandle.getStateFlow<String?>(Argument.ID, null)
.filterNotNull() .filterNotNull()
.onEach { loadDetails(it) } .onEach {
.firstOrNull() jobId = it
loadDetails()
}
.singleOrNull()
} }
} }
...@@ -49,14 +55,19 @@ class JobDetailsViewModel @Inject constructor( ...@@ -49,14 +55,19 @@ class JobDetailsViewModel @Inject constructor(
JobDetailsContract.Event.OpenConfirmationMapRoute -> setState { copy(routeConfirmationVisible = true) } JobDetailsContract.Event.OpenConfirmationMapRoute -> setState { copy(routeConfirmationVisible = true) }
JobDetailsContract.Event.DismissBuildRouteConfirmation -> setState { copy(routeConfirmationVisible = false) } JobDetailsContract.Event.DismissBuildRouteConfirmation -> setState { copy(routeConfirmationVisible = false) }
JobDetailsContract.Event.OpenNavigationApp -> openNavigationApp() JobDetailsContract.Event.OpenNavigationApp -> openNavigationApp()
JobDetailsContract.Event.LocationPermissionGranted -> locationPermissionGranted()
} }
} }
// handle events // handle events
private suspend fun loadDetails(id: String) { private suspend fun loadDetails() {
useCase.loadDetails(id) useCase.loadDetails(jobId)
.flowOn(Dispatchers.IO) .flowOn(Dispatchers.IO)
.catchTimber { } .catchTimber {
when (it) {
is NoLocationPermissionException -> setState { copy(requestLocationPermission = true) }
}
}
.collect { res -> .collect { res ->
when (res) { when (res) {
FlowResult.Loading -> setState { copy(isLoading = true) } FlowResult.Loading -> setState { copy(isLoading = true) }
...@@ -64,21 +75,27 @@ class JobDetailsViewModel @Inject constructor( ...@@ -64,21 +75,27 @@ class JobDetailsViewModel @Inject constructor(
item = res.result[Argument.INFO] as? PrintJobListItem, item = res.result[Argument.INFO] as? PrintJobListItem,
lat = res.result[Argument.LATITUDE] as? Double, lat = res.result[Argument.LATITUDE] as? Double,
lng = res.result[Argument.LONGITUDE] as? Double, lng = res.result[Argument.LONGITUDE] as? Double,
mapMarker = res.result[Argument.MARKER] as? MapMarker mapMarker = res.result[Argument.MARKER] as? MapMarker,
distance = res.result[Argument.DISTANCE] as? String
) )
} }
} }
} }
private suspend fun onDetails(item: PrintJobListItem?, lat: Double?, lng: Double?, mapMarker: MapMarker?) { private suspend fun onDetails(item: PrintJobListItem?, lat: Double?, lng: Double?, mapMarker: MapMarker?, distance: String?) {
val myLocation = MapMarker(id = "my_location", lat = lat ?: 0.0, lng = lng ?: 0.0, name = "My location", imageVector = Icons.Outlined.AccountCircle) val myLocation = MapMarker(id = "my_location", lat = lat ?: 0.0, lng = lng ?: 0.0, name = "My location", imageVector = Icons.Outlined.AccountCircle)
_spotResultStateFlow.emit(listOfNotNull(mapMarker, myLocation)) _spotResultStateFlow.emit(listOfNotNull(mapMarker, myLocation))
setState { copy(isLoading = false, printJob = item, lat = lat, lng = lng) } setState { copy(isLoading = false, printJob = item, lat = lat, lng = lng, distance = distance) }
} }
private fun openNavigationApp(){ private fun openNavigationApp() {
setState { copy(routeConfirmationVisible = false) } setState { copy(routeConfirmationVisible = false) }
setEffect { JobDetailsContract.Effect.Navigation.ToNavigationApp(viewState.value.lat, viewState.value.lng) } setEffect { JobDetailsContract.Effect.Navigation.ToNavigationApp(viewState.value.lat, viewState.value.lng) }
} }
private suspend fun locationPermissionGranted() {
setState { copy(requestLocationPermission = false) }
loadDetails()
}
} }
\ No newline at end of file
package com.isidroid.c23.ui.screen.details.component
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.isidroid.c23.R
import com.isidroid.c23.ui.screen.details.JobDetailsContract
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal 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
...@@ -171,40 +171,4 @@ private fun InformationContent( ...@@ -171,40 +171,4 @@ private fun InformationContent(
) )
} }
} }
}
@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 package com.isidroid.c23.ui.screen.details.component
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
...@@ -26,30 +25,27 @@ import androidx.compose.ui.draw.clip ...@@ -26,30 +25,27 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight 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.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp 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.R
import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.c23.ui._component.TopAppBarComponent import com.isidroid.c23.ui._component.TopAppBarComponent
import com.isidroid.c23.ui.screen.details.JobDetailsContract 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.ui.maps.model.MapMarker import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.utils.asCost import com.isidroid.utils.asCost
import com.isidroid.utils.spaceInCenter import com.isidroid.utils.spaceInCenter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun DetailsV2( fun DetailsV2(
state: State<JobDetailsContract.State>, state: State<JobDetailsContract.State>,
effectFlow: Flow<JobDetailsContract.Effect>?,
spotResultStateFlow: StateFlow<List<MapMarker>>, spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit, onEventSent: (event: JobDetailsContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val printJob = state.value.printJob ?: return val printJob = state.value.printJob ?: return
...@@ -69,39 +65,101 @@ fun DetailsV2( ...@@ -69,39 +65,101 @@ fun DetailsV2(
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding())
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
val textStrings = arrayOf(
"PIN code: ${printJob.accessCode.spaceInCenter()}", PrintInfoComponent(printJob)
"${printJob.cost.asCost()} | Unpaid", HorizontalDivider(Modifier.padding(vertical = 16.dp))
DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT).format(printJob.createdAt) SpotInfoComponent(
lat = state.value.lat,
lng = state.value.lng,
distance = state.value.distance,
printJob = printJob,
spotResultStateFlow = spotResultStateFlow,
onEventSent = onEventSent,
paddingBottom = paddingValues.calculateBottomPadding()
) )
}
for (text in textStrings) { DisplayMapRouteConfirmation(state, onEventSent)
Text(text = text) }
Spacer(modifier = Modifier.height(8.dp)) }
}
Text( @Composable
text = printJob.statusName, private fun PrintInfoComponent(printJob: PrintJobListItem) {
fontWeight = FontWeight.W200, val textStrings = listOfNotNull(
letterSpacing = .76.sp, "PIN code: ${printJob.accessCode.spaceInCenter()}",
color = Color.White, "${printJob.cost.asCost()} | Unpaid",
modifier = Modifier "${printJob.profileCost?.asCost()} per page",
.clip(RoundedCornerShape(6.dp)) DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT).format(printJob.createdAt),
.background(printJob.statusColor) "copies: ${printJob.copies}",
.padding(horizontal = 6.dp, vertical = 2.dp) printJob.profileName,
) )
Box( for (text in textStrings) {
modifier = Modifier Text(text = text)
.fillMaxWidth() Spacer(modifier = Modifier.height(8.dp))
.padding(horizontal = 16.dp, vertical = 8.dp) }
.background(Color.Black)
)
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)
)
}
Row(modifier = Modifier.fillMaxWidth()) { @Composable
private fun ColumnScope.SpotInfoComponent(
lat: Double?,
lng: Double?,
distance: String?,
printJob: PrintJobListItem,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit,
paddingBottom: Dp
) {
val textStrings = listOfNotNull(
"Spot: ${printJob.spotName}",
"Address: ${printJob.spotAddress}",
distance?.let { "Distance: $distance" }
)
} for (text in textStrings) {
} Text(text = text)
Spacer(modifier = Modifier.height(8.dp))
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 24.dp)
.weight(1f),
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 = lat,
lng = lng
)
}
Button(
onClick = { onEventSent(JobDetailsContract.Event.OpenConfirmationMapRoute) },
modifier = Modifier
.padding(bottom = paddingBottom, top = 16.dp)
.fillMaxWidth()
) {
Text(text = "Build a route to Print spot")
} }
} }
\ No newline at end of file
...@@ -53,7 +53,7 @@ fun MapScreen( ...@@ -53,7 +53,7 @@ fun MapScreen(
} }
RequestLocationPermissionComponent( RequestLocationPermissionComponent(
state = state, requestLocationPermission = state.value.requestLocationPermission,
onGranted = { onEventSent(MapContract.Event.PermissionGranted) } onGranted = { onEventSent(MapContract.Event.PermissionGranted) }
) )
......
...@@ -19,8 +19,8 @@ import com.isidroid.c23.ui.screen.map.MapContract ...@@ -19,8 +19,8 @@ import com.isidroid.c23.ui.screen.map.MapContract
import rememberPermissionState import rememberPermissionState
@Composable @Composable
internal fun RequestLocationPermissionComponent(state: State<MapContract.State>, onGranted: () -> Unit) { internal fun RequestLocationPermissionComponent(requestLocationPermission: Boolean, onGranted: () -> Unit) {
if (!state.value.requestLocationPermission) if (!requestLocationPermission)
return return
val context = LocalContext.current val context = LocalContext.current
......
...@@ -12,11 +12,16 @@ import kotlinx.coroutines.delay ...@@ -12,11 +12,16 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.single import kotlinx.coroutines.flow.single
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
...@@ -34,6 +39,25 @@ import kotlin.coroutines.cancellation.CancellationException ...@@ -34,6 +39,25 @@ import kotlin.coroutines.cancellation.CancellationException
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun stateFlow() { fun stateFlow() {
val coroutineScope = CoroutineScope(Dispatchers.Default)
val job = coroutineScope.launch {
val flow = flow {
repeat(10) {
delay(1000)
emit(it)
}
}
val stateFlow = flow.stateIn(this, started = SharingStarted.Lazily, -1)
stateFlow.onEach { println("result: $it") }.singleOrNull()
}
runBlocking {
delay(5_000)
job.cancel()
}
} }
} }
\ No newline at end of file
package com.isidroid.location.repository package com.isidroid.location.repository
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import androidx.compose.ui.text.intl.Locale
import com.google.android.gms.location.LocationServices import com.google.android.gms.location.LocationServices
import com.google.android.gms.tasks.Tasks import com.google.android.gms.tasks.Tasks
import com.isidroid.location.repository.LocationRepository import com.isidroid.location.ext.calculateDistanceBetweenPoints
import timber.log.Timber
class LocationRepositoryImpl(private val context: Context) : LocationRepository { class LocationRepositoryImpl(private val context: Context) : LocationRepository {
@SuppressLint("MissingPermission")
override fun getCurrentLocation(): Pair<Double, Double> { override fun getCurrentLocation(): Pair<Double, Double> {
val client = LocationServices.getFusedLocationProviderClient(context) val client = LocationServices.getFusedLocationProviderClient(context)
return with(Tasks.await(client.lastLocation)) { return with(Tasks.await(client.lastLocation)) { Pair(latitude, longitude) }
Pair(latitude, longitude) }
}
override fun findDistance(myLat: Double, myLng: Double, locationLat: Double?, locationLng: Double?): Float? {
if (locationLat == null || locationLng == null) return null
val result = calculateDistanceBetweenPoints(Locale.Companion.current, myLat, myLng, locationLat, locationLng)
return result.toFloat()
} }
} }
\ No newline at end of file
package com.isidroid.location.ext
import androidx.compose.ui.text.intl.Locale
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
internal fun calculateDistanceBetweenPoints(
locale: Locale,
myLat: Double,
myLng: Double,
locationLat: Double,
locationLng: Double
): Double {
val earthRadius = 6371.0 // Радиус Земли в километрах
val dLat = Math.toRadians(locationLat - myLat)
val dLng = Math.toRadians(locationLng - myLng)
val a = sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(myLat)) * cos(Math.toRadians(locationLat)) *
sin(dLng / 2) * sin(dLng / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
val distanceKm = earthRadius * c
val distance = if (locale.isMileUnit)
convertKmToMiles(distanceKm)
else
distanceKm
return distance
}
val Locale.isMileUnit get() = region in arrayOf("US")
internal fun convertKmToMiles(km: Double): Double {
return km * 0.621371
}
...@@ -2,4 +2,5 @@ package com.isidroid.location.repository ...@@ -2,4 +2,5 @@ package com.isidroid.location.repository
interface LocationRepository { interface LocationRepository {
fun getCurrentLocation(): Pair<Double, Double> fun getCurrentLocation(): Pair<Double, Double>
fun findDistance(myLat: Double, myLng: Double, locationLat: Double?, locationLng: Double?): Float?
} }
\ No newline at end of file
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