Commit 716c048f by Aleksandr

Details screen (template)

parent 564e344c
...@@ -2,4 +2,5 @@ package com.isidroid.c23 ...@@ -2,4 +2,5 @@ package com.isidroid.c23
import kotlin.jvm.Throws import kotlin.jvm.Throws
class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m) class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m)
\ No newline at end of file class JobNotFoundException(m: String? = null): Throwable(m)
\ No newline at end of file
package com.isidroid.c23.constant package com.isidroid.c23.constant
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)
annotation class Argument { annotation class Argument {
companion object { companion object {
const val URI = "URI" const val URI = "URI"
...@@ -8,5 +11,7 @@ annotation class Argument { ...@@ -8,5 +11,7 @@ annotation class Argument {
const val LONGITUDE = "LONGITUDE" const val LONGITUDE = "LONGITUDE"
const val SPOT_CODE = "SPOT_CODE" const val SPOT_CODE = "SPOT_CODE"
const val ID = "ID" const val ID = "ID"
const val INFO = "INFO"
const val MARKER = "MARKER"
} }
} }
package com.isidroid.c23.data.mapper
import android.content.Context
import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.c23.ext.getPrintJobStatus
import com.isidroid.c23.ext.getPrintJobStatusColor
import com.isidroid.job.model.PrintJob
import com.isidroid.rendering.constant.printSizeName
import com.isidroid.spot.model.RichSpot
import java.io.File
fun PrintJob.createListItem(context: Context, richSpot: RichSpot?): PrintJobListItem {
val profile = richSpot?.printProfiles?.find { it.id == profileId }
return PrintJobListItem(
id = id,
spotCode = richSpot?.spot?.code,
spotName = richSpot?.spot?.name ?: "Deleted spot",
cost = cost,
paperInfo = printSize.printSizeName,
isColor = profile?.grayscale != true,
statusColor = getPrintJobStatusColor(status),
comment = comment,
copies = copies,
cover = sourceFiles?.firstOrNull()?.takeIf { File(it).exists() },
statusName = context.getString(getPrintJobStatus(status)),
accessCode = accessCode.orEmpty(),
createdAt = createdAt,
)
}
\ No newline at end of file
package com.isidroid.c23.domain.use_case
import android.content.Context
import com.isidroid.c23.JobNotFoundException
import com.isidroid.c23.constant.Argument
import com.isidroid.c23.data.mapper.createListItem
import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult
import com.isidroid.job.repository.JobRepository
import com.isidroid.location.repository.LocationRepository
import com.isidroid.spot.repository.SpotRepository
import com.isidroid.ui.maps.ext.addKilometerToLatLng
import com.isidroid.ui.maps.model.MapMarker
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DetailsUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val jobRepository: JobRepository,
private val spotRepository: SpotRepository,
private val locationRepository: LocationRepository,
) {
fun loadDetails(id: String) = flow {
emit(FlowResult.Loading)
val job = jobRepository.readLocalList(ids = listOf(id)).firstOrNull() ?: throw JobNotFoundException()
val richSpot = spotRepository.findLocalRichSpots(ids = listOf(job.spotId))?.firstOrNull()
val result = job.createListItem(context = context, richSpot = richSpot)
val currentLocation = locationRepository.getCurrentLocation()
val location = if (isDebug() && richSpot?.spot != null)
addKilometerToLatLng(richSpot.spot.lat, richSpot.spot.lng, distance = 0.2)
else
currentLocation
val mapMarker = richSpot?.spot?.let { spot ->
MapMarker(id = spot.id, lat = spot.lat, lng = spot.lng, name = spot.name)
}
emit(
FlowResult.Success(
mapOf(
Argument.INFO to result,
Argument.LATITUDE to location.first,
Argument.LONGITUDE to location.second,
Argument.MARKER to mapMarker
)
)
)
}
}
\ No newline at end of file
package com.isidroid.c23.domain.use_case package com.isidroid.c23.domain.use_case
import android.content.Context import android.content.Context
import com.isidroid.c23.domain.dto.PrintJobListItem import com.isidroid.c23.data.mapper.createListItem
import com.isidroid.c23.ext.getPrintJobStatus
import com.isidroid.c23.ext.getPrintJobStatusColor
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.rendering.constant.printSizeName
import com.isidroid.spot.repository.SpotRepository import com.isidroid.spot.repository.SpotRepository
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import java.io.File
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class PrintJobsUseCase @Inject constructor( class PrintJobsUseCase @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val repository: JobRepository, private val jobRepository: JobRepository,
private val spotRepository: SpotRepository private val spotRepository: SpotRepository
) { ) {
fun load() = flow { fun load() = flow {
emit(FlowResult.Loading) emit(FlowResult.Loading)
val jobList = repository.readLocalList() val jobList = jobRepository.readLocalList()
val spots = jobList val spots = jobList
.map { it.spotId } .map { it.spotId }
.distinct() .distinct()
.let { spotRepository.findLocalRichSpots(it) } .let { spotRepository.findLocalRichSpots(it) }
?.associateBy({ it.spot.id }, { it }) ?.associateBy({ it.spot.id }, { it })
val result = jobList.map { job -> val result = jobList.map { job -> job.createListItem(context = context, richSpot = spots?.get(job.spotId)) }
val richSpot = spots?.get(job.spotId)
val profile = richSpot?.printProfiles?.find { it.id == job.profileId }
PrintJobListItem(
id = job.id,
spotCode = richSpot?.spot?.code,
spotName = richSpot?.spot?.name ?: "Deleted spot",
cost = job.cost,
paperInfo = job.printSize.printSizeName,
isColor = profile?.grayscale != true,
statusColor = getPrintJobStatusColor(job.status),
comment = job.comment,
copies = job.copies,
cover = job.sourceFiles?.firstOrNull()?.takeIf { File(it).exists() },
statusName = context.getString(getPrintJobStatus(job.status)),
accessCode = job.accessCode.orEmpty(),
createdAt = job.createdAt,
)
}
emit(FlowResult.Success(result)) emit(FlowResult.Success(result))
} }
......
package com.isidroid.c23.ui.navigation.destinations package com.isidroid.c23.ui.navigation.destinations
import android.content.Context
import android.content.Intent
import android.widget.Toast
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import com.isidroid.c23.ui.screen.details.JobDetailsContract import com.isidroid.c23.ui.screen.details.JobDetailsContract
import com.isidroid.c23.ui.screen.details.JobDetailsScreen import com.isidroid.c23.ui.screen.details.JobDetailsScreen
import com.isidroid.c23.ui.screen.details.JobDetailsViewModel import com.isidroid.c23.ui.screen.details.JobDetailsViewModel
import com.isidroid.c23.ui.screen.print_jobs.PrintJobsContract
import com.isidroid.c23.ui.screen.print_jobs.PrintJobsScreen
import com.isidroid.c23.ui.screen.print_jobs.PrintJobsViewModel
@Composable @Composable
fun JobDetailsDestination(navController: NavHostController) { fun JobDetailsDestination(navController: NavHostController) {
val viewModel: JobDetailsViewModel = hiltViewModel() val viewModel: JobDetailsViewModel = hiltViewModel()
val context = LocalContext.current
JobDetailsScreen( JobDetailsScreen(
state = viewModel.viewState, state = viewModel.viewState,
effectFlow = viewModel.effect, effectFlow = viewModel.effect,
spotResultStateFlow = viewModel.spotResultStateFlow,
onEventSent = { event -> viewModel.setEvent(event) }, onEventSent = { event -> viewModel.setEvent(event) },
onNavigationRequested = { effect -> onNavigationRequested = { effect ->
when (effect) { when (effect) {
JobDetailsContract.Effect.Navigation.ToBack -> navController.popBackStack() JobDetailsContract.Effect.Navigation.ToBack -> navController.popBackStack()
is JobDetailsContract.Effect.Navigation.ToNavigationApp -> context.openGoogleMaps(lat = effect.lat, lng = effect.lng)
} }
}, },
) )
}
private fun Context.openGoogleMaps(lat: Double?, lng: Double?) {
if (lat == null || lng == null) return
val gmmIntentUri = "geo:$lat,$lng".toUri()
val mapIntent = Intent(Intent.ACTION_VIEW, gmmIntentUri)
// mapIntent.setPackage("com.google.android.apps.maps")
if (mapIntent.resolveActivity(packageManager) != null) {
startActivity(mapIntent)
} else {
Toast.makeText(this, "Navigation app not found", Toast.LENGTH_LONG).show()
}
} }
\ No newline at end of file
package com.isidroid.c23.ui.screen.details package com.isidroid.c23.ui.screen.details
import com.isidroid.c23.ui.screen.content.ContentContract import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.core.vm.ViewEvent import com.isidroid.core.vm.ViewEvent
import com.isidroid.core.vm.ViewSideEffect import com.isidroid.core.vm.ViewSideEffect
import com.isidroid.core.vm.ViewState import com.isidroid.core.vm.ViewState
...@@ -8,13 +8,23 @@ import com.isidroid.core.vm.ViewState ...@@ -8,13 +8,23 @@ import com.isidroid.core.vm.ViewState
class JobDetailsContract { class JobDetailsContract {
sealed interface Event : ViewEvent { sealed interface Event : ViewEvent {
data object ToBack : Event data object ToBack : Event
data object OpenConfirmationMapRoute: Event
data object DismissBuildRouteConfirmation: Event
data object OpenNavigationApp: Event
} }
sealed interface Effect : ViewSideEffect { sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect { sealed interface Navigation : Effect {
data object ToBack : Navigation data object ToBack : Navigation
data class ToNavigationApp(val lat: Double?, val lng: Double?) : Navigation
} }
} }
data class State(val i: Int = 0) : ViewState data class State(
val isLoading: Boolean = false,
val printJob: PrintJobListItem? = null,
val lat: Double? = null,
val lng: Double? = null,
val routeConfirmationVisible: Boolean = false
) : ViewState
} }
\ No newline at end of file
...@@ -2,37 +2,73 @@ package com.isidroid.c23.ui.screen.details ...@@ -2,37 +2,73 @@ 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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.BottomSheetDefaults
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.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults 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.mutableStateOf import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview 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.R
import com.isidroid.c23.ui._component.TopAppBarComponent import com.isidroid.c23.ui._component.TopAppBarComponent
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
import com.isidroid.c23.ui.theme.AppTheme
import com.isidroid.core.vm.SIDE_EFFECTS_KEY import com.isidroid.core.vm.SIDE_EFFECTS_KEY
import com.isidroid.ui.maps.model.MapMarker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun JobDetailsScreen( fun JobDetailsScreen(
state: State<JobDetailsContract.State>, state: State<JobDetailsContract.State>,
effectFlow: Flow<JobDetailsContract.Effect>?, effectFlow: Flow<JobDetailsContract.Effect>?,
spotResultStateFlow: StateFlow<List<MapMarker>>,
onEventSent: (event: JobDetailsContract.Event) -> Unit, onEventSent: (event: JobDetailsContract.Event) -> Unit,
onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit, onNavigationRequested: (navigationEffect: JobDetailsContract.Effect.Navigation) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
...@@ -47,12 +83,11 @@ fun JobDetailsScreen( ...@@ -47,12 +83,11 @@ fun JobDetailsScreen(
BackHandler { onEventSent(JobDetailsContract.Event.ToBack) } BackHandler { onEventSent(JobDetailsContract.Event.ToBack) }
Scaffold( Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
topBar = { topBar = {
TopAppBarComponent( TopAppBarComponent(
text = "Print job details", text = stringResource(id = R.string.print_job_details),
colors = TopAppBarDefaults.topAppBarColors(), colors = TopAppBarDefaults.topAppBarColors(),
onNavigationClick = { onEventSent(JobDetailsContract.Event.ToBack) } onNavigationClick = { onEventSent(JobDetailsContract.Event.ToBack) }
) )
...@@ -60,11 +95,131 @@ fun JobDetailsScreen( ...@@ -60,11 +95,131 @@ fun JobDetailsScreen(
) { paddingValues -> ) { paddingValues ->
Box( Box(
modifier = Modifier modifier = Modifier
.padding(paddingValues) .consumeWindowInsets(paddingValues)
.padding(top = paddingValues.calculateTopPadding())
.fillMaxSize(), .fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.TopStart
) { ) {
Text("Screen is under construction") 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 package com.isidroid.c23.ui.screen.details
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AccountCircle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.isidroid.c23.constant.Argument
import com.isidroid.c23.domain.dto.PrintJobListItem
import com.isidroid.c23.domain.use_case.DetailsUseCase
import com.isidroid.c23.ext.isDebug import com.isidroid.c23.ext.isDebug
import com.isidroid.core.FlowResult
import com.isidroid.core.vm.BaseViewModel import com.isidroid.core.vm.BaseViewModel
import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.utils.catchTimber
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class JobDetailsViewModel @Inject constructor() : BaseViewModel<JobDetailsContract.Event, JobDetailsContract.State, JobDetailsContract.Effect>() { class JobDetailsViewModel @Inject constructor(
private val useCase: DetailsUseCase,
savedStateHandle: SavedStateHandle
) : BaseViewModel<JobDetailsContract.Event, JobDetailsContract.State, JobDetailsContract.Effect>() {
private val _spotResultStateFlow = MutableStateFlow<List<MapMarker>>(emptyList())
val spotResultStateFlow = _spotResultStateFlow.asStateFlow()
init {
viewModelScope.launch {
savedStateHandle.getStateFlow<String?>(Argument.ID, null)
.filterNotNull()
.onEach { loadDetails(it) }
.firstOrNull()
}
}
override val isDebug: Boolean = isDebug() override val isDebug: Boolean = isDebug()
override fun setInitialState(): JobDetailsContract.State = JobDetailsContract.State() override fun setInitialState(): JobDetailsContract.State = JobDetailsContract.State()
override suspend fun handleEvents(event: JobDetailsContract.Event) { override suspend fun handleEvents(event: JobDetailsContract.Event) {
when(event){ when (event) {
JobDetailsContract.Event.ToBack -> setEffect { JobDetailsContract.Effect.Navigation.ToBack } JobDetailsContract.Event.ToBack -> setEffect { JobDetailsContract.Effect.Navigation.ToBack }
} JobDetailsContract.Event.OpenConfirmationMapRoute -> setState { copy(routeConfirmationVisible = true) }
JobDetailsContract.Event.DismissBuildRouteConfirmation -> setState { copy(routeConfirmationVisible = false) }
JobDetailsContract.Event.OpenNavigationApp -> openNavigationApp()
}
}
// handle events
private suspend fun loadDetails(id: String) {
useCase.loadDetails(id)
.flowOn(Dispatchers.IO)
.catchTimber { }
.collect { res ->
when (res) {
FlowResult.Loading -> setState { copy(isLoading = true) }
is FlowResult.Success -> onDetails(
item = res.result[Argument.INFO] as? PrintJobListItem,
lat = res.result[Argument.LATITUDE] as? Double,
lng = res.result[Argument.LONGITUDE] as? Double,
mapMarker = res.result[Argument.MARKER] as? MapMarker
)
}
}
}
private suspend fun onDetails(item: PrintJobListItem?, lat: Double?, lng: Double?, mapMarker: MapMarker?) {
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))
setState { copy(isLoading = false, printJob = item, lat = lat, lng = lng) }
}
private fun openNavigationApp(){
setState { copy(routeConfirmationVisible = false) }
setEffect { JobDetailsContract.Effect.Navigation.ToNavigationApp(viewState.value.lat, viewState.value.lng) }
} }
} }
\ No newline at end of file
package com.isidroid.c23.ui.screen.details.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun PrintCodeComponent(
code: String,
modifier: Modifier = Modifier
) {
Row(modifier = modifier) {
for (c in code)
PrintCodeElement(c)
}
}
@Composable
private fun PrintCodeElement(c: Char) {
Text(
text = "$c",
fontSize = 24.sp,
color = Color.White,
style = MaterialTheme.typography.displayLarge,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 8.dp)
.background(Color.Blue, RoundedCornerShape(6.dp))
.padding(horizontal = 12.dp, vertical = 0.dp)
)
}
@Preview
@Composable
fun PrintCodeComponentPreview() {
Surface {
PrintCodeComponent("7819128")
}
}
\ No newline at end of file
...@@ -68,7 +68,8 @@ fun MapScreen( ...@@ -68,7 +68,8 @@ fun MapScreen(
} }
) { paddingValues -> ) { paddingValues ->
TPMapComponent( TPMapComponent(
state = state, lat = state.value.lat,
lng = state.value.lng,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.consumeWindowInsets(paddingValues), .consumeWindowInsets(paddingValues),
......
package com.isidroid.c23.ui.screen.map._components package com.isidroid.c23.ui.screen.map._components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.isidroid.c23.ui.screen.map.MapContract import com.isidroid.c23.ui.screen.map.MapContract
import com.isidroid.ui.maps.model.MapMarker import com.isidroid.ui.maps.model.MapMarker
...@@ -11,13 +10,11 @@ import kotlinx.coroutines.flow.StateFlow ...@@ -11,13 +10,11 @@ import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun TPMapComponent( fun TPMapComponent(
onEventSent: (event: MapContract.Event) -> Unit, onEventSent: (event: MapContract.Event) -> Unit,
state: State<MapContract.State>, lat: Double?,
lng: Double?,
mapMarkersStateFlow: StateFlow<List<MapMarker>>, mapMarkersStateFlow: StateFlow<List<MapMarker>>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val lat = state.value.lat
val lng = state.value.lng
if (lat == null || lng == null) if (lat == null || lng == null)
return return
......
...@@ -148,7 +148,7 @@ private fun OptionsModalComponent( ...@@ -148,7 +148,7 @@ private fun OptionsModalComponent(
sheetState = sheetState, sheetState = sheetState,
modifier = modifier.fillMaxWidth(), modifier = modifier.fillMaxWidth(),
) { ) {
Column(modifier = modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
TopAppBar( TopAppBar(
title = { Text(text = stringResource(id = title)) }, title = { Text(text = stringResource(id = title)) },
navigationIcon = { Icon(Icons.Rounded.Close, contentDescription = null, modifier = Modifier.clickable { onCancel() }) }, navigationIcon = { Icon(Icons.Rounded.Close, contentDescription = null, modifier = Modifier.clickable { onCancel() }) },
......
...@@ -38,4 +38,6 @@ ...@@ -38,4 +38,6 @@
<string name="print_job_list">My Print jobs</string> <string name="print_job_list">My Print jobs</string>
<string name="rendered_files_copied_message">Successfully copied %d files</string> <string name="rendered_files_copied_message">Successfully copied %d files</string>
<string name="empty" /> <string name="empty" />
<string name="print_job_details">Job details</string>
<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>
</resources> </resources>
\ No newline at end of file
...@@ -4,7 +4,7 @@ object BuildVersions { ...@@ -4,7 +4,7 @@ object BuildVersions {
const val TARGET_SDK = 34 const val TARGET_SDK = 34
const val KOTLIN_COMPILER_EXT_VERSION = "1.5.3" const val KOTLIN_COMPILER_EXT_VERSION = "1.5.3"
const val ANDROID_PLUGIN = "8.5.0" const val ANDROID_PLUGIN = "8.5.1"
const val KOTLIN = "1.9.10" const val KOTLIN = "1.9.10"
const val KSP = "1.9.10-1.0.13" const val KSP = "1.9.10-1.0.13"
} }
...@@ -21,7 +21,7 @@ object GoogleVersions { ...@@ -21,7 +21,7 @@ object GoogleVersions {
const val hilt = "2.49" const val hilt = "2.49"
const val hiltNavigationCompose = "1.2.0" const val hiltNavigationCompose = "1.2.0"
const val hiltWork = "1.0.0" const val hiltWork = "1.0.0"
const val lifecycle = "2.8.2" const val lifecycle = "2.8.3"
const val navigation = "2.7.0" const val navigation = "2.7.0"
const val navigationCompose = "2.7.7" const val navigationCompose = "2.7.7"
const val preferences = "1.2.1" const val preferences = "1.2.1"
...@@ -29,7 +29,7 @@ object GoogleVersions { ...@@ -29,7 +29,7 @@ object GoogleVersions {
const val roomCompiler = "2.5.1" const val roomCompiler = "2.5.1"
const val splash = "1.0.1" const val splash = "1.0.1"
const val work = "2.9.0" const val work = "2.9.0"
const val services = "4.4.1" const val services = "4.4.2"
const val constraint = "1.0.1" const val constraint = "1.0.1"
const val paging = "3.2.1" const val paging = "3.2.1"
const val materialView = "1.11.0" const val materialView = "1.11.0"
...@@ -51,14 +51,14 @@ object NetworkVersions { ...@@ -51,14 +51,14 @@ object NetworkVersions {
object FirebaseDependencies { object FirebaseDependencies {
const val analytics = "com.google.firebase:firebase-analytics-ktx" const val analytics = "com.google.firebase:firebase-analytics-ktx"
const val bom = "33.1.1" const val bom = "33.1.2"
const val crashlytics = "com.google.firebase:firebase-crashlytics-ktx" const val crashlytics = "com.google.firebase:firebase-crashlytics-ktx"
const val messaging = "com.google.firebase:firebase-messaging-ktx" const val messaging = "com.google.firebase:firebase-messaging-ktx"
const val config = "com.google.firebase:firebase-config-ktx" const val config = "com.google.firebase:firebase-config-ktx"
} }
object FirebaseVersions { object FirebaseVersions {
const val crashlyticsPlugin = "2.9.9" const val crashlyticsPlugin = "3.0.2"
} }
object ToolsVersions { object ToolsVersions {
......
...@@ -78,7 +78,7 @@ dependencies { ...@@ -78,7 +78,7 @@ dependencies {
googleImplementation("com.google.maps.android:maps-compose:${GoogleVersions.maps}") googleImplementation("com.google.maps.android:maps-compose:${GoogleVersions.maps}")
googleImplementation("com.google.maps.android:android-maps-utils:${GoogleVersions.mapUtils}") googleImplementation("com.google.maps.android:android-maps-utils:${GoogleVersions.mapUtils}")
googleImplementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.1") googleImplementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.3")
googleImplementation("com.google.maps.android:maps-compose-utils:2.11.4") googleImplementation("com.google.maps.android:maps-compose-utils:2.11.4")
googleImplementation("com.google.maps.android:maps-compose-widgets:2.11.4") googleImplementation("com.google.maps.android:maps-compose-widgets:2.11.4")
......
package com.isidroid.ui.maps package com.isidroid.ui.maps
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
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.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Place
import androidx.compose.material.icons.outlined.MailOutline
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
...@@ -16,13 +22,16 @@ import androidx.compose.runtime.remember ...@@ -16,13 +22,16 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.compose.GoogleMap import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapProperties import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapUiSettings import com.google.maps.android.compose.MapUiSettings
...@@ -53,6 +62,7 @@ fun MapsComponent( ...@@ -53,6 +62,7 @@ fun MapsComponent(
val items = mapMarkersStateFlow.collectAsState() val items = mapMarkersStateFlow.collectAsState()
val stateFlow = remember { MutableStateFlow(LatLng(lat, lng)) } val stateFlow = remember { MutableStateFlow(LatLng(lat, lng)) }
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
LaunchedEffect(cameraPositionState) { LaunchedEffect(cameraPositionState) {
snapshotFlow { cameraPositionState.position } snapshotFlow { cameraPositionState.position }
...@@ -79,12 +89,13 @@ fun MapsComponent( ...@@ -79,12 +89,13 @@ fun MapsComponent(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
properties = MapProperties(isMyLocationEnabled = false), properties = MapProperties(isMyLocationEnabled = false),
cameraPositionState = cameraPositionState, cameraPositionState = cameraPositionState,
uiSettings = MapUiSettings(zoomControlsEnabled = false ), uiSettings = MapUiSettings(zoomControlsEnabled = true),
onMapLoaded = {}, onMapLoaded = {},
onMapClick = {}, onMapClick = {},
onMyLocationClick = {}, onMyLocationClick = {},
) { ) {
val parkMarkers = items.value.map { it.transformToClusterItem() } val parkMarkers = items.value.map { it.transformToClusterItem() }
Clustering( Clustering(
items = parkMarkers, items = parkMarkers,
onClusterItemClick = { onClusterItemClick = {
...@@ -96,15 +107,26 @@ fun MapsComponent( ...@@ -96,15 +107,26 @@ fun MapsComponent(
true true
}, },
onClusterItemInfoWindowClick = {}, onClusterItemInfoWindowClick = {},
clusterItemContent = {
if (it.imageVector != null)
Icon(it.imageVector, contentDescription = null)
else
Icon(
Icons.Default.Place,
contentDescription = null,
tint = Color.Red,
modifier = Modifier.scale(1.2f)
)
}
) )
} }
IconButton( IconButton(
onClick = { moveCameraToMyLocation() }, onClick = { moveCameraToMyLocation() },
modifier = Modifier modifier = Modifier
.align(Alignment.BottomEnd) .align(Alignment.BottomStart)
.padding(bottom = 72.dp, end = 12.dp) .padding(bottom = 72.dp, start = 12.dp)
.background(Color.Black, CircleShape) .background(Color.Black.copy(alpha = .45f), CircleShape)
) { ) {
Icon( Icon(
imageVector = Icons.Default.LocationOn, imageVector = Icons.Default.LocationOn,
......
package com.isidroid.ui.maps
// val context = LocalContext.current
// val lifecycleOwner = LocalLifecycleOwner.current
//
// val stateFlow = remember { MutableStateFlow(LatLng(lat, lng)) }
// var googleMapView by remember { mutableStateOf<GoogleMap?>(null) }
// var clusterManager by remember { mutableStateOf<ClusterManager<MyClusterItem>?>(null) }
//
//
// LaunchedEffect(stateFlow) {
// stateFlow
// .flowWithLifecycle(lifecycleOwner.lifecycle)
// .distinctUntilChanged()
// .debounce(1000L)
// .collect { newLatLng ->
// val radius = findMapRadius(googleMapView)
// onCameraMove(newLatLng.latitude, newLatLng.longitude, radius)
// }
// }
//
// MapCreatorComponent(
// modifier = modifier,
// stateFlow = stateFlow,
// onMapViewReady = { map, manager ->
// googleMapView = map
// clusterManager = manager
//
// Timber.i("sdfsdfsdf MapCreatorComponent.onMapViewReady")
//
//// map.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(lat, lng), 16f))
// }
// )
//
// HandleCallbacks(mapMarkersStateFlow, clusterManager)
//
// LaunchedEffect(stateFlow) {
// stateFlow
// .flowWithLifecycle(lifecycleOwner.lifecycle)
// .distinctUntilChanged()
// .debounce(1000L)
// .collect { newLatLng ->
// val radius = findMapRadius(googleMapView)
// onCameraMove(newLatLng.latitude, newLatLng.longitude, radius)
// }
// }
//}
//
//@Composable
//private fun HandleCallbacks(mapMarkersStateFlow: StateFlow<List<MapMarker>>, clusterManager: ClusterManager<MyClusterItem>?) {
//// val coroutineScope = rememberCoroutineScope()
// val mapMarkers = mapMarkersStateFlow.collectAsState()
////
//// Timber.i("sdfsdfsdf mapMarkers=${mapMarkers.value.size}, clusterManager=$clusterManager")
//}
//
//@Composable
//private fun MapCreatorComponent(
// modifier: Modifier,
// stateFlow: MutableStateFlow<LatLng>,
// onMapViewReady: (GoogleMap, ClusterManager<MyClusterItem>) -> Unit
//) {
// val mapView = rememberMapViewWithLifecycle()
// val context = LocalContext.current
// val coroutineScope = rememberCoroutineScope()
// var isCreated by remember { mutableStateOf(false) }
//
// Timber.i("sdfsdfsdf MapCreatorComponent create isCreated=$isCreated")
//
// if(isCreated) return
//
// AndroidView(
// factory = { mapView },
// modifier = modifier
// ) {
// mapView.getMapAsync { googleMap ->
// Timber.i("sdfsdfsdf MapCreatorComponent.onMapViewReady")
// isCreated = true
//
//
// val clusterManager = ClusterManager<MyClusterItem>(context, googleMap)
// onMapViewReady(googleMap, clusterManager)
//
// googleMap.setOnCameraIdleListener(clusterManager)
// googleMap.setOnMarkerClickListener(clusterManager)
//
// clusterManager.setOnClusterClickListener { clusterItem ->
// true
// }
//
// clusterManager.setOnClusterItemClickListener { clusterItem -> true }
// googleMap.setOnCameraMoveListener {
// val centerLocation = googleMap.cameraPosition.target
// coroutineScope.launch { stateFlow.emit(centerLocation) }
// }
// }
// }
//}
//
//
////private fun addClusteredMarkers(clusterManager: ClusterManager<MyClusterItem>) {
//// val items = listOf(
//// MyClusterItem(LatLng(40.7579247, -73.9881229), "Title 1", "Snippet 1"),
//// MyClusterItem(LatLng(40.7579347, -73.9881129), "Title 2", "Snippet 2"),
//// MyClusterItem(LatLng(40.7579347, -73.9851129), "Title 3", "Snippet 2")
////
//// )
////
//// clusterManager.clearItems()
//// clusterManager.addItems(items)
//// clusterManager.cluster()
////}
\ No newline at end of file
...@@ -4,4 +4,4 @@ import com.google.android.gms.maps.model.LatLng ...@@ -4,4 +4,4 @@ import com.google.android.gms.maps.model.LatLng
import com.isidroid.ui.maps.model.MapMarker import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.ui.maps.model.MyClusterItem import com.isidroid.ui.maps.model.MyClusterItem
fun MapMarker.transformToClusterItem() = MyClusterItem(latLng = LatLng(lat, lng), title = name, snippet = "", id = id) fun MapMarker.transformToClusterItem() = MyClusterItem(latLng = LatLng(lat, lng), title = name, snippet = "", id = id, imageVector = imageVector)
\ No newline at end of file \ No newline at end of file
package com.isidroid.ui.maps.model package com.isidroid.ui.maps.model
import androidx.compose.ui.graphics.vector.ImageVector
import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem import com.google.maps.android.clustering.ClusterItem
...@@ -7,7 +8,8 @@ data class MyClusterItem( ...@@ -7,7 +8,8 @@ data class MyClusterItem(
val id: String, val id: String,
private val latLng: LatLng, private val latLng: LatLng,
private val title: String, private val title: String,
private val snippet: String private val snippet: String,
val imageVector: ImageVector? = null
) : ClusterItem { ) : ClusterItem {
override fun getPosition(): LatLng = latLng override fun getPosition(): LatLng = latLng
override fun getTitle(): String = title override fun getTitle(): String = title
......
package com.isidroid.ui.maps.ext
// Константы
const val EARTH_RADIUS = 6371.0 // Радиус Земли в километрах
fun addKilometerToLatLng(lat: Double, lng: Double, distance: Double = 1.0): Pair<Double, Double> {
val latInRadians = Math.toRadians(lat)
val newLat = latInRadians + distance / EARTH_RADIUS
val newLatInDegrees = Math.toDegrees(newLat)
return Pair(newLatInDegrees, lng)
}
package com.isidroid.ui.maps.model package com.isidroid.ui.maps.model
data class MapMarker(val id: String, val name: String, val lat: Double, val lng: Double) import androidx.compose.ui.graphics.vector.ImageVector
\ No newline at end of file
data class MapMarker(
val id: String,
val name: String,
val lat: Double,
val lng: Double,
val imageVector: ImageVector? = null
)
\ 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