Commit 716c048f by Aleksandr

Details screen (template)

parent 564e344c
......@@ -2,4 +2,5 @@ package com.isidroid.c23
import kotlin.jvm.Throws
class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m)
\ No newline at end of file
class SpotHasNoPrintProfilesException(m: String? = null): Throwable(m)
class JobNotFoundException(m: String? = null): Throwable(m)
\ No newline at end of file
package com.isidroid.c23.constant
import androidx.annotation.StringDef
@Retention(AnnotationRetention.SOURCE)
@StringDef(Argument.URI, Argument.LATITUDE, Argument.LONGITUDE, Argument.SPOT_CODE, Argument.ID, Argument.INFO, Argument.MARKER)
annotation class Argument {
companion object {
const val URI = "URI"
......@@ -8,5 +11,7 @@ annotation class Argument {
const val LONGITUDE = "LONGITUDE"
const val SPOT_CODE = "SPOT_CODE"
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
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.c23.data.mapper.createListItem
import com.isidroid.core.FlowResult
import com.isidroid.job.repository.JobRepository
import com.isidroid.rendering.constant.printSizeName
import com.isidroid.spot.repository.SpotRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.flow
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PrintJobsUseCase @Inject constructor(
@ApplicationContext private val context: Context,
private val repository: JobRepository,
private val jobRepository: JobRepository,
private val spotRepository: SpotRepository
) {
fun load() = flow {
emit(FlowResult.Loading)
val jobList = repository.readLocalList()
val jobList = jobRepository.readLocalList()
val spots = jobList
.map { it.spotId }
.distinct()
.let { spotRepository.findLocalRichSpots(it) }
?.associateBy({ it.spot.id }, { it })
val result = jobList.map { job ->
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,
)
}
val result = jobList.map { job -> job.createListItem(context = context, richSpot = spots?.get(job.spotId)) }
emit(FlowResult.Success(result))
}
......
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.ui.platform.LocalContext
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.isidroid.c23.ui.screen.details.JobDetailsContract
import com.isidroid.c23.ui.screen.details.JobDetailsScreen
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
fun JobDetailsDestination(navController: NavHostController) {
val viewModel: JobDetailsViewModel = hiltViewModel()
val context = LocalContext.current
JobDetailsScreen(
state = viewModel.viewState,
effectFlow = viewModel.effect,
spotResultStateFlow = viewModel.spotResultStateFlow,
onEventSent = { event -> viewModel.setEvent(event) },
onNavigationRequested = { effect ->
when (effect) {
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
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.ViewSideEffect
import com.isidroid.core.vm.ViewState
......@@ -8,13 +8,23 @@ import com.isidroid.core.vm.ViewState
class JobDetailsContract {
sealed interface Event : ViewEvent {
data object ToBack : Event
data object OpenConfirmationMapRoute: Event
data object DismissBuildRouteConfirmation: Event
data object OpenNavigationApp: Event
}
sealed interface Effect : ViewSideEffect {
sealed interface Navigation : Effect {
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
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.displayCutout
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.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.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
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.mutableStateOf
import androidx.compose.runtime.remember
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.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
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.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.ui.maps.model.MapMarker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun JobDetailsScreen(
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,
......@@ -47,12 +83,11 @@ fun JobDetailsScreen(
BackHandler { onEventSent(JobDetailsContract.Event.ToBack) }
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBarComponent(
text = "Print job details",
text = stringResource(id = R.string.print_job_details),
colors = TopAppBarDefaults.topAppBarColors(),
onNavigationClick = { onEventSent(JobDetailsContract.Event.ToBack) }
)
......@@ -60,11 +95,131 @@ fun JobDetailsScreen(
) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.consumeWindowInsets(paddingValues)
.padding(top = paddingValues.calculateTopPadding())
.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
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.core.FlowResult
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 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
@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 fun setInitialState(): JobDetailsContract.State = JobDetailsContract.State()
override suspend fun handleEvents(event: JobDetailsContract.Event) {
when(event){
JobDetailsContract.Event.ToBack -> setEffect { JobDetailsContract.Effect.Navigation.ToBack }
}
when (event) {
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(
}
) { paddingValues ->
TPMapComponent(
state = state,
lat = state.value.lat,
lng = state.value.lng,
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(paddingValues),
......
package com.isidroid.c23.ui.screen.map._components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import com.isidroid.c23.ui.screen.map.MapContract
import com.isidroid.ui.maps.model.MapMarker
......@@ -11,13 +10,11 @@ import kotlinx.coroutines.flow.StateFlow
@Composable
fun TPMapComponent(
onEventSent: (event: MapContract.Event) -> Unit,
state: State<MapContract.State>,
lat: Double?,
lng: Double?,
mapMarkersStateFlow: StateFlow<List<MapMarker>>,
modifier: Modifier = Modifier
) {
val lat = state.value.lat
val lng = state.value.lng
if (lat == null || lng == null)
return
......
......@@ -148,7 +148,7 @@ private fun OptionsModalComponent(
sheetState = sheetState,
modifier = modifier.fillMaxWidth(),
) {
Column(modifier = modifier.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth()) {
TopAppBar(
title = { Text(text = stringResource(id = title)) },
navigationIcon = { Icon(Icons.Rounded.Close, contentDescription = null, modifier = Modifier.clickable { onCancel() }) },
......
......@@ -38,4 +38,6 @@
<string name="print_job_list">My Print jobs</string>
<string name="rendered_files_copied_message">Successfully copied %d files</string>
<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>
\ No newline at end of file
......@@ -4,7 +4,7 @@ object BuildVersions {
const val TARGET_SDK = 34
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 KSP = "1.9.10-1.0.13"
}
......@@ -21,7 +21,7 @@ object GoogleVersions {
const val hilt = "2.49"
const val hiltNavigationCompose = "1.2.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 navigationCompose = "2.7.7"
const val preferences = "1.2.1"
......@@ -29,7 +29,7 @@ object GoogleVersions {
const val roomCompiler = "2.5.1"
const val splash = "1.0.1"
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 paging = "3.2.1"
const val materialView = "1.11.0"
......@@ -51,14 +51,14 @@ object NetworkVersions {
object FirebaseDependencies {
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 messaging = "com.google.firebase:firebase-messaging-ktx"
const val config = "com.google.firebase:firebase-config-ktx"
}
object FirebaseVersions {
const val crashlyticsPlugin = "2.9.9"
const val crashlyticsPlugin = "3.0.2"
}
object ToolsVersions {
......
......@@ -78,7 +78,7 @@ dependencies {
googleImplementation("com.google.maps.android:maps-compose:${GoogleVersions.maps}")
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-widgets:2.11.4")
......
package com.isidroid.ui.maps
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
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.IconButton
import androidx.compose.runtime.Composable
......@@ -16,13 +22,16 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import androidx.lifecycle.flowWithLifecycle
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.CameraPosition
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.MapProperties
import com.google.maps.android.compose.MapUiSettings
......@@ -53,6 +62,7 @@ fun MapsComponent(
val items = mapMarkersStateFlow.collectAsState()
val stateFlow = remember { MutableStateFlow(LatLng(lat, lng)) }
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
LaunchedEffect(cameraPositionState) {
snapshotFlow { cameraPositionState.position }
......@@ -79,12 +89,13 @@ fun MapsComponent(
modifier = modifier.fillMaxSize(),
properties = MapProperties(isMyLocationEnabled = false),
cameraPositionState = cameraPositionState,
uiSettings = MapUiSettings(zoomControlsEnabled = false ),
uiSettings = MapUiSettings(zoomControlsEnabled = true),
onMapLoaded = {},
onMapClick = {},
onMyLocationClick = {},
) {
val parkMarkers = items.value.map { it.transformToClusterItem() }
Clustering(
items = parkMarkers,
onClusterItemClick = {
......@@ -96,15 +107,26 @@ fun MapsComponent(
true
},
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(
onClick = { moveCameraToMyLocation() },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(bottom = 72.dp, end = 12.dp)
.background(Color.Black, CircleShape)
.align(Alignment.BottomStart)
.padding(bottom = 72.dp, start = 12.dp)
.background(Color.Black.copy(alpha = .45f), CircleShape)
) {
Icon(
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
import com.isidroid.ui.maps.model.MapMarker
import com.isidroid.ui.maps.model.MyClusterItem
fun MapMarker.transformToClusterItem() = MyClusterItem(latLng = LatLng(lat, lng), title = name, snippet = "", id = id)
\ No newline at end of file
fun MapMarker.transformToClusterItem() = MyClusterItem(latLng = LatLng(lat, lng), title = name, snippet = "", id = id, imageVector = imageVector)
\ No newline at end of file
package com.isidroid.ui.maps.model
import androidx.compose.ui.graphics.vector.ImageVector
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem
......@@ -7,7 +8,8 @@ data class MyClusterItem(
val id: String,
private val latLng: LatLng,
private val title: String,
private val snippet: String
private val snippet: String,
val imageVector: ImageVector? = null
) : ClusterItem {
override fun getPosition(): LatLng = latLng
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
data class MapMarker(val id: String, val name: String, val lat: Double, val lng: Double)
\ No newline at end of file
import androidx.compose.ui.graphics.vector.ImageVector
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