diff --git a/android/app/src/main/java/app/tourism/Constants.kt b/android/app/src/main/java/app/tourism/Constants.kt index 0e23efd318..c600b11238 100644 --- a/android/app/src/main/java/app/tourism/Constants.kt +++ b/android/app/src/main/java/app/tourism/Constants.kt @@ -10,8 +10,6 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.organicmaps.R import app.tourism.ui.theme.getBorderColor @@ -33,7 +31,7 @@ object Constants { const val IMAGE_URL_EXAMPLE = "https://img.freepik.com/free-photo/young-woman-hiker-taking-photo-with-smartphone-on-mountains-peak-in-winter_335224-427.jpg?w=2000" const val THUMBNAIL_URL_EXAMPLE = - "https://cdn.pixabay.com/photo/2020/03/24/22/34/illustration-4965674_960_720.jpg" + "https://render.fineartamerica.com/images/images-profile-flow/400/images-medium-large-5/awesome-solitude-bess-hamiti.jpg" const val LOGO_URL_EXAMPLE = "https://brandeps.com/logo-download/O/OSCE-logo-vector-01.svg" // data diff --git a/android/app/src/main/java/app/tourism/MainActivity.kt b/android/app/src/main/java/app/tourism/MainActivity.kt index a3becc2d39..2f15757cb0 100644 --- a/android/app/src/main/java/app/tourism/MainActivity.kt +++ b/android/app/src/main/java/app/tourism/MainActivity.kt @@ -3,6 +3,7 @@ package app.tourism import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels @@ -10,6 +11,7 @@ import androidx.compose.runtime.collectAsState import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.lifecycleScope import app.organicmaps.DownloadResourcesLegacyActivity +import app.organicmaps.R import app.organicmaps.downloader.CountryItem import app.tourism.data.prefs.UserPreferences import app.tourism.domain.models.resource.Resource @@ -36,7 +38,11 @@ class MainActivity : ComponentActivity() { navigateToMapToDownloadIfNotPresent() navigateToAuthIfNotAuthed() - enableEdgeToEdge() + val blackest = resources.getColor(R.color.button_text) // yes, I know + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.dark(blackest), + navigationBarStyle = SystemBarStyle.dark(blackest) + ) setContent { val isDark = themeVM.theme.collectAsState().value?.code == "dark" diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt index d15f119793..a5455eed9a 100644 --- a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt +++ b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt @@ -1,7 +1,7 @@ package app.tourism.data.dto.profile data class User( - val id: Int, + val id: Long, val avatar: String?, val country: String, val full_name: String, diff --git a/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt b/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt index de2a79a58c..4b2095bb9e 100644 --- a/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt +++ b/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt @@ -1,7 +1,7 @@ package app.tourism.domain.models.common data class PlaceShort( - val id: Int, + val id: Long, val name: String, val pic: String? = null, val rating: Double? = null, diff --git a/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt b/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt index abc9586872..dd13ba5f9b 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt @@ -3,7 +3,7 @@ package app.tourism.domain.models.details import app.tourism.data.dto.PlaceLocation data class PlaceFull( - val id: Int, + val id: Long, val name: String, val rating: Double? = null, val excerpt: String? = null, @@ -11,5 +11,5 @@ data class PlaceFull( val placeLocation: PlaceLocation? = null, val pic: String? = null, val pics: List = emptyList(), - val reviews: List = emptyList(), + val isFavorite: Boolean = false, ) diff --git a/android/app/src/main/java/app/tourism/domain/models/details/Review.kt b/android/app/src/main/java/app/tourism/domain/models/details/Review.kt index ad28ca0ac2..95eec0324d 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/Review.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/Review.kt @@ -1,11 +1,10 @@ package app.tourism.domain.models.details data class Review( + val id: Long, val rating: Double? = null, - val name: String, - val pfpUrl: String? = null, - val countryCodeName: String? = null, + val user: User, val date: String? = null, - val text: String? = null, + val comment: String? = null, val picsUrls: List = emptyList(), ) diff --git a/android/app/src/main/java/app/tourism/domain/models/details/User.kt b/android/app/src/main/java/app/tourism/domain/models/details/User.kt new file mode 100644 index 0000000000..a4181307ab --- /dev/null +++ b/android/app/src/main/java/app/tourism/domain/models/details/User.kt @@ -0,0 +1,8 @@ +package app.tourism.domain.models.details + +data class User( + val id: Long, + val name: String, + val pfpUrl: String? = null, + val countryCodeName: String? = null, +) diff --git a/android/app/src/main/java/app/tourism/ui/common/ImagePicker.kt b/android/app/src/main/java/app/tourism/ui/common/ImagePicker.kt index 07aa6480d8..a5e87bfe76 100644 --- a/android/app/src/main/java/app/tourism/ui/common/ImagePicker.kt +++ b/android/app/src/main/java/app/tourism/ui/common/ImagePicker.kt @@ -6,7 +6,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf diff --git a/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt b/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt index f98ad52041..9f093505ed 100644 --- a/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt +++ b/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import app.organicmaps.R import coil.compose.AsyncImage -import coil.decode.SvgDecoder import coil.request.ImageRequest @Composable @@ -35,6 +34,7 @@ fun LoadImg( ) else Column( + modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { diff --git a/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt b/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt index 4e22a0a00a..a3b52c3600 100644 --- a/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt +++ b/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt @@ -1,6 +1,5 @@ package app.tourism.ui.common -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions diff --git a/android/app/src/main/java/app/tourism/ui/common/Spacer.kt b/android/app/src/main/java/app/tourism/ui/common/Spacer.kt index c7378ff553..aab9af5edb 100644 --- a/android/app/src/main/java/app/tourism/ui/common/Spacer.kt +++ b/android/app/src/main/java/app/tourism/ui/common/Spacer.kt @@ -1,7 +1,5 @@ package app.tourism.ui.common -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width @@ -10,7 +8,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp @Composable -fun RowScope.HorizontalSpace(width: Dp) = Spacer(modifier = Modifier.width(width)) +fun HorizontalSpace(width: Dp) = Spacer(modifier = Modifier.width(width)) @Composable -fun ColumnScope.VerticalSpace(height: Dp) = Spacer(modifier = Modifier.height(height)) \ No newline at end of file +fun VerticalSpace(height: Dp) = Spacer(modifier = Modifier.height(height)) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/WebView.kt b/android/app/src/main/java/app/tourism/ui/common/WebView.kt new file mode 100644 index 0000000000..ab60b13be8 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/WebView.kt @@ -0,0 +1,20 @@ +package app.tourism.ui.common + +import android.webkit.WebView +import androidx.compose.runtime.Composable +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +fun WebView(data: String) { + AndroidView( + factory = { context -> + WebView(context).apply { + settings.loadWithOverviewMode = true + loadData(data, "text/html", "UTF-8") + } + }, + update = { + it.loadData(data, "text/html", "UTF-8") + } + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt b/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt index b3f9dd6d76..974c6412db 100644 --- a/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt +++ b/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt @@ -13,6 +13,7 @@ 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.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.tourism.ui.theme.TextStyles @@ -48,6 +49,7 @@ fun ButtonText(buttonLabel: String) { Text( text = buttonLabel, style = TextStyles.h4, + fontWeight = FontWeight.W700, maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colorScheme.onPrimary diff --git a/android/app/src/main/java/app/tourism/ui/common/buttons/SecondaryButton.kt b/android/app/src/main/java/app/tourism/ui/common/buttons/SecondaryButton.kt index 271438e4a2..b12e1cadd3 100644 --- a/android/app/src/main/java/app/tourism/ui/common/buttons/SecondaryButton.kt +++ b/android/app/src/main/java/app/tourism/ui/common/buttons/SecondaryButton.kt @@ -7,9 +7,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt b/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt index 7a9953ae50..3311ab2023 100644 --- a/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt +++ b/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt @@ -24,7 +24,7 @@ import app.tourism.ui.theme.TextStyles fun AppTopBar( modifier: Modifier = Modifier, title: String, - onBackClick: (() -> Boolean)? = null, + onBackClick: (() -> Unit)? = null, actions: List = emptyList() ) { Column( diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt index 44426b5ecd..0c64f0bb67 100644 --- a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt +++ b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt @@ -14,12 +14,12 @@ import app.organicmaps.R @Composable fun BackButton( modifier: Modifier = Modifier, - onBackClick: () -> Boolean, + onBackClick: () -> Unit, tint: Color = MaterialTheme.colorScheme.onBackground ) { Icon( - modifier = modifier - .size(24.dp) + modifier = Modifier + .size(30.dp) .clickable { onBackClick() } .then(modifier), painter = painterResource(id = R.drawable.back), diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/BackButtonWithText.kt b/android/app/src/main/java/app/tourism/ui/common/nav/BackButtonWithText.kt new file mode 100644 index 0000000000..39bcff56a6 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/nav/BackButtonWithText.kt @@ -0,0 +1,34 @@ +package app.tourism.ui.common.nav + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.organicmaps.R +import app.tourism.ui.common.HorizontalSpace + +@Composable +fun BackButtonWithText(modifier: Modifier = Modifier, onBackClick: () -> Unit) { + TextButton( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + onClick = { onBackClick() }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.back), + contentDescription = stringResource(id = R.string.back) + ) + HorizontalSpace(width = 16.dp) + Text(text = stringResource(id = R.string.back)) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt b/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt new file mode 100644 index 0000000000..6eb99936c6 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt @@ -0,0 +1,124 @@ +package app.tourism.ui.common.nav + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.painterResource +import androidx.compose.ui.unit.dp +import app.organicmaps.R +import app.tourism.ui.common.LoadImg +import app.tourism.ui.theme.TextStyles + +@Composable +fun PlaceTopBar( + modifier: Modifier = Modifier, + title: String, + picUrl: String?, + onBackClick: (() -> Unit)? = null, + isFavorite: Boolean, + onFavoriteChanged: (Boolean) -> Unit, + onMapClick: () -> Unit +) { + val height = 144.dp + Box( + Modifier + .fillMaxWidth() + .height(height) + .clip( + RoundedCornerShape( + topStart = 0.dp, + topEnd = 0.dp, + bottomStart = 20.dp, + bottomEnd = 20.dp + ) + ) + .then(modifier) + ) { + LoadImg( + modifier = Modifier + .fillMaxWidth() + .height(height), + url = picUrl, + ) + + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color.Black.copy(alpha = 0.3f)), + ) + + val padding = 16.dp + Box( + Modifier + .fillMaxWidth() + .align(alignment = Alignment.TopCenter) + .padding(start = padding, end = padding, top = padding) + ) { + onBackClick?.let { + PlaceTopBarAction( + modifier.align(Alignment.CenterStart), + iconDrawable = R.drawable.back, + onClick = { onBackClick() }, + ) + } + Row(modifier.align(Alignment.CenterEnd)) { + PlaceTopBarAction( + iconDrawable = if (isFavorite) R.drawable.heart_selected else R.drawable.heart, + onClick = { onFavoriteChanged(!isFavorite) }, + ) + PlaceTopBarAction( + iconDrawable = R.drawable.map, + onClick = onMapClick, + ) + } + } + + Text( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(start = padding, end = padding, bottom = padding), + text = title, + style = TextStyles.h2, + color = Color.White + ) + } +} + +@Composable +private fun PlaceTopBarAction( + modifier: Modifier = Modifier, + @DrawableRes iconDrawable: Int, + onClick: () -> Unit, +) { + val shape = CircleShape + IconButton(modifier = Modifier.then(modifier), onClick = onClick) { + Icon( + modifier = Modifier + .clickable { onClick() } + .background(color = Color.White.copy(alpha = 0.2f), shape = shape) + .clip(shape) + .size(40.dp) + .padding(8.dp), + painter = painterResource(id = iconDrawable), + tint = Color.White, + contentDescription = null, + ) + } +} diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt b/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt new file mode 100644 index 0000000000..ddbb1abafb --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt @@ -0,0 +1,70 @@ +package app.tourism.ui.common.nav + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import app.organicmaps.R +import app.tourism.ui.theme.TextStyles +import app.tourism.ui.theme.getHintColor + +@Composable +fun SearchTopBar( + modifier: Modifier = Modifier, + query: String, + onQueryChanged: (String) -> Unit, + onSearchClicked: (String) -> Unit, + onClearClicked: () -> Unit, + onBackClicked: () -> Unit +) { + val searchLabel = stringResource(id = R.string.search) + + TextField( + modifier = Modifier + .fillMaxWidth() + .then(modifier), + value = query, + onValueChange = onQueryChanged, + placeholder = { + Text( + text = searchLabel, + style = TextStyles.h4.copy(color = getHintColor()), + ) + }, + singleLine = true, + maxLines = 1, + leadingIcon = { + IconButton(onClick = { onBackClicked() }) { + Icon( + painter = painterResource(id = R.drawable.back), + contentDescription = stringResource(id = R.string.back), + ) + } + }, + trailingIcon = { + if (query.isNotEmpty()) + IconButton(onClick = { onClearClicked() }) { + Icon( + painter = painterResource(id = R.drawable.ic_clear_rounded), + contentDescription = stringResource(id = R.string.clear_search_field), + ) + } + }, + keyboardActions = KeyboardActions(onSearch = { onSearchClicked(query) }), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.background, + unfocusedContainerColor = MaterialTheme.colorScheme.background, + ) + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt b/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt new file mode 100644 index 0000000000..77eb1f9a73 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/special/CountryAsLabel.kt @@ -0,0 +1,25 @@ +package app.tourism.ui.common.special + +import android.view.LayoutInflater +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import app.organicmaps.R +import com.hbb20.CountryCodePicker + +@Composable +fun CountryAsLabel(modifier: Modifier = Modifier, countryCodeName: String, contentColor: Int) { + AndroidView( + modifier = Modifier.then(modifier), + factory = { context -> + val view = LayoutInflater.from(context) + .inflate(R.layout.ccp_as_country_label, null, false) + val ccp = view.findViewById(R.id.ccp) + ccp.contentColor = contentColor + ccp.setCountryForNameCode(countryCodeName) + ccp.showArrow(false) + ccp.setCcpClickable(false) + view + } + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt b/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt index faa11a76c0..f2f4ad5b37 100644 --- a/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt +++ b/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt @@ -1,6 +1,5 @@ package app.tourism.ui.common.special -import android.text.Html import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -30,6 +29,7 @@ import app.tourism.ui.common.LoadImg import app.tourism.ui.theme.HeartRed import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getStarColor +import app.tourism.utils.getAnnotatedStringFromHtml @Composable fun PlacesItem( @@ -65,6 +65,7 @@ fun PlacesItem( verticalAlignment = Alignment.CenterVertically ) { Text( + modifier = Modifier.weight(1f, fill = true), text = place.name, style = TextStyles.h3, maxLines = 1, @@ -72,7 +73,7 @@ fun PlacesItem( ) IconButton( - modifier = Modifier.size(20.dp), + modifier = Modifier.size(28.dp), onClick = { onFavoriteChanged(!isFavorite) }, @@ -98,7 +99,7 @@ fun PlacesItem( place.excerpt?.let { Text( - text = Html.fromHtml(it).toString(), + text = it.getAnnotatedStringFromHtml(), style = TextStyles.b1, maxLines = 3, overflow = TextOverflow.Ellipsis, diff --git a/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt b/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt new file mode 100644 index 0000000000..a32571f706 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt @@ -0,0 +1,44 @@ +package app.tourism.ui.common.special + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import app.organicmaps.R +import app.tourism.ui.theme.getStarColor + +@Composable +fun RatingBar( + rating: Float, + size: Dp = 30.dp, + maxRating: Int = 5, + onRatingChanged: ((Float) -> Unit)? = null, +) { + Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + for (i in 1..maxRating) { + Icon( + modifier = Modifier + .size(size) + .clickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + onRatingChanged?.invoke(i.toFloat()) + }, + ), + painter = + painterResource(id = if (i <= rating) R.drawable.star else R.drawable.star_border), + contentDescription = null, + tint = getStarColor() + ) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt index ed961ea317..248c19bc5d 100644 --- a/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt +++ b/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt @@ -5,13 +5,10 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.organicmaps.R import app.tourism.ui.theme.TextStyles @Composable @@ -21,7 +18,8 @@ fun AppEditText( hint: String = "", isError: () -> Boolean = { false }, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default + keyboardActions: KeyboardActions = KeyboardActions.Default, + maxLines: Int = 1, ) { EditText( value = value, @@ -42,6 +40,7 @@ fun AppEditText( cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), focusedColor = MaterialTheme.colorScheme.onBackground, unfocusedColor = MaterialTheme.colorScheme.onBackground, - errorColor = MaterialTheme.colorScheme.onError + errorColor = MaterialTheme.colorScheme.onError, + maxLines = maxLines, ) } diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt index 8b5265667c..18255bebf4 100644 --- a/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt +++ b/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateOffsetAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -74,22 +75,25 @@ fun EditText( val hintCondition = etState == EtState.Unfocused && value.isEmpty() val hintOffset by animateOffsetAsState( - targetValue = if (hintCondition) Offset(0f, 0f) - else Offset(0f, -(hintFontSizeInt * 1.3f)) + targetValue = if (hintCondition) Offset(0f, hintFontSizeInt * 1.6f) + else Offset(0f, 0f) ) val hintSize by animateIntAsState( targetValue = if (hintCondition) hintFontSizeInt else (hintFontSizeInt * 0.8).roundToInt() ) + val heightModifier = + if (maxLines > 1) Modifier.height(IntrinsicSize.Min) else Modifier.height(textFieldHeight) + Column(modifier) { BasicTextField( modifier = Modifier - .height(textFieldHeight) .padding(textFieldPadding) .onFocusChanged { etState = if (it.hasFocus) EtState.Focused else EtState.Unfocused } - .fillMaxWidth(), + .fillMaxWidth() + .then(heightModifier), value = value, onValueChange = { onValueChange(it) @@ -107,11 +111,7 @@ fun EditText( decorationBox = { Row { leadingIcon?.invoke() - Box( - Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomStart - ) { + Column(Modifier.fillMaxSize()) { Text( modifier = Modifier.offset(hintOffset.x.dp, hintOffset.y.dp), text = hint, @@ -119,7 +119,7 @@ fun EditText( color = hintColor, ) it() - Box(Modifier.align(Alignment.CenterEnd)) { + Box(Modifier.align(Alignment.End)) { trailingIcon?.invoke() } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt index 7269f2ff20..40e2b2902e 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt @@ -35,7 +35,7 @@ fun AuthNavigation() { val context = LocalContext.current val navController = rememberNavController() - val navigateUp = { navController.navigateUp() } + val navigateUp: () -> Unit = { navController.navigateUp() } NavHost( navController = navController, diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt index 188c3c9828..cf8575e728 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt @@ -39,7 +39,7 @@ import app.tourism.ui.utils.showToast @Composable fun SignInScreen( onSignInComplete: () -> Unit, - onBackClick: () -> Boolean, + onBackClick: () -> Unit, vm: SignInViewModel = hiltViewModel(), ) { val context = LocalContext.current diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt index b04895d7fd..d4c52b241e 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt @@ -43,7 +43,7 @@ import com.hbb20.CountryCodePicker @Composable fun SignUpScreen( onSignUpComplete: () -> Unit, - onBackClick: () -> Boolean, + onBackClick: () -> Unit, vm: SignUpViewModel = hiltViewModel(), ) { val context = LocalContext.current diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/welcome/WelcomeScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/welcome/WelcomeScreen.kt index 547e95da03..829acdbcae 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/welcome/WelcomeScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/welcome/WelcomeScreen.kt @@ -13,9 +13,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource diff --git a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt index 63ceca9899..77fb7666bc 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt @@ -20,7 +20,7 @@ import app.tourism.utils.changeSystemAppLanguage @Composable fun LanguageScreen( - onBackClick: () -> Boolean, + onBackClick: () -> Unit, vm: LanguageViewModel = hiltViewModel() ) { val context = LocalContext.current @@ -30,7 +30,9 @@ fun LanguageScreen( topBar = { AppTopBar( title = stringResource(id = R.string.chose_language), - onBackClick = onBackClick + onBackClick = { + onBackClick() + } ) }, containerColor = MaterialTheme.colorScheme.background, @@ -40,7 +42,7 @@ fun LanguageScreen( VerticalSpace(height = 16.dp) SingleChoiceCheckBoxes( itemNames = languages.map { it.name }, - selectedItemName = if(selectedLanguage != null) selectedLanguage?.name else null, + selectedItemName = if (selectedLanguage != null) selectedLanguage?.name else null, onItemChecked = { name -> val language = languages.first { it.name == name } vm.updateLanguage(language) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt index 110a063b47..0c1dc139b0 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt @@ -2,7 +2,6 @@ package app.tourism.ui.screens.main import android.content.Context import android.content.Intent -import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat @@ -21,10 +20,10 @@ import app.tourism.ui.screens.main.categories.categories.CategoriesViewModel import app.tourism.ui.screens.main.favorites.favorites.FavoritesScreen import app.tourism.ui.screens.main.home.home.HomeScreen import app.tourism.ui.screens.main.home.search.SearchScreen +import app.tourism.ui.screens.main.place_details.PlaceDetailsScreen import app.tourism.ui.screens.main.profile.personal_data.PersonalDataScreen import app.tourism.ui.screens.main.profile.profile.ProfileScreen import app.tourism.ui.screens.main.profile.profile.ProfileViewModel -import app.tourism.ui.screens.main.place_details.PlaceDetailsScreen import app.tourism.utils.navigateToMap import app.tourism.utils.navigateToMapForRoute import kotlinx.serialization.Serializable @@ -53,7 +52,7 @@ object PersonalData // place details @Serializable -data class PlaceDetails(val id: Int) +data class PlaceDetails(val id: Long) @Composable fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel) { @@ -61,7 +60,7 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel val categoriesVM: CategoriesViewModel = hiltViewModel() - val onPlaceClick: (id: Int) -> Unit = { id -> + val onPlaceClick: (id: Long) -> Unit = { id -> rootNavController.navigate(PlaceDetails(id = id)) } val onMapClick = { navigateToMap(context) } @@ -108,7 +107,7 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel @Composable fun HomeNavHost( - onPlaceClick: (id: Int) -> Unit, + onPlaceClick: (id: Long) -> Unit, onMapClick: () -> Unit, onCategoryClicked: () -> Unit, categoriesVM: CategoriesViewModel, @@ -139,7 +138,7 @@ fun HomeNavHost( @Composable fun CategoriesNavHost( - onPlaceClick: (id: Int) -> Unit, + onPlaceClick: (id: Long) -> Unit, onMapClick: () -> Unit, categoriesVM: CategoriesViewModel, ) { @@ -152,7 +151,7 @@ fun CategoriesNavHost( } @Composable -fun FavoritesNavHost(onPlaceClick: (id: Int) -> Unit) { +fun FavoritesNavHost(onPlaceClick: (id: Long) -> Unit) { val favoritesNavController = rememberNavController() NavHost(favoritesNavController, startDestination = Favorites) { composable { @@ -165,7 +164,7 @@ fun FavoritesNavHost(onPlaceClick: (id: Int) -> Unit) { fun ProfileNavHost(themeVM: ThemeViewModel, profileVM: ProfileViewModel = hiltViewModel()) { val context = LocalContext.current val profileNavController = rememberNavController() - val onBackClick = { profileNavController.navigateUp() } + val onBackClick: () -> Unit = { profileNavController.navigateUp() } NavHost(profileNavController, startDestination = Profile) { composable { diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt b/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt index c925c8cadf..a6aca97042 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt @@ -46,57 +46,61 @@ fun MainSection(themeVM: ThemeViewModel) { MainNavigation(rootNavController = rootNavController, themeVM = themeVM) Column(modifier = Modifier.align(alignment = Alignment.BottomCenter)) { - NavigationBar( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(shape = RoundedCornerShape(50.dp)), - containerColor = MaterialTheme.colorScheme.primary, - windowInsets = WindowInsets( - left = 24.dp, - right = 24.dp, - bottom = 0.dp, - top = 0.dp - ) - ) { - items.forEach { item -> - val isSelected = item.route == navBackStackEntry?.destination?.route - val title = stringResource(id = item.title) - NavigationBarItem( - colors = NavigationBarItemColors( - disabledIconColor = MaterialTheme.colorScheme.onPrimary, - disabledTextColor = MaterialTheme.colorScheme.onPrimary, - selectedIconColor = MaterialTheme.colorScheme.onPrimary, - selectedTextColor = MaterialTheme.colorScheme.onPrimary, - unselectedIconColor = MaterialTheme.colorScheme.onPrimary, - unselectedTextColor = MaterialTheme.colorScheme.onPrimary, - selectedIndicatorColor = Color.Transparent, - ), - selected = isSelected, - - label = { - Text(text = title, style = TextStyles.b3) - }, - icon = { - Icon( - painter = painterResource( - if (isSelected) item.selectedIcon else item.unselectedIcon - ), - contentDescription = title, - ) - }, - onClick = { - rootNavController.navigate(item.route) { - popUpTo(rootNavController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } + val destination = + rootNavController.currentBackStackEntryAsState().value?.destination + val isCurrentATopScreen = items.any { it.route == destination?.route } + if (isCurrentATopScreen) + NavigationBar( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .clip(shape = RoundedCornerShape(50.dp)), + containerColor = MaterialTheme.colorScheme.primary, + windowInsets = WindowInsets( + left = 24.dp, + right = 24.dp, + bottom = 0.dp, + top = 0.dp ) + ) { + items.forEach { item -> + val isSelected = item.route == navBackStackEntry?.destination?.route + val title = stringResource(id = item.title) + NavigationBarItem( + colors = NavigationBarItemColors( + disabledIconColor = MaterialTheme.colorScheme.onPrimary, + disabledTextColor = MaterialTheme.colorScheme.onPrimary, + selectedIconColor = MaterialTheme.colorScheme.onPrimary, + selectedTextColor = MaterialTheme.colorScheme.onPrimary, + unselectedIconColor = MaterialTheme.colorScheme.onPrimary, + unselectedTextColor = MaterialTheme.colorScheme.onPrimary, + selectedIndicatorColor = Color.Transparent, + ), + selected = isSelected, + + label = { + Text(text = title, style = TextStyles.b3) + }, + icon = { + Icon( + painter = painterResource( + if (isSelected) item.selectedIcon else item.unselectedIcon + ), + contentDescription = title, + ) + }, + onClick = { + rootNavController.navigate(item.route) { + popUpTo(rootNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } } - } VerticalSpace(height = 0.dp) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt index 7b60c20457..a47ceadfa4 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt @@ -5,14 +5,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -27,7 +24,7 @@ import app.tourism.ui.common.special.PlacesItem @Composable fun CategoriesScreen( - onPlaceClick: (id: Int) -> Unit, + onPlaceClick: (id: Long) -> Unit, onMapClick: () -> Unit, categoriesVM: CategoriesViewModel = hiltViewModel() ) { diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt index ba214141a6..9f306c0329 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt @@ -53,6 +53,7 @@ class CategoriesViewModel @Inject constructor( fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { // todo } + init { // todo replace with real data _selectedCategory.value = SingleChoiceItem("sights", "Sights") @@ -65,7 +66,7 @@ class CategoriesViewModel @Inject constructor( repeat(15) { dummyData.add( PlaceShort( - id = it, + id = it.toLong(), name = "Гора Эмина", pic = Constants.IMAGE_URL_EXAMPLE, rating = 5.0, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt index 6d9ab12fc4..8a757a67de 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt @@ -2,16 +2,15 @@ package app.tourism.ui.screens.main.categories.categories import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import app.tourism.applyAppBorder import app.tourism.ui.common.HorizontalSpace @@ -23,16 +22,22 @@ fun HorizontalSingleChoice( modifier: Modifier = Modifier, items: List, selected: SingleChoiceItem?, - onSelectedChanged: (SingleChoiceItem) -> Unit + onSelectedChanged: (SingleChoiceItem) -> Unit, + selectedColor: Color = MaterialTheme.colorScheme.surface, + unselectedColor: Color = MaterialTheme.colorScheme.background, + itemModifier: Modifier = Modifier, ) { Row(Modifier.then(modifier)) { items.forEach { SingleChoiceItem( + modifier = itemModifier, item = it, isSelected = it.key == selected?.key, onClick = { onSelectedChanged(it) }, + selectedColor = selectedColor, + unselectedColor = unselectedColor ) HorizontalSpace(width = 12.dp) } @@ -44,7 +49,9 @@ private fun SingleChoiceItem( modifier: Modifier = Modifier, item: SingleChoiceItem, isSelected: Boolean, - onClick: () -> Unit + onClick: () -> Unit, + selectedColor: Color = MaterialTheme.colorScheme.surface, + unselectedColor: Color = MaterialTheme.colorScheme.background, ) { val shape = RoundedCornerShape(16.dp) Text( @@ -55,8 +62,8 @@ private fun SingleChoiceItem( } .clip(shape) .background( - color = if (isSelected) MaterialTheme.colorScheme.surface - else MaterialTheme.colorScheme.background, + color = if (isSelected) selectedColor + else unselectedColor, shape = shape ) .padding(12.dp) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt index 325373f665..d12e0865ae 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt @@ -2,36 +2,106 @@ package app.tourism.ui.screens.main.favorites.favorites import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import app.organicmaps.R +import app.tourism.Constants +import app.tourism.ui.common.SpaceForNavBar +import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.nav.AppTopBar +import app.tourism.ui.common.nav.SearchTopBar import app.tourism.ui.common.nav.TopBarActionData +import app.tourism.ui.common.special.PlacesItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun FavoritesScreen( - onPlaceClick: (id: Int) -> Unit, + onPlaceClick: (id: Long) -> Unit, + favoritesVM: FavoritesViewModel = hiltViewModel() ) { + val scope = rememberCoroutineScope() + + var isSearchActive by remember { mutableStateOf(false) } + val focusRequester = remember { FocusRequester() } + + val query = favoritesVM.query.collectAsState().value + val places = favoritesVM.places.collectAsState().value + Scaffold( topBar = { - AppTopBar( - title = stringResource(id = R.string.favorites), - actions = listOf( - TopBarActionData( - iconDrawable = R.drawable.search, - onClick = { - // todo - } + if (isSearchActive) { + Column { + SearchTopBar( + modifier = Modifier.focusRequester(focusRequester), + query = query, + onQueryChanged = { favoritesVM.setQuery(it) }, + onSearchClicked = { favoritesVM.search(it) }, + onClearClicked = { favoritesVM.clearSearchField() }, + onBackClicked = { isSearchActive = false }, + ) + } + } else { + AppTopBar( + title = stringResource(id = R.string.favorites), + actions = listOf( + TopBarActionData( + iconDrawable = R.drawable.search, + onClick = { + isSearchActive = true + scope.launch(context = Dispatchers.Main) { + /*This delay is here so our textfield would first become enabled for editing + and only then it should get receive focus*/ + delay(100L) + focusRequester.requestFocus() + } + } + ), ), - ), - ) - } + ) + } + }, + contentWindowInsets = Constants.USUAL_WINDOW_INSETS ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - // todo + LazyColumn(Modifier.padding(paddingValues)) { + item { + VerticalSpace(16.dp) + } + items(places) { item -> + Column { + PlacesItem( + place = item, + onPlaceClick = { onPlaceClick(item.id) }, + isFavorite = item.isFavorite, + onFavoriteChanged = { isFavorite -> + favoritesVM.setFavoriteChanged(item, isFavorite) + }, + ) + VerticalSpace(height = 16.dp) + } + } + + item { + Column { + SpaceForNavBar() + } + } } } } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt new file mode 100644 index 0000000000..9958827839 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt @@ -0,0 +1,64 @@ +package app.tourism.ui.screens.main.favorites.favorites + +import androidx.lifecycle.ViewModel +import app.tourism.Constants +import app.tourism.domain.models.common.PlaceShort +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class FavoritesViewModel @Inject constructor( +) : ViewModel() { + private val uiChannel = Channel() + val uiEventsChannelFlow = uiChannel.receiveAsFlow() + + // region search query + private val _query = MutableStateFlow("") + val query = _query.asStateFlow() + + fun setQuery(value: String) { + _query.value = value + } + + fun search(value: String) { + // todo + } + + fun clearSearchField() { + _query.value = "" + } + // endregion search query + + private val _places = MutableStateFlow>(emptyList()) + val places = _places.asStateFlow() + + fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { + // todo + } + + init { + // todo replace with real data + val dummyData = mutableListOf() + repeat(15) { + dummyData.add( + PlaceShort( + id = it.toLong(), + name = "Гиссарская крепость", + pic = Constants.IMAGE_URL_EXAMPLE, + rating = 5.0, + excerpt = "завтрак включен, бассейн, сауна, с видом на озеро" + ) + ) + } + _places.update { dummyData } + } +} + +sealed interface UiEvent { + data class ShowToast(val message: String) : UiEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt index ae1743843d..7baf317dda 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -60,7 +59,7 @@ import app.tourism.ui.theme.getStarColor @Composable fun HomeScreen( onSearchClick: (String) -> Unit, - onPlaceClick: (id: Int) -> Unit, + onPlaceClick: (id: Long) -> Unit, onMapClick: () -> Unit, onCategoryClicked: () -> Unit, homeVM: HomeViewModel = hiltViewModel(), diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt index 6599f04202..e858424254 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt @@ -50,7 +50,7 @@ class HomeViewModel @Inject constructor( repeat(15) { dummyData.add( PlaceShort( - id = it, + id = it.toLong(), name = "Гора Эмина", pic = Constants.IMAGE_URL_EXAMPLE, rating = 5.0, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt index 737ea0eacf..f58aa15847 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt @@ -29,7 +29,7 @@ import app.tourism.ui.theme.TextStyles @OptIn(ExperimentalFoundationApi::class) @Composable fun SearchScreen( - onPlaceClick: (id: Int) -> Unit, + onPlaceClick: (id: Long) -> Unit, onMapClick: () -> Unit, queryArg: String, searchVM: SearchViewModel = hiltViewModel() diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt index 1b38f6df07..f6da2d57f7 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt @@ -50,7 +50,7 @@ class SearchViewModel @Inject constructor( repeat(15) { dummyData.add( PlaceShort( - id = it, + id = it.toLong(), name = "Гиссарская крепость", pic = Constants.IMAGE_URL_EXAMPLE, rating = 5.0, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt index aafa0c2342..878a11c69f 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt @@ -1,34 +1,100 @@ package app.tourism.ui.screens.main.place_details +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import app.organicmaps.R +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import app.tourism.Constants import app.tourism.data.dto.PlaceLocation -import app.tourism.ui.common.nav.AppTopBar +import app.tourism.ui.common.VerticalSpace +import app.tourism.ui.common.nav.PlaceTopBar +import app.tourism.ui.screens.main.place_details.description.DescriptionScreen +import app.tourism.ui.screens.main.place_details.gallery.GalleryNavigation +import app.tourism.ui.screens.main.place_details.reviews.ReviewsNavigation +import kotlinx.coroutines.launch @Composable fun PlaceDetailsScreen( - id: Int, - onBackClick: () -> Boolean, + id: Long, + onBackClick: () -> Unit, onMapClick: () -> Unit, onCreateRoute: (PlaceLocation) -> Unit, + placeVM: PlaceViewModel = hiltViewModel() ) { + val scope = rememberCoroutineScope() + + val place = placeVM.place.collectAsState().value + Scaffold( topBar = { - AppTopBar( - title = stringResource(id = R.string.profile_tourism), - onBackClick = onBackClick, - ) - } + place?.let { + PlaceTopBar( + title = it.name, + picUrl = it.pic, + isFavorite = it.isFavorite, + onFavoriteChanged = { placeVM.setFavoriteChanged(id, it) }, + onMapClick = onMapClick, + onBackClick = onBackClick, + ) + } + }, + contentWindowInsets = WindowInsets.statusBars ) { paddingValues -> - Column(Modifier.padding(paddingValues)) { - // todo - Text("id: $id") + place?.let { + Column(Modifier.padding(paddingValues)) { + val pagerState = rememberPagerState(pageCount = { 3 }) + + VerticalSpace(height = 16.dp) + Box(modifier = Modifier.padding(horizontal = Constants.SCREEN_PADDING)){ + PlaceTabRow( + tabIndex = pagerState.currentPage, + onTabIndexChanged = { + scope.launch { + pagerState.scrollToPage(it) + } + }, + ) + } + + + HorizontalPager( + modifier = Modifier.fillMaxSize(), + state = pagerState, + verticalAlignment = Alignment.Top, + ) { page -> + when (page) { + 0 -> { + DescriptionScreen( + description = place.description, + onCreateRoute = { + place.placeLocation?.let { onCreateRoute(it) } + }, + ) + } + + 1 -> { + GalleryNavigation(urls = place.pics) + } + + 2 -> { + ReviewsNavigation(placeId = place.id, rating = place.rating) + } + } + } + } } } -} \ No newline at end of file +} + diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt new file mode 100644 index 0000000000..f3021aaf7f --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt @@ -0,0 +1,77 @@ +package app.tourism.ui.screens.main.place_details + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.organicmaps.R +import app.tourism.ui.models.SingleChoiceItem +import app.tourism.ui.theme.TextStyles + +@Composable +fun PlaceTabRow(modifier: Modifier = Modifier, tabIndex: Int, onTabIndexChanged: (Int) -> Unit) { + val tabs = listOf( + SingleChoiceItem("0", stringResource(id = R.string.description_tourism)), + SingleChoiceItem("1", stringResource(id = R.string.gallery)), + SingleChoiceItem("2", stringResource(id = R.string.reviews)), + ) + + val shape = RoundedCornerShape(50.dp) + + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface, shape) + .then(modifier) + .padding(8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + tabs.forEach { + SingleChoiceItem( + item = it, + isSelected = it.key?.toInt() == tabIndex, + onClick = { + val key = it.key?.toInt() + if (key != null) { + onTabIndexChanged(key) + } + }, + ) + } + } +} + +@Composable +private fun SingleChoiceItem( + item: SingleChoiceItem, + isSelected: Boolean, + onClick: () -> Unit, +) { + val shape = RoundedCornerShape(50.dp) + Text( + modifier = Modifier + .wrapContentSize() + .clip(shape) + .clickable { onClick() } + .background( + color = if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) + else MaterialTheme.colorScheme.surface, + shape = shape + ) + .padding(8.dp), + text = item.label, + style = TextStyles.b1, + maxLines = 1 + ) +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt new file mode 100644 index 0000000000..080b1c828f --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt @@ -0,0 +1,74 @@ +package app.tourism.ui.screens.main.place_details + +import androidx.lifecycle.ViewModel +import app.tourism.Constants +import app.tourism.data.dto.PlaceLocation +import app.tourism.domain.models.details.PlaceFull +import app.tourism.utils.makeLongListOfTheSameItem +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class PlaceViewModel @Inject constructor( +) : ViewModel() { + private val uiChannel = Channel() + val uiEventsChannelFlow = uiChannel.receiveAsFlow() + + private val _place = MutableStateFlow(null) + val place = _place.asStateFlow() + + fun setFavoriteChanged(itemId: Long, isFavorite: Boolean) { + // todo + } + + init { + //todo replace with real data + val galleryPics = makeLongListOfTheSameItem(Constants.IMAGE_URL_EXAMPLE, 15).toMutableList() + galleryPics.add(Constants.THUMBNAIL_URL_EXAMPLE) + galleryPics.add(Constants.THUMBNAIL_URL_EXAMPLE) + galleryPics.add(Constants.IMAGE_URL_EXAMPLE) + _place.update { + PlaceFull( + id = 1, + name = "Гора Эмина", + rating = 5.0, + pic = Constants.IMAGE_URL_EXAMPLE, + excerpt = null, + description = htmlExample, + placeLocation = PlaceLocation(name = "Гора Эмина", lat = 38.579, lon = 68.782), + pics = galleryPics, + ) + } + } +} + +sealed interface UiEvent { + data class ShowToast(val message: String) : UiEvent +} + +val htmlExample = + """ + + + + + + +

Гиссарская крепость

+

⭐️ 4,8 работает каждый день, с 8:00 по 17:00

+

О месте

+

Город республиканского подчинения в западной части Таджикистана, в 20 километрах от столицы.

+

Город славится историческими достопримечательностями, например, Гиссарской крепостью, которая считается одним из самых известных исторических сооружений в Центральной Азии.

+

Адрес

+

районы республиканского подчинения, город Гиссар с административной территорией, джамоат Хисор, село Гиссар

+

Контакты

+

+992 998 201 201

+ + + + """.trimIndent() \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/description/Description.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/description/Description.kt new file mode 100644 index 0000000000..b97d2c6416 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/description/Description.kt @@ -0,0 +1,50 @@ +package app.tourism.ui.screens.main.place_details.description + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.organicmaps.R +import app.tourism.Constants +import app.tourism.ui.common.VerticalSpace +import app.tourism.ui.common.WebView +import app.tourism.ui.common.buttons.PrimaryButton + +@Composable +fun DescriptionScreen( + description: String?, + onCreateRoute: (() -> Unit)?, +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = Constants.SCREEN_PADDING) + ) { + description?.let { + Column(Modifier.verticalScroll(rememberScrollState())) { + VerticalSpace(height = 16.dp) + WebView(data = it) + VerticalSpace(height = 100.dp) + } + } + + onCreateRoute?.let { + PrimaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .offset(y = (-32).dp), + label = stringResource(id = R.string.show_route), + onClick = { onCreateRoute() }, + ) + } + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt new file mode 100644 index 0000000000..a8617cf76a --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/AllGalleryScreen.kt @@ -0,0 +1,38 @@ +package app.tourism.ui.screens.main.place_details.gallery + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.tourism.Constants +import app.tourism.ui.common.LoadImg +import app.tourism.ui.common.nav.BackButtonWithText + +@Composable +fun AllGalleryScreen(urls: List, onBackClick: () -> Unit) { + Scaffold( + topBar = { + BackButtonWithText { onBackClick() } + } + ) { paddingValues -> + LazyVerticalGrid( + modifier = Modifier.padding(paddingValues), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(Constants.SCREEN_PADDING), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(urls) { + LoadImg( + modifier = Modifier.propertiesForSmallImage(), url = it + ) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt new file mode 100644 index 0000000000..ea34349ece --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryNavigation.kt @@ -0,0 +1,37 @@ +package app.tourism.ui.screens.main.place_details.gallery + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import kotlinx.serialization.Serializable + +@Serializable +object Gallery + +@Serializable +object AllGallery + +@Composable +fun GalleryNavigation(urls: List) { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = Gallery) { + composable { + GalleryScreen( + urls = urls, + onMoreClick = { + navController.navigate(AllGallery) + }, + ) + } + composable { + AllGalleryScreen( + urls = urls, + onBackClick = { + navController.navigateUp() + }, + ) + } + } +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt new file mode 100644 index 0000000000..6d80d9cc2b --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/GalleryScreen.kt @@ -0,0 +1,75 @@ +package app.tourism.ui.screens.main.place_details.gallery + +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.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.unit.dp +import app.tourism.Constants +import app.tourism.ui.common.HorizontalSpace +import app.tourism.ui.common.LoadImg +import app.tourism.ui.common.VerticalSpace +import app.tourism.ui.theme.TextStyles + +@Composable +fun GalleryScreen(urls: List, onMoreClick: () -> Unit) { + Column(Modifier.padding(Constants.SCREEN_PADDING)) { + if (urls.isNotEmpty()) { + LoadImg( + modifier = Modifier + .fillMaxWidth() + .height(200.dp) + .clip(imageShape), + url = urls.first(), + ) + VerticalSpace(height = 16.dp) + + Row { + if (urls.size > 1) { + LoadImg( + modifier = Modifier + .weight(1f) + .propertiesForSmallImage(), + url = urls[1], + ) + if (urls.size > 2) { + HorizontalSpace(16.dp) + Box( + modifier = Modifier + .weight(1f) + .clickable { onMoreClick() } + .propertiesForSmallImage(), + contentAlignment = Alignment.Center + ) { + LoadImg(url = urls[2]) + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = Color.Black.copy(alpha = 0.5f), + shape = imageShape + ), + ) + Text( + text = "+${urls.size - 3}", + style = TextStyles.h1, + color = Color.White, + ) + } + } + } + } + } + } +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/Utils.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/Utils.kt new file mode 100644 index 0000000000..577846eca6 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/gallery/Utils.kt @@ -0,0 +1,16 @@ +package app.tourism.ui.screens.main.place_details.gallery + +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp + +@Composable +fun Modifier.propertiesForSmallImage() = + this + .height(150.dp) + .clip(imageShape) + +val imageShape = RoundedCornerShape(10.dp) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt new file mode 100644 index 0000000000..b540fefd7d --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/AllReviewsScreen.kt @@ -0,0 +1,39 @@ +package app.tourism.ui.screens.main.place_details.reviews + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import app.tourism.Constants +import app.tourism.ui.common.nav.BackButtonWithText +import app.tourism.ui.screens.main.place_details.reviews.components.Review + + +@Composable +fun AllReviewsScreen( + reviewsVM: ReviewsViewModel = hiltViewModel(), + onBackClick: () -> Unit, + onMoreClick: (picsUrls: List) -> Unit, +) { + val reviews = reviewsVM.reviews.collectAsState().value + + Scaffold( + topBar = { + BackButtonWithText { onBackClick() } + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier.padding(paddingValues), + contentPadding = PaddingValues(Constants.SCREEN_PADDING), + ) { + items(reviews) { + Review(review = it, onMoreClick = onMoreClick) + } + } + } +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt new file mode 100644 index 0000000000..3f89684a57 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt @@ -0,0 +1,57 @@ +package app.tourism.ui.screens.main.place_details.reviews + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import java.io.File +import javax.inject.Inject + +@HiltViewModel +class PostReviewViewModel @Inject constructor( +) : ViewModel() { + private val uiChannel = Channel() + val uiEventsChannelFlow = uiChannel.receiveAsFlow() + + private val _rating = MutableStateFlow(5f) + val rating = _rating.asStateFlow() + + fun setRating(value: Float) { + _rating.value = value + } + + + private val _comment = MutableStateFlow("") + val comment = _comment.asStateFlow() + + fun setComment(value: String) { + _comment.value = value + } + + + private val _files = MutableStateFlow>(emptyList()) + val files = _files.asStateFlow() + + fun addFile(file: File) { + _files.update { + val list = _files.value.toMutableList() + list.add(file) + list + } + } + + fun removeFile(file: File) { + _files.update { + val list = _files.value.toMutableList() + list.remove(file) + list + } + } + + fun postReview() { + + } +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt new file mode 100644 index 0000000000..cc8758222d --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewPicsScreen.kt @@ -0,0 +1,39 @@ +package app.tourism.ui.screens.main.place_details.reviews + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import app.tourism.Constants +import app.tourism.ui.common.LoadImg +import app.tourism.ui.common.nav.BackButtonWithText +import app.tourism.ui.screens.main.place_details.gallery.propertiesForSmallImage + +@Composable +fun ReviewPicsScreen(urls: List, onBackClick: () -> Unit) { + Scaffold( + topBar = { + BackButtonWithText { onBackClick() } + } + ) { paddingValues -> + LazyVerticalGrid( + modifier = Modifier.padding(paddingValues), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(Constants.SCREEN_PADDING), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(urls) { + LoadImg( + modifier = Modifier.propertiesForSmallImage(), url = it + ) + } + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt new file mode 100644 index 0000000000..f00cea8270 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt @@ -0,0 +1,60 @@ +package app.tourism.ui.screens.main.place_details.reviews + +import androidx.compose.runtime.Composable +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import kotlinx.serialization.Serializable + +@Serializable +object Reviews + +@Serializable +object AllReviews + +@Serializable +data class ReviewPics(val urls: List) + +@Composable +fun ReviewsNavigation( + placeId: Long, + rating: Double?, + reviewsVM: ReviewsViewModel = hiltViewModel(), +) { + val navController = rememberNavController() + + val onBackClick: () -> Unit = { navController.navigateUp() } + val onMoreClick: (picsUrls: List) -> Unit = { + navController.navigate(ReviewPics(urls = it)) + } + + NavHost(navController = navController, startDestination = Reviews) { + composable { + ReviewsScreen( + placeId, + rating, + onSeeAllClick = { + navController.navigate(AllReviews) + }, + onMoreClick = onMoreClick, + reviewsVM = reviewsVM, + ) + } + composable { + AllReviewsScreen( + reviewsVM = reviewsVM, + onBackClick = onBackClick, + onMoreClick = onMoreClick + ) + } + composable { navBackStackEntry -> + val reviewPics = navBackStackEntry.toRoute() + ReviewPicsScreen( + urls = reviewPics.urls, + onBackClick = onBackClick + ) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt new file mode 100644 index 0000000000..04d4c7737b --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt @@ -0,0 +1,131 @@ +package app.tourism.ui.screens.main.place_details.reviews + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import app.organicmaps.R +import app.tourism.Constants +import app.tourism.ui.common.HorizontalSpace +import app.tourism.ui.common.VerticalSpace +import app.tourism.ui.common.special.RatingBar +import app.tourism.ui.screens.main.place_details.reviews.components.PostReview +import app.tourism.ui.screens.main.place_details.reviews.components.Review +import app.tourism.ui.theme.TextStyles +import app.tourism.ui.theme.getStarColor +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ReviewsScreen( + placeId: Long, + rating: Double?, + onSeeAllClick: () -> Unit, + onMoreClick: (picsUrls: List) -> Unit, + reviewsVM: ReviewsViewModel = hiltViewModel(), +) { + val scope = rememberCoroutineScope() + + val sheetState = rememberModalBottomSheetState() + var showReviewBottomSheet by remember { mutableStateOf(false) } + + val userReview = reviewsVM.userReview.collectAsState().value + val reviews = reviewsVM.reviews.collectAsState().value + + LazyColumn( + contentPadding = PaddingValues(Constants.SCREEN_PADDING), + ) { + rating?.let { + item { + Column { + VerticalSpace(height = 16.dp) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(30.dp), + painter = painterResource(id = R.drawable.star), + contentDescription = null, + tint = getStarColor(), + ) + HorizontalSpace(width = 8.dp) + Text(text = "%.1f".format(rating) + "/5", style = TextStyles.h1) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = { + showReviewBottomSheet = true + scope.launch { + // Have to do add this delay, because bottom sheet doesn't expand fully itself + // and expands with duration after showReviewBottomSheet is set to true + delay(300L) + sheetState.expand() + } + }, + ) { + Text(text = stringResource(id = R.string.compose_review)) + } + + RatingBar(rating = it.toFloat()) + } + VerticalSpace(height = 24.dp) + + TextButton( + modifier = Modifier.align(Alignment.End), + onClick = { + onSeeAllClick() + }, + ) { + Text(text = stringResource(id = R.string.see_all)) + } + } + } + } + + userReview?.let { + item { + Review(review = userReview, onMoreClick = onMoreClick) + } + } + + items(3) { + Review(review = reviews[it], onMoreClick = onMoreClick) + } + } + + if (showReviewBottomSheet) + ModalBottomSheet( + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.background, + onDismissRequest = { showReviewBottomSheet = false }, + ) { + PostReview() + } +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt new file mode 100644 index 0000000000..4b6ca8189a --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt @@ -0,0 +1,47 @@ +package app.tourism.ui.screens.main.place_details.reviews + +import androidx.lifecycle.ViewModel +import app.tourism.Constants +import app.tourism.domain.models.details.Review +import app.tourism.domain.models.details.User +import app.tourism.utils.makeLongListOfTheSameItem +import app.tourism.utils.toUserFriendlyDate +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import javax.inject.Inject + +@HiltViewModel +class ReviewsViewModel @Inject constructor( +) : ViewModel() { + private val uiChannel = Channel() + val uiEventsChannelFlow = uiChannel.receiveAsFlow() + + private val _reviews = MutableStateFlow>(emptyList()) + val reviews = _reviews.asStateFlow() + + private val _userReview = MutableStateFlow(null) + val userReview = _userReview.asStateFlow() + + init { + //todo replace with real data + _reviews.value = makeLongListOfTheSameItem( + Review( + id = 1, + rating = 5.0, + user = User( + id = 1, + name = "Эмин Уайт", + pfpUrl = Constants.IMAGE_URL_EXAMPLE, + countryCodeName = "tj", + ), + date = "2024-06-06".toUserFriendlyDate(), + comment = "Это было прекрасное место! Мне очень понравилось, обязательно поситите это место gnjfhjgefkjgnjcsld\n" + + "Это было прекрасное место! Мне очень понравилось, обязательно поситите это место.", + picsUrls = makeLongListOfTheSameItem(Constants.IMAGE_URL_EXAMPLE, 5) + ) + ) + } +} diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/UiEvent.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/UiEvent.kt new file mode 100644 index 0000000000..2459cf729c --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/UiEvent.kt @@ -0,0 +1,6 @@ +package app.tourism.ui.screens.main.place_details.reviews + +sealed interface UiEvent { + data object CloseReviewBottomSheet : UiEvent + data class ShowToast(val message: String) : UiEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt new file mode 100644 index 0000000000..3d772f6e16 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt @@ -0,0 +1,183 @@ +package app.tourism.ui.screens.main.place_details.reviews.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import app.organicmaps.R +import app.tourism.Constants +import app.tourism.ui.common.ImagePicker +import app.tourism.ui.common.VerticalSpace +import app.tourism.ui.common.buttons.PrimaryButton +import app.tourism.ui.common.special.RatingBar +import app.tourism.ui.common.textfields.AppEditText +import app.tourism.ui.screens.main.place_details.reviews.PostReviewViewModel +import app.tourism.ui.theme.TextStyles +import app.tourism.ui.theme.getBorderColor +import app.tourism.utils.FileUtils +import coil.compose.AsyncImage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun PostReview( + modifier: Modifier = Modifier, + postReviewVM: PostReviewViewModel = hiltViewModel(), +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + + val rating = postReviewVM.rating.collectAsState().value + val comment = postReviewVM.comment.collectAsState().value + val files = postReviewVM.files.collectAsState().value + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Constants.SCREEN_PADDING) + .then(modifier), + ) { + Text(text = stringResource(id = R.string.review_title), style = TextStyles.h2) + VerticalSpace(height = 32.dp) + + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = stringResource(id = R.string.tap_to_rate), style = TextStyles.b3) + VerticalSpace(height = 4.dp) + RatingBar(rating = rating, onRatingChanged = { postReviewVM.setRating(it) }) + } + VerticalSpace(height = 16.dp) + + AppEditText( + value = comment, onValueChange = { postReviewVM.setComment(it) }, + hint = stringResource(id = R.string.text), + maxLines = 10 + ) + VerticalSpace(height = 32.dp) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + files.forEach { + ImagePreview( + model = it, + onDelete = { + postReviewVM.removeFile(it) + }, + ) + } + ImagePicker( + showPreview = false, + onSuccess = { uri -> + scope.launch(Dispatchers.IO) { + postReviewVM.addFile( + File(FileUtils(context).getPath(uri)) + ) + } + } + ) { + AddPhoto() + } + } + VerticalSpace(height = 32.dp) + + PrimaryButton( + label = stringResource(id = R.string.send), + onClick = { postReviewVM.postReview() }, + ) + VerticalSpace(height = 64.dp) + } +} + +@Composable +fun AddPhoto() { + Box( + modifier = Modifier + .getPhotoBoxProperties() + .background(color = MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + painter = painterResource(id = R.drawable.add), + contentDescription = null, + tint = MaterialTheme.colorScheme.onBackground + ) + VerticalSpace(height = 8.dp) + Text( + text = stringResource(id = R.string.upload_photo), + style = TextStyles.b2, + color = MaterialTheme.colorScheme.onBackground + ) + } + } +} + +@Composable +fun ImagePreview(model: Any?, onDelete: () -> Unit) { + Box { + AsyncImage( + modifier = Modifier + .getPhotoBoxProperties(), + model = model, + contentScale = ContentScale.Crop, + contentDescription = null + ) + + Icon( + modifier = Modifier + .size(30.dp) + .align(alignment = Alignment.TopEnd) + .clickable { onDelete() } + .offset(x = 12.dp, y = (-12).dp), + painter = painterResource(id = R.drawable.ic_route_remove), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } +} + +@Composable +fun Modifier.getPhotoBoxProperties() = + this + .width(104.dp) + .aspectRatio(1f) + .border( + width = 1.dp, + color = getBorderColor(), + shape = photoShape + ) + .clip(photoShape) + +val photoShape = RoundedCornerShape(12.dp) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt new file mode 100644 index 0000000000..a941119b25 --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt @@ -0,0 +1,218 @@ +package app.tourism.ui.screens.main.place_details.reviews.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.graphics.toArgb +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.organicmaps.R +import app.tourism.Constants +import app.tourism.domain.models.details.Review +import app.tourism.domain.models.details.User +import app.tourism.ui.common.HorizontalSpace +import app.tourism.ui.common.LoadImg +import app.tourism.ui.common.VerticalSpace +import app.tourism.ui.common.special.CountryAsLabel +import app.tourism.ui.common.special.RatingBar +import app.tourism.ui.screens.main.place_details.gallery.imageShape +import app.tourism.ui.theme.TextStyles +import app.tourism.ui.theme.getHintColor + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun Review( + modifier: Modifier = Modifier, + review: Review, + onMoreClick: (picsUrls: List) -> Unit +) { + Column { + HorizontalDivider(color = MaterialTheme.colorScheme.surface) + VerticalSpace(height = 16.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .then(modifier), + horizontalArrangement = Arrangement.SpaceBetween + ) { + User(modifier = Modifier.weight(1f), user = review.user) + review.date?.let { + Text(text = it, style = TextStyles.b2, color = getHintColor()) + } + } + VerticalSpace(height = 16.dp) + + review.rating?.let { + RatingBar( + rating = it.toFloat(), + size = 24.dp, + ) + VerticalSpace(height = 16.dp) + } + + val maxPics = 3 + val theresMore = review.picsUrls.size > maxPics + val first3pics = if (theresMore) review.picsUrls.take(3) else review.picsUrls + if (first3pics.isNotEmpty()) { + FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + first3pics.forEachIndexed { index, url -> + if (index == maxPics - 1 && theresMore) { + ShowMore( + url = url, + onClick = { + onMoreClick(review.picsUrls) + }, + remaining = review.picsUrls.size - 3 + ) + } else { + ReviewPic(url = url) + } + } + } + VerticalSpace(height = 16.dp) + } + + review.comment?.let { + Comment(comment = it) + VerticalSpace(height = 16.dp) + } + } +} + + +@Composable +fun User(modifier: Modifier = Modifier, user: User) { + Row( + modifier = Modifier + .size(66.dp) + .then(modifier), + verticalAlignment = Alignment.CenterVertically, + ) { + LoadImg( + modifier = Modifier + .fillMaxHeight() + .aspectRatio(1f) + .clip(CircleShape), + url = user.pfpUrl, + ) + HorizontalSpace(width = 8.dp) + Column { + + Text(text = user.name, style = TextStyles.h4, fontWeight = FontWeight.W600) + user.countryCodeName?.let { + CountryAsLabel( + Modifier.fillMaxWidth(), + user.countryCodeName, + contentColor = MaterialTheme.colorScheme.onBackground.toArgb(), + ) + } + } + } +} + +@Composable +fun Comment(modifier: Modifier = Modifier, comment: String) { + var expanded by remember { mutableStateOf(false) } + + val shape = RoundedCornerShape(20.dp) + val onClick = { expanded = !expanded } + Column( + Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.surface, shape = shape) + .clip(shape) + .clickable { onClick() } + .padding( + start = Constants.SCREEN_PADDING, + end = Constants.SCREEN_PADDING, + top = Constants.SCREEN_PADDING, + ) + .then(modifier), + ) { + Text( + text = comment, + style = TextStyles.h4.copy(fontWeight = FontWeight.W400), + maxLines = if (expanded) 6969 else 2, + overflow = TextOverflow.Ellipsis, + ) + TextButton(onClick = { onClick() }, contentPadding = PaddingValues(0.dp)) { + Text(text = stringResource(id = if (expanded) R.string.less else R.string.more)) + } + } +} + + +@Composable +fun ReviewPic(modifier: Modifier = Modifier, url: String) { + LoadImg( + modifier = Modifier + .width(73.dp) + .height(65.dp) + .clip(RoundedCornerShape(4.dp)) + .then(modifier), + url = url, + ) +} + +@Composable +fun ShowMore(url: String, onClick: () -> Unit, remaining: Int) { + Box( + modifier = Modifier + .clickable { onClick() } + .getImageProperties(), + contentAlignment = Alignment.Center + ) { + ReviewPic(url = url) + Box( + modifier = Modifier + .fillMaxSize() + .background( + color = Color.Black.copy(alpha = 0.5f), + shape = imageShape + ), + ) + Text( + text = "+$remaining", + style = TextStyles.h3, + color = Color.White, + ) + } +} + +@Composable +fun Modifier.getImageProperties() = + this + .width(73.dp) + .height(65.dp) + .clip(RoundedCornerShape(4.dp)) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt index 7fbe4abd01..6b04cb5144 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt @@ -58,7 +58,7 @@ import kotlinx.coroutines.launch import java.io.File @Composable -fun PersonalDataScreen(onBackClick: () -> Boolean, profileVM: ProfileViewModel) { +fun PersonalDataScreen(onBackClick: () -> Unit, profileVM: ProfileViewModel) { val context = LocalContext.current val focusManager = LocalFocusManager.current val coroutineScope = rememberCoroutineScope() diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt index e1df1d0dd1..e889835937 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt @@ -1,6 +1,5 @@ package app.tourism.ui.screens.main.profile.profile -import android.view.LayoutInflater import androidx.annotation.DrawableRes import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -37,7 +36,6 @@ 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.viewinterop.AndroidView import app.organicmaps.R import app.tourism.Constants import app.tourism.applyAppBorder @@ -52,12 +50,12 @@ import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.buttons.PrimaryButton import app.tourism.ui.common.buttons.SecondaryButton import app.tourism.ui.common.nav.AppTopBar +import app.tourism.ui.common.special.CountryAsLabel import app.tourism.ui.common.ui_state.Loading import app.tourism.ui.screens.main.ThemeViewModel import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getBorderColor import app.tourism.ui.utils.showToast -import com.hbb20.CountryCodePicker @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -160,7 +158,7 @@ fun ProfileBar(personalData: PersonalData) { HorizontalSpace(width = 16.dp) Column { Text(text = personalData.fullName, style = TextStyles.h2) - Country( + CountryAsLabel( Modifier.fillMaxWidth(), personalData.country, contentColor = MaterialTheme.colorScheme.onBackground.toArgb(), @@ -169,23 +167,6 @@ fun ProfileBar(personalData: PersonalData) { } } -@Composable -fun Country(modifier: Modifier = Modifier, countryCodeName: String, contentColor: Int) { - AndroidView( - modifier = Modifier.then(modifier), - factory = { context -> - val view = LayoutInflater.from(context) - .inflate(R.layout.ccp_as_country_label, null, false) - val ccp = view.findViewById(R.id.ccp) - ccp.contentColor = contentColor - ccp.setCountryForNameCode(countryCodeName) - ccp.showArrow(false) - ccp.setCcpClickable(false) - view - } - ) -} - @Composable fun CurrencyRates(modifier: Modifier = Modifier, currencyRates: CurrencyRates) { // todo diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt index a42a8636c3..4506732dce 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt @@ -1,6 +1,5 @@ package app.tourism.ui.screens.main.profile.profile -import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.tourism.data.prefs.UserPreferences diff --git a/android/app/src/main/java/app/tourism/ui/theme/Color.kt b/android/app/src/main/java/app/tourism/ui/theme/Color.kt index 4443927abb..545365d996 100644 --- a/android/app/src/main/java/app/tourism/ui/theme/Color.kt +++ b/android/app/src/main/java/app/tourism/ui/theme/Color.kt @@ -2,7 +2,6 @@ package app.tourism.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color val Blue = Color(0xFF0688E7) diff --git a/android/app/src/main/java/app/tourism/utils/DateUtils.kt b/android/app/src/main/java/app/tourism/utils/DateUtils.kt new file mode 100644 index 0000000000..e9b32e6982 --- /dev/null +++ b/android/app/src/main/java/app/tourism/utils/DateUtils.kt @@ -0,0 +1,35 @@ +package app.tourism.utils + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +fun String.toUserFriendlyDate(dateFormat: String = "yyyy-MM-dd"): String { + var userFriendlyDate = "" + + val currentLocale = Locale.getDefault() + val formatter = SimpleDateFormat(dateFormat, currentLocale) + + var date: Date? = null + + try { + date = formatter.parse(this) + } catch (e: ParseException) { + userFriendlyDate = this + } + val givenDate = Calendar.getInstance() + + if (date != null) { + givenDate.time = date + + givenDate.isLenient = false + val givenDay = givenDate.get(Calendar.DAY_OF_MONTH) + val givenMonth = givenDate.getDisplayName(Calendar.MONTH, Calendar.LONG, currentLocale) + val givenYear = givenDate.get(Calendar.YEAR) + + userFriendlyDate = "$givenDay $givenMonth $givenYear" + } + return userFriendlyDate +} diff --git a/android/app/src/main/java/app/tourism/utils/FakeDataUtils.kt b/android/app/src/main/java/app/tourism/utils/FakeDataUtils.kt new file mode 100644 index 0000000000..04bc020263 --- /dev/null +++ b/android/app/src/main/java/app/tourism/utils/FakeDataUtils.kt @@ -0,0 +1,7 @@ +package app.tourism.utils + +fun makeLongListOfTheSameItem(item: T, itemsNum: Int = 20): List { + val list = mutableListOf() + repeat(itemsNum) { list.add(item) } + return list +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/utils/HtmlUtils.kt b/android/app/src/main/java/app/tourism/utils/HtmlUtils.kt new file mode 100644 index 0000000000..e9e1c75aa3 --- /dev/null +++ b/android/app/src/main/java/app/tourism/utils/HtmlUtils.kt @@ -0,0 +1,57 @@ +package app.tourism.utils + +import android.graphics.Typeface +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.text.style.UnderlineSpan +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration + +fun String.getAnnotatedStringFromHtml(): AnnotatedString { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + Html.fromHtml(this, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE).toAnnotatedString() + } else { + Html.fromHtml(this).toAnnotatedString() + } +} + +fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString { + val spanned = this@toAnnotatedString + append(spanned.toString()) + getSpans(0, spanned.length, Any::class.java).forEach { span -> + val start = getSpanStart(span) + val end = getSpanEnd(span) + when (span) { + is StyleSpan -> when (span.style) { + Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) + Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) + Typeface.BOLD_ITALIC -> addStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Italic + ), start, end + ) + } + + is UnderlineSpan -> addStyle( + SpanStyle(textDecoration = TextDecoration.Underline), + start, + end + ) + + is ForegroundColorSpan -> addStyle( + SpanStyle(color = Color(span.foregroundColor)), + start, + end + ) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/star.xml b/android/app/src/main/res/drawable/star.xml index 04797ed095..62c9b1f4f7 100644 --- a/android/app/src/main/res/drawable/star.xml +++ b/android/app/src/main/res/drawable/star.xml @@ -1,9 +1,9 @@ + android:width="30dp" + android:height="26dp" + android:viewportWidth="30" + android:viewportHeight="26"> + android:pathData="M29.244,11.899L23.213,16.574L25.051,23.565C25.152,23.944 25.126,24.342 24.976,24.709C24.826,25.075 24.558,25.393 24.206,25.622C23.855,25.852 23.436,25.983 23.002,25.998C22.568,26.014 22.138,25.914 21.768,25.71L15,21.969L8.229,25.71C7.858,25.913 7.429,26.012 6.996,25.996C6.563,25.979 6.144,25.848 5.794,25.619C5.443,25.39 5.176,25.072 5.026,24.707C4.876,24.341 4.849,23.944 4.95,23.565L6.794,16.574L0.763,11.899C0.436,11.644 0.198,11.309 0.082,10.934C-0.035,10.559 -0.026,10.161 0.107,9.791C0.24,9.42 0.492,9.093 0.831,8.85C1.17,8.608 1.581,8.46 2.012,8.426L9.919,7.853L12.969,1.222C13.134,0.86 13.415,0.551 13.776,0.334C14.137,0.116 14.562,0 14.997,0C15.432,0 15.857,0.116 16.218,0.334C16.579,0.551 16.86,0.86 17.025,1.222L20.074,7.853L27.98,8.426C28.413,8.459 28.825,8.606 29.165,8.848C29.505,9.09 29.758,9.418 29.892,9.789C30.026,10.16 30.035,10.558 29.919,10.933C29.802,11.309 29.564,11.646 29.236,11.9L29.244,11.899Z" + android:fillColor="#F9C236"/> diff --git a/android/app/src/main/res/drawable/star_border.xml b/android/app/src/main/res/drawable/star_border.xml new file mode 100644 index 0000000000..8a774dd6a4 --- /dev/null +++ b/android/app/src/main/res/drawable/star_border.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/app/src/main/res/layout/ccp_as_country_label.xml b/android/app/src/main/res/layout/ccp_as_country_label.xml index 28b17c10f5..64a21013c8 100644 --- a/android/app/src/main/res/layout/ccp_as_country_label.xml +++ b/android/app/src/main/res/layout/ccp_as_country_label.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingVertical="12dp" + app:ccp_textSize="15sp" app:ccp_showArrow="false" app:ccp_autoDetectLanguage="true" app:ccp_textGravity="LEFT" diff --git a/android/app/src/main/res/layout/ccp_profile.xml b/android/app/src/main/res/layout/ccp_profile.xml index 27460be0a8..c85d74e33a 100644 --- a/android/app/src/main/res/layout/ccp_profile.xml +++ b/android/app/src/main/res/layout/ccp_profile.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingVertical="0dp" + app:ccp_textSize="17sp" app:ccp_autoDetectLanguage="true" app:ccp_textGravity="LEFT" app:ccp_padding="0dp" diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 3c7e1e92ca..5f59741dff 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -2183,8 +2183,9 @@ Фотогаллерея Отзывы Оставить отзыв - Посмотреть все + Все отзывы Развернуть + Свернуть Отзыв Нажмите, чтобы оценить: Текст @@ -2215,4 +2216,5 @@ Рестораны Отели Добавить в избранное + Посмотреть маршрут diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8d8e243830..480b51ee84 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,2258 +2,2260 @@ - - - Back - - Cancel - - Delete - Download Maps - - Download has failed. Touch to try again. - - Downloading… - - Kilometers - - MB - GB - - Miles - - My Position - - Later - - Search - - Search Map - - You currently have all Location Services for this device or application disabled. Please enable them in Settings. - - Show on the map - - Download has failed - - Try Again - About Organic Maps - - Free for everyone, made with love - - • No ads, no tracking, no data collection - - • No battery drain, works offline - - • Fast, minimalist, developed by community - - Open-source application created by enthusiasts and volunteers. - - Location Settings - Close - The app requires hardware accelerated OpenGL. Unfortunately, your device is not supported. - Download - - Please disconnect USB cable or insert memory card to use Organic Maps - - Please free up some space on the SD card/USB storage first in order to use the app - Before you start using the app, please download the general world map to your device.\nIt will use %s of storage. - Go to Map - Downloading %s. You can now\nproceed to the map. - Download %s? - Update %s? - - Pause - - Continue - - %s download has failed - - Add a New List - - Bookmark Color - - Bookmark List Name - - Bookmarks - - Bookmarks and Tracks - - My Places - - Name - - Address - - List - - Settings - - Save maps to - - Select the folder to download maps to. - - Downloaded maps - - Internal private storage - - Internal shared storage - - SD card - - External shared storage - - %1$s free of %2$s - - Move maps? - - Error moving map files - - This can take several minutes.\nPlease wait… - - Measurement units - - Choose between miles and kilometers + + + Back + + Cancel + + Delete + Download Maps + + Download has failed. Touch to try again. + + Downloading… + + Kilometers + + MB + GB + + Miles + + My Position + + Later + + Search + + Search Map + + You currently have all Location Services for this device or application disabled. Please enable them in Settings. + + Show on the map + + Download has failed + + Try Again + About Organic Maps + + Free for everyone, made with love + + • No ads, no tracking, no data collection + + • No battery drain, works offline + + • Fast, minimalist, developed by community + + Open-source application created by enthusiasts and volunteers. + + Location Settings + Close + The app requires hardware accelerated OpenGL. Unfortunately, your device is not supported. + Download + + Please disconnect USB cable or insert memory card to use Organic Maps + + Please free up some space on the SD card/USB storage first in order to use the app + Before you start using the app, please download the general world map to your device.\nIt will use %s of storage. + Go to Map + Downloading %s. You can now\nproceed to the map. + Download %s? + Update %s? + + Pause + + Continue + + %s download has failed + + Add a New List + + Bookmark Color + + Bookmark List Name + + Bookmarks + + Bookmarks and Tracks + + My Places + + Name + + Address + + List + + Settings + + Save maps to + + Select the folder to download maps to. + + Downloaded maps + + Internal private storage + + Internal shared storage + + SD card + + External shared storage + + %1$s free of %2$s + + Move maps? + + Error moving map files + + This can take several minutes.\nPlease wait… + + Measurement units + + Choose between miles and kilometers - - - Where to eat - - Groceries - - Transport - - Gas - - Parking - - Shopping - - Second Hand - - Hotel - - Sights - - Entertainment - - ATM - - Nightlife - - Family holiday - - Bank - - Pharmacy - - Hospital - - Toilet - - Post - - Police - - WiFi - - Recycling - - Water - - RV Facilities + + + Where to eat + + Groceries + + Transport + + Gas + + Parking + + Shopping + + Second Hand + + Hotel + + Sights + + Entertainment + + ATM + + Nightlife + + Family holiday + + Bank + + Pharmacy + + Hospital + + Toilet + + Post + + Police + + WiFi + + Recycling + + Water + + RV Facilities - - - Notes - - Organic Maps bookmarks were shared with you - Hello!\n\nAttached are my bookmarks; please open them in Organic Maps. If you don\'t have it installed you can download it here: https://omaps.app/get?kmz\n\nEnjoy travelling with Organic Maps! - - Loading Bookmarks - - Bookmarks loaded successfully! You can find them on the map or on the Bookmarks Manager screen. - - Failed to load bookmarks. The file may be corrupted or defective. - - The file type is not recognized by the app:\n%1$s - - Failed to open file %1$s\n\n%2$s - - Edit - - Your location hasn\'t been determined yet - - Sorry, Map Storage settings are currently disabled. - - Map download is in progress now. - - Check out my current location in Organic Maps! %1$s or %2$s Don\'t have offline maps? Download here: https://omaps.app/get - - Hey, check out my pin in Organic Maps! - - Hey, check out my current location on the Organic Maps map! - - Hi,\n\nI\'m here now: %1$s. Click this link %2$s or this one %3$s to see the place on the map.\n\nThanks. - - Share - - Email - - Copied to clipboard: %s - - Done - - OpenStreetMap data: %s - - Are you sure you want to continue? - - Tracks - - Length - Share My Location - - General settings - - Information - Navigation - Zoom buttons - Display on the map - - Night Mode - - Off - - On - - Auto - - Perspective view - - 3D buildings - - 3D buildings are disabled in power saving mode - - Voice Instructions - - Announce Street Names - - When enabled, the name of the street or exit to turn onto will be spoken aloud. - - Voice Language - - Test Voice Directions (TTS, Text-To-Speech) - - Check the volume or system Text-To-Speech settings if you don\'t hear the voice now. - - Not Available - Auto zoom - Off - 1 hour - 2 hours - 6 hours - 12 hours - 1 day - Distance - View on map - - Menu - - Website - - News - - GitHub - - Telegram - - [Matrix] - - Mastodon - - Facebook - - X (Twitter) - - Instagram - - VK - - LINE - - OpenStreetMap - - Feedback - - Rate the app - - Help - - Frequently Asked Questions - - Donate - - Support the project - - Copyright - - Report a bug - - Improve arrow direction by moving the phone in a figure-eight motion to calibrate the compass. - - Move the phone in a figure-eight motion to calibrate the compass and fix the arrow direction on the map. - - Update All - - Cancel All - - Downloaded - - Queued - Near me - - Maps - Download All - Downloading: - - To delete map, please stop navigation. - - Routes can only be created that are fully contained within a map of a single region. - - Download map - - Retry - - Delete Map - - Update Map - - Google Play Location Services - - Quickly determine your approximate location using Bluetooth, WiFi, or mobile network - - Download all of the maps along your route - - In order to create a route, we need to download and update all the maps from your location to your destination. - - Not enough space - - Please enable Location Services - Save - Your descriptions (text or html) - create - - Red - - Yellow - - Blue - - Green - - Purple - - Orange - - Brown - - Pink - - Deep Purple - - Light Blue - - Cyan - - Teal - - Lime - - Deep Orange - - Gray - - Blue Gray + + + Notes + + Organic Maps bookmarks were shared with you + Hello!\n\nAttached are my bookmarks; please open them in Organic Maps. If you don\'t have it installed you can download it here: https://omaps.app/get?kmz\n\nEnjoy travelling with Organic Maps! + + Loading Bookmarks + + Bookmarks loaded successfully! You can find them on the map or on the Bookmarks Manager screen. + + Failed to load bookmarks. The file may be corrupted or defective. + + The file type is not recognized by the app:\n%1$s + + Failed to open file %1$s\n\n%2$s + + Edit + + Your location hasn\'t been determined yet + + Sorry, Map Storage settings are currently disabled. + + Map download is in progress now. + + Check out my current location in Organic Maps! %1$s or %2$s Don\'t have offline maps? Download here: https://omaps.app/get + + Hey, check out my pin in Organic Maps! + + Hey, check out my current location on the Organic Maps map! + + Hi,\n\nI\'m here now: %1$s. Click this link %2$s or this one %3$s to see the place on the map.\n\nThanks. + + Share + + Email + + Copied to clipboard: %s + + Done + + OpenStreetMap data: %s + + Are you sure you want to continue? + + Tracks + + Length + Share My Location + + General settings + + Information + Navigation + Zoom buttons + Display on the map + + Night Mode + + Off + + On + + Auto + + Perspective view + + 3D buildings + + 3D buildings are disabled in power saving mode + + Voice Instructions + + Announce Street Names + + When enabled, the name of the street or exit to turn onto will be spoken aloud. + + Voice Language + + Test Voice Directions (TTS, Text-To-Speech) + + Check the volume or system Text-To-Speech settings if you don\'t hear the voice now. + + Not Available + Auto zoom + Off + 1 hour + 2 hours + 6 hours + 12 hours + 1 day + Distance + View on map + + Menu + + Website + + News + + GitHub + + Telegram + + [Matrix] + + Mastodon + + Facebook + + X (Twitter) + + Instagram + + VK + + LINE + + OpenStreetMap + + Feedback + + Rate the app + + Help + + Frequently Asked Questions + + Donate + + Support the project + + Copyright + + Report a bug + + Improve arrow direction by moving the phone in a figure-eight motion to calibrate the compass. + + Move the phone in a figure-eight motion to calibrate the compass and fix the arrow direction on the map. + + Update All + + Cancel All + + Downloaded + + Queued + Near me + + Maps + Download All + Downloading: + + To delete map, please stop navigation. + + Routes can only be created that are fully contained within a map of a single region. + + Download map + + Retry + + Delete Map + + Update Map + + Google Play Location Services + + Quickly determine your approximate location using Bluetooth, WiFi, or mobile network + + Download all of the maps along your route + + In order to create a route, we need to download and update all the maps from your location to your destination. + + Not enough space + + Please enable Location Services + Save + Your descriptions (text or html) + create + + Red + + Yellow + + Blue + + Green + + Purple + + Orange + + Brown + + Pink + + Deep Purple + + Light Blue + + Cyan + + Teal + + Lime + + Deep Orange + + Gray + + Blue Gray - - When following the route, please keep in mind: - — Road conditions, traffic laws, and road signs always take priority over the navigation hints; - — The map might be inaccurate, and the suggested route might not always be the most optimal way to reach the destination; - — Suggested routes should only be understood as recommendations; - — Exercise caution with routes in border zones: the routes created by our app may sometimes cross country borders in unauthorized places. - Please stay alert and safe on the roads! - Check GPS signal - Unable to create route. Current GPS coordinates could not be identified. - Please check your GPS signal. Enabling Wi-Fi will improve your location accuracy. - Enable location services - Unable to locate current GPS coordinates. Enable location services to calculate route. - Unable to locate route - Unable to create route. - Please adjust your starting point or destination. - Adjust starting point - Route was not created. Unable to locate starting point. - Please select a starting point closer to a road. - Adjust destination - Route was not created. Unable to locate the destination. - Please select a destination point located closer to a road. - Unable to locate the intermediate point. - Please adjust your intermediate point. - System error - Unable to create route due to an application error. - Please try again - Would you like to download the map and create a more optimal route spanning more than one map? - Download additional maps to create a better route that crosses the boundaries of this map. + + When following the route, please keep in mind: + — Road conditions, traffic laws, and road signs always take priority over the navigation hints; + — The map might be inaccurate, and the suggested route might not always be the most optimal way to reach the destination; + — Suggested routes should only be understood as recommendations; + — Exercise caution with routes in border zones: the routes created by our app may sometimes cross country borders in unauthorized places. + Please stay alert and safe on the roads! + Check GPS signal + Unable to create route. Current GPS coordinates could not be identified. + Please check your GPS signal. Enabling Wi-Fi will improve your location accuracy. + Enable location services + Unable to locate current GPS coordinates. Enable location services to calculate route. + Unable to locate route + Unable to create route. + Please adjust your starting point or destination. + Adjust starting point + Route was not created. Unable to locate starting point. + Please select a starting point closer to a road. + Adjust destination + Route was not created. Unable to locate the destination. + Please select a destination point located closer to a road. + Unable to locate the intermediate point. + Please adjust your intermediate point. + System error + Unable to create route due to an application error. + Please try again + Would you like to download the map and create a more optimal route spanning more than one map? + Download additional maps to create a better route that crosses the boundaries of this map. - - To start searching and creating routes, please download the map. After that you will no longer need an Internet connection. - Select Map - - Show - - Hide - Categories - History - Oops, no results found. - Try changing your search criteria. - Search History - View your recent searches. - Clear Search History - - Wikipedia - - Wikimedia Commons - Your Location - Start - Route from - Route to - Navigation is only available from your current location. - Do you want to plan a route from your current location? - - Next - - From - - To - Add Schedule - Delete Schedule - - All Day (24 hours) - Open - Closed - Add Non-Business Hours - Business Hours - Advanced Mode - Simple Mode - Non-Business Hours - Example Values - Correct mistake - Location - Please describe the problem in detail so that the OpenStreetMap community can fix it. - Or do it yourself at https://www.openstreetmap.org/ - Send - Issue - This place does not exist - Сlosed for maintenance - Duplicate place - Auto-download maps - - Daily - 24/7 - Closed today - Closed - Today - Opens in %s - Closes in %s - Closed - Edit business hours - Don\'t have an OpenStreetMap account? - Register at OpenStreetMap - Login - Login to OpenStreetMap - Password - Forgot your password? - Log Out - Edit Place - Add a language - Street - - Building number - Details - - Add a street - - Please enter a street name - Choose a language - Choose a street - Postal Code - Cuisine - Select cuisine - - Email or username - Add Phone - Floor - All of your map edits will be deleted with the map. - Update Maps - To create a route, you need to update all maps and then plan the route again. - Find map - Please make sure your device is connected to the Internet. - Not enough space - Please delete any unnecessary data - Login error. - Verified Changes - Drag the map to select the correct location of the object. - Editing - Adding - Name of the place - - As it is written in the local language - Category - Detailed description of the issue - Different problem - Add business - No object can be located here - - Community-created OpenStreetMap data as of %s. Learn more about how to edit and update the map at OpenStreetMap.org - Log in to openstreetmap.org to publish your changes to the world. - - %1$d of %2$d - Download over a cellular network connection? - This could be considerably expensive with some plans or if roaming. - Enter a valid building number - Number of floors (maximum of %d) - - The number of floors must non exceed %d - ZIP Code - Enter a valid ZIP code - - Unknown Place - Send a note to OSM editors - Detailed comment - Your suggested map changes will be sent to the OpenStreetMap community. Please describe any additional details that cannot be edited in Organic Maps. - More about OpenStreetMap - Owner - Can\'t find a suitable category? - Organic Maps allows to add simple point categories only, that means no towns, roads, lakes, building outlines, etc. Please add such categories directly to OpenStreetMap.org. Check our guide for detailed step by step instructions. - You haven\'t downloaded any maps - Download maps to search and navigate offline. - - m - - km - - km/h - - mi - - ft - mph - h - min - More - - Photos, reviews, booking - - The referral bonus received for each booking through this link goes towards the development of Organic Maps. - - Details on Kayak - Edit Bookmark - Comment… - Discard all local changes? - Discard - Delete added place? - Delete - Place does not exist - - Please indicate the reason for deleting the place - - Enter a valid phone number - Enter a valid web address - Enter a valid email - Enter a valid Facebook web address, account, or page name - Enter a valid Instagram username or web address - Enter a valid Twitter username or web address - Enter a valid VK username or web address - Enter a valid LINE ID or web address - Add Place to OpenStreetMap - - Do you want to send it to all users? - - Make sure you did not enter any private or personal data. - OpenStreetMap editors will check the changes and contact you if they have any questions. - Stop - - Accept - - Decline - Use mobile internet to show detailed information? - Use Always - Only Today - Do not Use Today - Mobile Internet - - Mobile internet is required for map update notifications and uploading edits. - Never Use - Always Ask - To display traffic data, maps must be updated. - Increase size for map labels - Please update Organic Maps - - Traffic data is not available - Enable logging - - General Feedback - We use system TTS for voice instructions. Many Android devices use Google TTS, you can download or update it from Google Play (https://play.google.com/store/apps/details?id=com.google.android.tts) - For some languages, you will need to install a speech synthesizer or an additional language pack from the app store (Google Play, Galaxy Store, App Gallery, FDroid).\nOpen your device\'s settings → Language and input → Speech → Text to speech output.\nHere you can manage settings for speech synthesis (for example, download language pack for offline use) and select another text-to-speech engine. - For more information please check this guide. - Transliteration into Latin alphabet - Learn more - - Exit - Add a starting point to plan a route - Add a destination to plan a route - Remove - Add Stop - - Please login to OpenStreetMap to automatically upload all your map edits. Learn more here. - Storage access problem - External storage is not accessible. The SD Card may have been removed, damaged, or the file system is read-only. Please, check your SD Card or contact us at support\@organicmaps.app - Emulate bad storage - Entrance - Please enter a correct name - Lists - - Hide all - Show all - Create a new list - - Import Bookmarks and Tracks - Unable to share due to an application error - Sharing error - Cannot share an empty list - The name can\'t be empty - Please enter the list name - New list - This name is already taken - Please choose another name - Please wait… - Phone number - OpenStreetMap profile - - %d file was found. You can see it after conversion. - %d files were found. You can see them after conversion. - - - %d object - %d objects - - - %d place - %d places - - - %d track - %d tracks - - - Privacy - Privacy policy - Terms of use - Traffic - Subway - Map Styles and Layers - Subway map is unavailable - This list is empty - To add a bookmark, tap a place on the map and then tap the star icon - …more - Export KMZ - Export GPX - Delete list - Public access - Limited access - Enter a description (text or html) - Private - Speed cameras - Place Description - - Map downloader - - Warn if speeding - - Always warn - - Never warn - Power saving mode - Try to reduce power usage at the expense of some functionality. - Never - When battery is low - Always - Enable this option temporarily to record and manually send detailed diagnostic logs about your issue to us using \"Report a bug\" in the Help dialog. Logs may include location info. - Online editing - Routing options - - Avoid tolls - - Avoid unpaved roads - - Avoid ferries - Avoid freeways - Unable to calculate route - A route could not be found. This may be caused by your routing options or incomplete OpenStreetMap data. Please change your routing options and retry. - Define roads to avoid - Routing options enabled - Toll road - Unpaved road - Ferry crossing - - Yes - - No - - Yes - - No - - Capacity: %s - You have arrived! - OK - - Sort… - - Sort bookmarks - - By default - - By type - - By distance - - By date - - By name - A week ago - A month ago - More than a month ago - More than a year ago - Near me - Others + + To start searching and creating routes, please download the map. After that you will no longer need an Internet connection. + Select Map + + Show + + Hide + Categories + History + Oops, no results found. + Try changing your search criteria. + Search History + View your recent searches. + Clear Search History + + Wikipedia + + Wikimedia Commons + Your Location + Start + Route from + Route to + Navigation is only available from your current location. + Do you want to plan a route from your current location? + + Next + + From + + To + Add Schedule + Delete Schedule + + All Day (24 hours) + Open + Closed + Add Non-Business Hours + Business Hours + Advanced Mode + Simple Mode + Non-Business Hours + Example Values + Correct mistake + Location + Please describe the problem in detail so that the OpenStreetMap community can fix it. + Or do it yourself at https://www.openstreetmap.org/ + Send + Issue + This place does not exist + Сlosed for maintenance + Duplicate place + Auto-download maps + + Daily + 24/7 + Closed today + Closed + Today + Opens in %s + Closes in %s + Closed + Edit business hours + Don\'t have an OpenStreetMap account? + Register at OpenStreetMap + Login + Login to OpenStreetMap + Password + Forgot your password? + Log Out + Edit Place + Add a language + Street + + Building number + Details + + Add a street + + Please enter a street name + Choose a language + Choose a street + Postal Code + Cuisine + Select cuisine + + Email or username + Add Phone + Floor + All of your map edits will be deleted with the map. + Update Maps + To create a route, you need to update all maps and then plan the route again. + Find map + Please make sure your device is connected to the Internet. + Not enough space + Please delete any unnecessary data + Login error. + Verified Changes + Drag the map to select the correct location of the object. + Editing + Adding + Name of the place + + As it is written in the local language + Category + Detailed description of the issue + Different problem + Add business + No object can be located here + + Community-created OpenStreetMap data as of %s. Learn more about how to edit and update the map at OpenStreetMap.org + Log in to openstreetmap.org to publish your changes to the world. + + %1$d of %2$d + Download over a cellular network connection? + This could be considerably expensive with some plans or if roaming. + Enter a valid building number + Number of floors (maximum of %d) + + The number of floors must non exceed %d + ZIP Code + Enter a valid ZIP code + + Unknown Place + Send a note to OSM editors + Detailed comment + Your suggested map changes will be sent to the OpenStreetMap community. Please describe any additional details that cannot be edited in Organic Maps. + More about OpenStreetMap + Owner + Can\'t find a suitable category? + Organic Maps allows to add simple point categories only, that means no towns, roads, lakes, building outlines, etc. Please add such categories directly to OpenStreetMap.org. Check our guide for detailed step by step instructions. + You haven\'t downloaded any maps + Download maps to search and navigate offline. + + m + + km + + km/h + + mi + + ft + mph + h + min + More + + Photos, reviews, booking + + The referral bonus received for each booking through this link goes towards the development of Organic Maps. + + Details on Kayak + Edit Bookmark + Comment… + Discard all local changes? + Discard + Delete added place? + Delete + Place does not exist + + Please indicate the reason for deleting the place + + Enter a valid phone number + Enter a valid web address + Enter a valid email + Enter a valid Facebook web address, account, or page name + Enter a valid Instagram username or web address + Enter a valid Twitter username or web address + Enter a valid VK username or web address + Enter a valid LINE ID or web address + Add Place to OpenStreetMap + + Do you want to send it to all users? + + Make sure you did not enter any private or personal data. + OpenStreetMap editors will check the changes and contact you if they have any questions. + Stop + + Accept + + Decline + Use mobile internet to show detailed information? + Use Always + Only Today + Do not Use Today + Mobile Internet + + Mobile internet is required for map update notifications and uploading edits. + Never Use + Always Ask + To display traffic data, maps must be updated. + Increase size for map labels + Please update Organic Maps + + Traffic data is not available + Enable logging + + General Feedback + We use system TTS for voice instructions. Many Android devices use Google TTS, you can download or update it from Google Play (https://play.google.com/store/apps/details?id=com.google.android.tts) + For some languages, you will need to install a speech synthesizer or an additional language pack from the app store (Google Play, Galaxy Store, App Gallery, FDroid).\nOpen your device\'s settings → Language and input → Speech → Text to speech output.\nHere you can manage settings for speech synthesis (for example, download language pack for offline use) and select another text-to-speech engine. + For more information please check this guide. + Transliteration into Latin alphabet + Learn more + + Exit + Add a starting point to plan a route + Add a destination to plan a route + Remove + Add Stop + + Please login to OpenStreetMap to automatically upload all your map edits. Learn more here. + Storage access problem + External storage is not accessible. The SD Card may have been removed, damaged, or the file system is read-only. Please, check your SD Card or contact us at support\@organicmaps.app + Emulate bad storage + Entrance + Please enter a correct name + Lists + + Hide all + Show all + Create a new list + + Import Bookmarks and Tracks + Unable to share due to an application error + Sharing error + Cannot share an empty list + The name can\'t be empty + Please enter the list name + New list + This name is already taken + Please choose another name + Please wait… + Phone number + OpenStreetMap profile + + %d file was found. You can see it after conversion. + %d files were found. You can see them after conversion. + + + %d object + %d objects + + + %d place + %d places + + + %d track + %d tracks + + + Privacy + Privacy policy + Terms of use + Traffic + Subway + Map Styles and Layers + Subway map is unavailable + This list is empty + To add a bookmark, tap a place on the map and then tap the star icon + …more + Export KMZ + Export GPX + Delete list + Public access + Limited access + Enter a description (text or html) + Private + Speed cameras + Place Description + + Map downloader + + Warn if speeding + + Always warn + + Never warn + Power saving mode + Try to reduce power usage at the expense of some functionality. + Never + When battery is low + Always + Enable this option temporarily to record and manually send detailed diagnostic logs about your issue to us using \"Report a bug\" in the Help dialog. Logs may include location info. + Online editing + Routing options + + Avoid tolls + + Avoid unpaved roads + + Avoid ferries + Avoid freeways + Unable to calculate route + A route could not be found. This may be caused by your routing options or incomplete OpenStreetMap data. Please change your routing options and retry. + Define roads to avoid + Routing options enabled + Toll road + Unpaved road + Ferry crossing + + Yes + + No + + Yes + + No + + Capacity: %s + You have arrived! + OK + + Sort… + + Sort bookmarks + + By default + + By type + + By distance + + By date + + By name + A week ago + A month ago + More than a month ago + More than a year ago + Near me + Others - - Food - Sights - Museums - Parks - Swim - Mountains - Animals - Hotels - Buildings - Money - Shops - Parking - Gas Stations - Medicine - Search in the list - Religious places - Select list - Subway navigation in this region is not available yet - Subway route is not found - Please choose a start or end point closer to a subway station - Contour Lines - Activating contour lines requires downloading map data for this area - Contour lines are not yet available in this area - Ascent - Descent - Min. altitude - Max. altitude - Difficulty - Dist.: - Time: - Zoom in to explore isolines - Downloading - Download the world map - - Unable to create folder and move files on internal device\'s memory or sdcard - - Disk error - - Connection failure - - Disconnect USB cable - Keep the screen on - - When enabled, the screen will always be on when displaying the map. - - Show on the lock screen - - When enabled, the app will work on the lockscreen even when the device is locked. - - Map data from OpenStreetMap - - https://t.me/OrganicMapsApp - - https://www.instagram.com/organicmaps.app - - https://organicmaps.app/ - - https://wiki.openstreetmap.org/wiki/About_OpenStreetMap - - %1$s, %2$s - - Thank you for using our community-built maps! - - With your donations and support, we can create the best maps in the World! - - Do you like our app? Please donate to support the development! Don\'t like it yet? Please let us know why, and we will fix it! - - If you know a software developer, you can ask him or her to implement a feature that you need. - - Do you know that you can long-tap any place on the map to select it? - - Did you know that your current location on the map can be selected? - - You can help to translate our app into your language. - - Our app is developed by a few enthusiasts and the community. - - You can easily fix and improve the map data. - - Our main goal is to build fast, privacy-focused, easy-to-use maps that you will love. - - You are now using Organic Maps on the phone screen - - You are now using Organic Maps on the car screen - - You are connected to Android Auto - - Continue on the phone - - To the car screen - - This application requires access to your location for navigation purposes. - - Grant Permissions - - Connected to car - - Outdoors - - Web browser is not available - Volume - - Export all Bookmarks and Tracks - - Speech synthesis system settings - - Speech Synthesis settings were not found, are you sure your device supports it? - Drive-through - Clear the search - Zoom in - - Zoom out - - Menu Link - - View Menu + + Food + Sights + Museums + Parks + Swim + Mountains + Animals + Hotels + Buildings + Money + Shops + Parking + Gas Stations + Medicine + Search in the list + Religious places + Select list + Subway navigation in this region is not available yet + Subway route is not found + Please choose a start or end point closer to a subway station + Contour Lines + Activating contour lines requires downloading map data for this area + Contour lines are not yet available in this area + Ascent + Descent + Min. altitude + Max. altitude + Difficulty + Dist.: + Time: + Zoom in to explore isolines + Downloading + Download the world map + + Unable to create folder and move files on internal device\'s memory or sdcard + + Disk error + + Connection failure + + Disconnect USB cable + Keep the screen on + + When enabled, the screen will always be on when displaying the map. + + Show on the lock screen + + When enabled, the app will work on the lockscreen even when the device is locked. + + Map data from OpenStreetMap + + https://t.me/OrganicMapsApp + + https://www.instagram.com/organicmaps.app + + https://organicmaps.app/ + + https://wiki.openstreetmap.org/wiki/About_OpenStreetMap + + %1$s, %2$s + + Thank you for using our community-built maps! + + With your donations and support, we can create the best maps in the World! + + Do you like our app? Please donate to support the development! Don\'t like it yet? Please let us know why, and we will fix it! + + If you know a software developer, you can ask him or her to implement a feature that you need. + + Do you know that you can long-tap any place on the map to select it? + + Did you know that your current location on the map can be selected? + + You can help to translate our app into your language. + + Our app is developed by a few enthusiasts and the community. + + You can easily fix and improve the map data. + + Our main goal is to build fast, privacy-focused, easy-to-use maps that you will love. + + You are now using Organic Maps on the phone screen + + You are now using Organic Maps on the car screen + + You are connected to Android Auto + + Continue on the phone + + To the car screen + + This application requires access to your location for navigation purposes. + + Grant Permissions + + Connected to car + + Outdoors + + Web browser is not available + Volume + + Export all Bookmarks and Tracks + + Speech synthesis system settings + + Speech Synthesis settings were not found, are you sure your device supports it? + Drive-through + Clear the search + Zoom in + + Zoom out + + Menu Link + + View Menu - - Address/Block - Address/Block - Address/Block - Aerialway - Cable Car - Chair Lift - Drag Lift - Gondola - Mixed Lift - Aerialway Station - Airspace Infrastructure - Airport - International Airport - Apron - Gate - Helipad - Runway - Taxiway - Terminal - Amenity - Arts Center - ATM - Bank - Bar - Barbecue Grill - Bench - Bicycle Parking - Bicycle Rental - Bicycle Repair Station - Biergarten - Brothel - Currency Exchange - Bus Station - Cafe - Car Rental - Car Sharing - Car Wash - Casino - Gambling - Adult Gaming Centre - Arcade - Charging Station - Bicycle Charging Station - Car Charging Station - Nursery - Cinema - Bowling Alley - Clinic - College - Community Centre - Compressed Air - Conference Center - Courthouse - Dentist - Doctor - Drinking Water - Drinking Water - Driving School - Exhibition Center - Money Transfer - Music School - Language School - Embassy - Fast Food - Ferry - Fire Station - Food Court - Fountain - Gas Station - - Graveyard - - Christian Graveyard - Hospital - Hunting Stand - Ice Cream - Internet Cafe - Kindergarten - Library - Loading Dock - Marketplace - Motorcycle Parking - Nightclub - Nursing Home - Parking - Parking - Multi Storey Parking - Multi Storey Parking - Private Parking - Private Parking - Private Parking - Park And Ride Parking - Underground Parking - Underground Parking - Private Underground Parking - Street-Side Parking - Street-Side Parking - Private Street-Side Parking - Lane Parking - Lane Parking - Private Lane Parking - Parking Entrance - Private Parking Entrance - Parking Entrance - Parking Space - Parking Space - Parking Space - Parking Space - Disabled Parking Space - Payment Terminal - Pharmacy - Place of Worship - Buddhist Temple - Church - Church of Jesus Christ of Latter Day Saints - Jehovah\'s Witnesses Kingdom Hall - Hindu Temple - Synagogue - Mosque - Shinto Shrine - Taoist Temple - Police - Mailbox - Post Office - Prison - Pub - Book Exchange + + Address/Block + Address/Block + Address/Block + Aerialway + Cable Car + Chair Lift + Drag Lift + Gondola + Mixed Lift + Aerialway Station + Airspace Infrastructure + Airport + International Airport + Apron + Gate + Helipad + Runway + Taxiway + Terminal + Amenity + Arts Center + ATM + Bank + Bar + Barbecue Grill + Bench + Bicycle Parking + Bicycle Rental + Bicycle Repair Station + Biergarten + Brothel + Currency Exchange + Bus Station + Cafe + Car Rental + Car Sharing + Car Wash + Casino + Gambling + Adult Gaming Centre + Arcade + Charging Station + Bicycle Charging Station + Car Charging Station + Nursery + Cinema + Bowling Alley + Clinic + College + Community Centre + Compressed Air + Conference Center + Courthouse + Dentist + Doctor + Drinking Water + Drinking Water + Driving School + Exhibition Center + Money Transfer + Music School + Language School + Embassy + Fast Food + Ferry + Fire Station + Food Court + Fountain + Gas Station + + Graveyard + + Christian Graveyard + Hospital + Hunting Stand + Ice Cream + Internet Cafe + Kindergarten + Library + Loading Dock + Marketplace + Motorcycle Parking + Nightclub + Nursing Home + Parking + Parking + Multi Storey Parking + Multi Storey Parking + Private Parking + Private Parking + Private Parking + Park And Ride Parking + Underground Parking + Underground Parking + Private Underground Parking + Street-Side Parking + Street-Side Parking + Private Street-Side Parking + Lane Parking + Lane Parking + Private Lane Parking + Parking Entrance + Private Parking Entrance + Parking Entrance + Parking Space + Parking Space + Parking Space + Parking Space + Disabled Parking Space + Payment Terminal + Pharmacy + Place of Worship + Buddhist Temple + Church + Church of Jesus Christ of Latter Day Saints + Jehovah\'s Witnesses Kingdom Hall + Hindu Temple + Synagogue + Mosque + Shinto Shrine + Taoist Temple + Police + Mailbox + Post Office + Prison + Pub + Book Exchange - - Recycling Center - Recycling Container - Recycling Container - Batteries - Clothes - Glass Bottles - Paper - Plastic - Plastic Bottles - Scrap Metal - Electronic Waste - Cardboard - Cans - Shoes - Green/Organic Waste - Cartons - Restaurant - Holding Tank Dump Station - School - - Shelter - - Shelter - - Bivouac Hut - - Lean-to Shelter - Public Bath - Shower - Stripclub - Taxi Stand - Phone - Theatre - Toilet - Toilet - Town Hall - University - Vending Machine - Cigarette Dispenser - Coffee Dispenser - Condoms Dispenser - Drinks Dispenser - Food Dispenser - Newspaper Dispenser - Parking Meter - Ticket Machine - Sweets Dispenser - Excrement Bags Dispenser - Parcel Locker - Vehicle Inspection - Fuel Pump - Veterinary Doctor - Trash Bin - Dumpster - Waste Transfer Station - Water Tank Refill Point - Barrier - Block - Bollard - Border Control - Chain - City Wall - Cycle Barrier - Drainage Ditch - Moat - Wastewater - Entrance - Fence - Gate - Hedge - Kissing Gate - Lift Gate - Retaining Wall - Stile - Turnstile - Swing Gate - Toll Booth - Wall - Boundary - Administrative Boundary - - National Border - - Regional Boundary - - Regional Boundary - National Park - Indigenous Lands - Protected Area - Protected Area - Protected Area - Protected Area - Protected Area - Protected Area - Protected Area - Building - - Address - Building - Building - Garage - Train Station - Warehouse - Grave - Craft - Beekeeper - Blacksmith - Craft Brewery - Caterer - Carpenter - Confectioner - Electrician - Electronics Repair - Gardener - Grinding Mill - Handicraft - - HVAC Shop - Key Cutting - Locksmith - Metal Worker - House Painter - Photographer - Camera Shop - Plumber - Sawmill - Shoe Repair - Winery - Tailor - African - American - Arab - Argentinian - Asian - Austrian - Bagel - Balkan - Barbecue - Bavarian - Beef Bowl - Brazilian - Breakfast - Bubble Tea - Burger - Buschenschank - Cake - Caribbean - Chicken - Chinese - Coffee - Crepe - Croatian - Curry - Deli - Diner - Donut - Ethiopian - Filipino - Fine Dining - Fish - Fish and Chips - French - Friture - Georgian - German - Greek - Grill - Heuriger - Hotdog - Hungarian - Ice Cream - Indian - Indonesian - International - Irish - Italian - Italian, Pizza - Japanese - Kebab - Korean - Lao - Lebanese - Local - Malagasy - Malaysian - Mediterranean - Mexican - Moroccan - Noodles - East Asian - Pancake - Pasta - Persian - Peruvian - Pizza - Polish - Portuguese - Ramen - Regional - Russian - Sandwich - Sausage - Savory Pancakes - Seafood - Soba - Spanish - Steak House - Sushi - Tapas - Tea - Thai - Turkish - Vegan - Vegetarian - Vietnamese - Emergency - Emergency Assembly Point - Defibrillator - Fire Hydrant - Emergency Phone - - Entrance - - Main Entrance - Exit - $ - Free - Medical Laboratory - Physiotherapist - Alternative Medicine - Audiologist - Blood Donation Center - Optometrist - Podiatrist - Psychotherapist - Sample Collection Centre - Logopedics + + Recycling Center + Recycling Container + Recycling Container + Batteries + Clothes + Glass Bottles + Paper + Plastic + Plastic Bottles + Scrap Metal + Electronic Waste + Cardboard + Cans + Shoes + Green/Organic Waste + Cartons + Restaurant + Holding Tank Dump Station + School + + Shelter + + Shelter + + Bivouac Hut + + Lean-to Shelter + Public Bath + Shower + Stripclub + Taxi Stand + Phone + Theatre + Toilet + Toilet + Town Hall + University + Vending Machine + Cigarette Dispenser + Coffee Dispenser + Condoms Dispenser + Drinks Dispenser + Food Dispenser + Newspaper Dispenser + Parking Meter + Ticket Machine + Sweets Dispenser + Excrement Bags Dispenser + Parcel Locker + Vehicle Inspection + Fuel Pump + Veterinary Doctor + Trash Bin + Dumpster + Waste Transfer Station + Water Tank Refill Point + Barrier + Block + Bollard + Border Control + Chain + City Wall + Cycle Barrier + Drainage Ditch + Moat + Wastewater + Entrance + Fence + Gate + Hedge + Kissing Gate + Lift Gate + Retaining Wall + Stile + Turnstile + Swing Gate + Toll Booth + Wall + Boundary + Administrative Boundary + + National Border + + Regional Boundary + + Regional Boundary + National Park + Indigenous Lands + Protected Area + Protected Area + Protected Area + Protected Area + Protected Area + Protected Area + Protected Area + Building + + Address + Building + Building + Garage + Train Station + Warehouse + Grave + Craft + Beekeeper + Blacksmith + Craft Brewery + Caterer + Carpenter + Confectioner + Electrician + Electronics Repair + Gardener + Grinding Mill + Handicraft + + HVAC Shop + Key Cutting + Locksmith + Metal Worker + House Painter + Photographer + Camera Shop + Plumber + Sawmill + Shoe Repair + Winery + Tailor + African + American + Arab + Argentinian + Asian + Austrian + Bagel + Balkan + Barbecue + Bavarian + Beef Bowl + Brazilian + Breakfast + Bubble Tea + Burger + Buschenschank + Cake + Caribbean + Chicken + Chinese + Coffee + Crepe + Croatian + Curry + Deli + Diner + Donut + Ethiopian + Filipino + Fine Dining + Fish + Fish and Chips + French + Friture + Georgian + German + Greek + Grill + Heuriger + Hotdog + Hungarian + Ice Cream + Indian + Indonesian + International + Irish + Italian + Italian, Pizza + Japanese + Kebab + Korean + Lao + Lebanese + Local + Malagasy + Malaysian + Mediterranean + Mexican + Moroccan + Noodles + East Asian + Pancake + Pasta + Persian + Peruvian + Pizza + Polish + Portuguese + Ramen + Regional + Russian + Sandwich + Sausage + Savory Pancakes + Seafood + Soba + Spanish + Steak House + Sushi + Tapas + Tea + Thai + Turkish + Vegan + Vegetarian + Vietnamese + Emergency + Emergency Assembly Point + Defibrillator + Fire Hydrant + Emergency Phone + + Entrance + + Main Entrance + Exit + $ + Free + Medical Laboratory + Physiotherapist + Alternative Medicine + Audiologist + Blood Donation Center + Optometrist + Podiatrist + Psychotherapist + Sample Collection Centre + Logopedics - - Highway - Bridle Path - - Bridge - Bridle Path - - Tunnel - Dedicated Bus Road - - Bridge - - Tunnel - Bus Stop - Road Under Construction - Cycle Path - - Bridge - Cycle Path - - Tunnel - Elevator - Foot Path - Sidewalk - Pedestrian Crossing - Pedestrian Area - - Pedestrian Bridge - - Pedestrian Tunnel - Ford - Living Street - - Bridge - - Tunnel - Motorway - - Motorway Bridge - - Motorway Tunnel - Road Exit - Motorway Ramp - - Bridge - - Tunnel - Path - - Difficult or Indistinct Trail - - Expert or Indiscernible Trail - Cycle & Foot Path - Cycle & Foot Path - - Bridge - Bridle Path - - Tunnel - Pedestrian Street - Pedestrian Area - - Pedestrian Bridge - - Pedestrian Tunnel - Primary Road - - Bridge - - Tunnel - Primary Road Ramp - - Bridge - - Tunnel - Racetrack - Residential Street - Residential Street - - Bridge - - Tunnel - Rest Area - Road - - Bridge - - Bridge - - Tunnel - Secondary Road - - Bridge - - Tunnel - Secondary Road Ramp - - Bridge - - Tunnel - Service Road - Service Road - - Bridge - Driveway - Parking Aisle - - Tunnel - Service Area - Speed Camera - Stairs - - Bridge - - Tunnel - Tertiary Road - - Bridge - - Tunnel - Tertiary Road Ramp - - Bridge - - Tunnel - Track - Track - - Bridge - Track - Track - - Tunnel - Traffic Lights - Trunk Road - - Bridge - - Tunnel - Trunk Road Ramp - - Bridge - - Tunnel - Minor Road - Minor Road - - Bridge - - Tunnel - Cycle Path - Foot Path - Living Street - Motorway - Path - Pedestrian Street - Primary Road - Residential Street - Secondary Road - Service Road - Tertiary Road - Stairs - Track - Trunk Road - Minor Road - highway-world_level - highway-world_towns_level + + Highway + Bridle Path + + Bridge + Bridle Path + + Tunnel + Dedicated Bus Road + + Bridge + + Tunnel + Bus Stop + Road Under Construction + Cycle Path + + Bridge + Cycle Path + + Tunnel + Elevator + Foot Path + Sidewalk + Pedestrian Crossing + Pedestrian Area + + Pedestrian Bridge + + Pedestrian Tunnel + Ford + Living Street + + Bridge + + Tunnel + Motorway + + Motorway Bridge + + Motorway Tunnel + Road Exit + Motorway Ramp + + Bridge + + Tunnel + Path + + Difficult or Indistinct Trail + + Expert or Indiscernible Trail + Cycle & Foot Path + Cycle & Foot Path + + Bridge + Bridle Path + + Tunnel + Pedestrian Street + Pedestrian Area + + Pedestrian Bridge + + Pedestrian Tunnel + Primary Road + + Bridge + + Tunnel + Primary Road Ramp + + Bridge + + Tunnel + Racetrack + Residential Street + Residential Street + + Bridge + + Tunnel + Rest Area + Road + + Bridge + + Bridge + + Tunnel + Secondary Road + + Bridge + + Tunnel + Secondary Road Ramp + + Bridge + + Tunnel + Service Road + Service Road + + Bridge + Driveway + Parking Aisle + + Tunnel + Service Area + Speed Camera + Stairs + + Bridge + + Tunnel + Tertiary Road + + Bridge + + Tunnel + Tertiary Road Ramp + + Bridge + + Tunnel + Track + Track + + Bridge + Track + Track + + Tunnel + Traffic Lights + Trunk Road + + Bridge + + Tunnel + Trunk Road Ramp + + Bridge + + Tunnel + Minor Road + Minor Road + + Bridge + + Tunnel + Cycle Path + Foot Path + Living Street + Motorway + Path + Pedestrian Street + Primary Road + Residential Street + Secondary Road + Service Road + Tertiary Road + Stairs + Track + Trunk Road + Minor Road + highway-world_level + highway-world_towns_level - - Historic Object - Historic Aircraft - Historic Anchor - Archaeological Site - Historic Battlefield - Boundary Stone - Cannon - Castle - Roman Fort - Stronghold Castle - Fortified Church - Fortress - Hillfort - Kremlin - Manor House - Palace - Japanese Castle - Stately Castle - City Gate - City Wall - Fort - Gallows - Historic Locomotive - Memorial - Memorial Cross - Commemorative Plaque - Sculpture - Statue - Stolperstein - Historic Stone - War Memorial - Historic Mine - Monument - Pillory - Historic Ruins - Ship - Historic Tank - Tomb - Wayside Cross - Wayside Shrine - Shipwreck - hwtag - hwtag-bidir_bicycle - hwtag-onedir_bicycle - hwtag-lit - hwtag-nobicycle - hwtag-nocar - hwtag-nofoot - hwtag-oneway - hwtag-private - hwtag-toll - hwtag-yesbicycle - hwtag-yescar - hwtag-yesfoot - Internet - Internet - Junction - Roundabout - Roundabout - Landuse - Allotments - Basin - Brownfield - - Cemetery - - Christian Cemetery - Churchyard - Commercial Area - Construction Area - Educational Facility - Farmland - Farmyard - Field - Flowerbed - Forest - Coniferous Forest - Deciduous Forest - Mixed-Leaf Forest - Garages - Grass - Greenfield - Greenhouse - Industrial Area - Landfill - Meadow - Military Area - Orchard - Quarry - Railway Premises - Recreation Ground - Reservoir - Residential Area - Retail Area - Salt Pond - Land - Vineyard - Leisure - Public Land - Dog Park - Fitness Centre - Fitness Station - Dance Hall - Garden - Residential Garden - Golf Course - Minigolf - Hackerspace - Ice Rink - Marina - Nature Reserve - Outdoor Seating - Park - Private Park - Park - Private Park - Picnic Table - Sport Pitch - Playground - Recreation Ground - Sauna - Slipway - Sports Center - Climbing Centre - Yoga Studio - Stadium - Swimming Pool - Swimming Pool - Track - Track - Water Park - Beach Resort - Man Made - Breakwater - Cairn - Chimney - Cutline - Survey Point - Flagpole - Lighthouse - Mast - Pier - Pipeline - Overground Pipeline - Silo - Storage Tank - Surveillance Camera - Tower - Communications Tower - Wastewater Treatment Plant - Water Tap - Water Tower - Water Well - Windmill - Industrial Works - mapswithme - mapswithme-grid - Military - Bunker - Mountain Pass - Nature - - Bare Rock - - Shingle - - Scree - Bay - Beach - Sandy Beach - Gravel Beach - Cape - Cave Entrance - Cliff - Earth Bank - Embankment - Coastline - Desert - Geyser - Glacier - Grassland - Heath - Hot Spring - Lake - Lock Chamber - Pond - Reservoir - Basin - River - Land - Meadow - Orchard - Peak - Mountain Saddle - Rock - Scrub - Natural Spring - Strait - Tree Row - Vineyard - Volcano - Water - Wetland - Bog - Marsh - Dead End - Office - Company Office - Estate Agent - Government Office - Insurance Office - Lawyer - Non-Governmental Organization - Telecom Company - Organic - Organic - City - Capital - City - City - Capital - City - City - City - City - City - City - City - Continent - Country - County - Farm - Hamlet - Island - Islet - Isolated Dwelling - Locality - Neighbourhood - Ocean - Region - Sea - Square - State - State - Suburb - Town - Village - Power - Power Generator - Solar Generator - Wind Generator - Gas Turbine Power Plant - Hydroelectric Power Plant - Power Line - Underground Power Line - Minor Power Line - Power Plant - Coal Power Plant - Gas Turbine Power Plant - Hydroelectric Power Plant - Solar Power Plant - Wind Power Plant - Power Station - Substation - Power Tower - psurface - psurface-paved_bad - psurface-paved_good - psurface-unpaved_bad - psurface-unpaved_good - Public Transport - Platform - Railway - Abandoned Railway - Abandoned Railway Bridge - Abandoned Railway Tunnel - Railway Construction - Railway Crossing - Disused Railway - Funicular - Funicular Bridge - Funicular Tunnel - Rail Halt - Level Crossing - Light Rail - Light Rail Bridge - Light Rail Tunnel - Monorail - Monorail Bridge - Monorail Tunnel - Narrow Gauge Rail - Narrow Gauge Rail Bridge - Narrow Gauge Rail Tunnel - Railway Platform - Preserved Rail - Preserved Rail Bridge - Preserved Rail Tunnel - Railway - High-Speed Railway - Touristic Railway - Railway - - Railway Branch - - Utility Railway - Railway Spur - - Service Rail Track - Railway Bridge - Railway Bridge - Railway Bridge - Railway Bridge - Railway Bridge - Railway Bridge - Railway Bridge - Railway Bridge - Railway Tunnel - Railway Tunnel - Railway Tunnel - Railway Tunnel - Railway Tunnel - Railway Tunnel - Railway Tunnel - Railway Tunnel - Train Station - Funicular - Light Rail Station - DLR Station - Porto Metro - Monorail Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Underground Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Station - Subway Line - Subway Line Bridge - Subway Line Tunnel - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Metro Station Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Subway Entrance - Tram Line - Tram Line Bridge - Tram Line Tunnel - Tram Stop - Route - Ferry - route-shuttle_train - Shop - Liquor Shop - Bakery - Bathroom Furnishings - Beauty Shop - Beverages - Bicycle Shop - Bookmaker - Bookstore - Butcher - Cannabis Shop - Car Dealership - Car Parts Shop - Car Repair Workshop - Tyre Repair - RV Dealership - Carpet Shop - Chemist - Chocolate Shop - Clothes Shop - Coffee Shop - Computer Store - Candy Shop - Convenience Store - Copyshop - Cosmetics Shop - Curtain Shop - Delicatessen - Department Store - Home Improvement Store - Dry Cleaner - Electronics Shop - Erotic Shop - Fabric Shop - Farm Food Shop - Fashion Accessories - Florist - Funeral Directors - Furniture Store - Garden Center - Gas Store - Gift Shop - Greengrocer - Grocery Store - Hairdresser - Hardware Store - Health Food Shop - Herbalist - HiFi Audio Shop - Housewares Store - Jewelry Store - Kiosk - Kitchen Store - Laundry - Mall - Massage Salon - Cell Phone Store - Money Lender - Motorcycle Shop - Motorcycle Repair - Record Store - Musical Instrument Shop - Newspaper Stand - Optician - Outdoor Equipment Shop - Pickup Point - Pastry Shop - Pawnbroker - Pet Store - Pet Grooming - Photo Shop - Rental Shop - Bicycle Rental Shop - Fishmonger - Second Hand Shop - Shoe Shop - Sports Shop - Stationery Shop - Supermarket - Tattoo Parlour - Tea Shop - Ticket Shop - Toy Store - Travel Agency - Tyre Shop - Variety Store - Video Shop - Video Game Shop - Wine Shop - Agricultural Shop - Antiques Shop - Appliance Shop - - Artwork Shop - Baby Goods Shop - Bag Shop - Bed Shop - Boutique - Charity Shop - Cheese Shop - Craft Supplies Store - Dairy Shop - Electrical Supplies Store - Fishing Store - Interior Decorations Store - Lottery Tickets - Medical Supplies Store - Nutrition Supplement Store - Paint Shop - Perfume Shop - Sewing Supplies Shop - Storage Rental - Smoke Shop - Trade Supplies - Watch Store - Wholesale Store - Sport - American Football - Archery - Athletics - Australian Football - Baseball - Basketball - Beach Volleyball - Bowls - Chess - Cricket - Curling - Equestrian Sports - Golf - Gymnastics - Handball - Various Sports - - Scuba Diving Site - Shooting - Skateboarding - Skiing - Soccer - Swimming - Table Tennis - Tennis Court - Volleyball - Bowling - Bowling - Padel - Futsal - Ice Hockey - Field Hockey - Badminton - Basque Pelota - Tourism - Aquarium - - Mountain Lodge - Holiday Apartment - Artwork - Architectural Artwork - Painting - Sculpture - Statue - Attraction - Animal Enclosure - Attraction - Campground - RV Park - - Holiday Cottage - Art Gallery - Guest House - Hostel - Hotel - Tourist Information - Information Board - Guidepost - Tourist Map - Tourist Office - Visitor Centre - Motel - Museum - Picnic Site - Resort - Theme Park - Viewpoint - - Wilderness Hut - Zoo - Petting Zoo - Traffic Calming - Traffic Bump - Traffic Hump - Waterway - Canal - Canal Tunnel - Fish Pass - Dam - Ditch - Drainage Ditch - Culvert - Waterway Dock - Drain - Drain - Culvert - Lock Gate - River - River - Stream - Ephemeral Stream - Intermittent Stream - Stream - Waterfall - Weir - Wheelchair - Limited Wheelchair Access - No Wheelchair Access - Full Wheelchair Access - J-bar Lift - Magic Carpet - Platter Lift - Rope Tow - T-bar Lift - Downhill Ski Run - Downhill Ski Run - Advanced Downhill Ski Run - Advanced Downhill Ski Run - Easy Downhill Ski Run - Easy Downhill Ski Run - Expert Downhill Ski Run - Expert Downhill Ski Run - Freeride Downhill Ski Run - Intermediate Downhill Ski Run - Intermediate Downhill Ski Run - Novice Downhill Ski Run - Novice Downhill Ski Run - Nordic Ski Trail - Sledding Piste - Sledding Piste - Snow Park - Snow Hiking Trail - Piste Connection - Skitour Trail - Events Venue - Auction - Collectables - //todo - Committee for Tourism Development under the Government of the Republic of Tajikistan - Error - Please wait, the map of Tajikistan is loading, stay in the app - Welcome to Tajikistan - Log in - Registration - Entry - Registration - Login - Full name - Country - Repeat the password - Home - Favorites - Account - Edit - Found it - Popular in Tajikistan - Popular in - Description - Photogallery - Feedbacks - Leave feedback - View all - Unfold - Feedback - Click to rate: - Text - Upload photo - Send - Profile - USD - EUR - RUB - Personal information - Language - English - Dark theme - Light theme - Exit - Exit - Are you sure you want to get out? - Edit data - Phone number - Select a language - Try again - Couldn\'t reach the server, please check connection - No image - Tajikistan - Clear search field - Top 30 places - Sights - Restaurants - Hotels + + Historic Object + Historic Aircraft + Historic Anchor + Archaeological Site + Historic Battlefield + Boundary Stone + Cannon + Castle + Roman Fort + Stronghold Castle + Fortified Church + Fortress + Hillfort + Kremlin + Manor House + Palace + Japanese Castle + Stately Castle + City Gate + City Wall + Fort + Gallows + Historic Locomotive + Memorial + Memorial Cross + Commemorative Plaque + Sculpture + Statue + Stolperstein + Historic Stone + War Memorial + Historic Mine + Monument + Pillory + Historic Ruins + Ship + Historic Tank + Tomb + Wayside Cross + Wayside Shrine + Shipwreck + hwtag + hwtag-bidir_bicycle + hwtag-onedir_bicycle + hwtag-lit + hwtag-nobicycle + hwtag-nocar + hwtag-nofoot + hwtag-oneway + hwtag-private + hwtag-toll + hwtag-yesbicycle + hwtag-yescar + hwtag-yesfoot + Internet + Internet + Junction + Roundabout + Roundabout + Landuse + Allotments + Basin + Brownfield + + Cemetery + + Christian Cemetery + Churchyard + Commercial Area + Construction Area + Educational Facility + Farmland + Farmyard + Field + Flowerbed + Forest + Coniferous Forest + Deciduous Forest + Mixed-Leaf Forest + Garages + Grass + Greenfield + Greenhouse + Industrial Area + Landfill + Meadow + Military Area + Orchard + Quarry + Railway Premises + Recreation Ground + Reservoir + Residential Area + Retail Area + Salt Pond + Land + Vineyard + Leisure + Public Land + Dog Park + Fitness Centre + Fitness Station + Dance Hall + Garden + Residential Garden + Golf Course + Minigolf + Hackerspace + Ice Rink + Marina + Nature Reserve + Outdoor Seating + Park + Private Park + Park + Private Park + Picnic Table + Sport Pitch + Playground + Recreation Ground + Sauna + Slipway + Sports Center + Climbing Centre + Yoga Studio + Stadium + Swimming Pool + Swimming Pool + Track + Track + Water Park + Beach Resort + Man Made + Breakwater + Cairn + Chimney + Cutline + Survey Point + Flagpole + Lighthouse + Mast + Pier + Pipeline + Overground Pipeline + Silo + Storage Tank + Surveillance Camera + Tower + Communications Tower + Wastewater Treatment Plant + Water Tap + Water Tower + Water Well + Windmill + Industrial Works + mapswithme + mapswithme-grid + Military + Bunker + Mountain Pass + Nature + + Bare Rock + + Shingle + + Scree + Bay + Beach + Sandy Beach + Gravel Beach + Cape + Cave Entrance + Cliff + Earth Bank + Embankment + Coastline + Desert + Geyser + Glacier + Grassland + Heath + Hot Spring + Lake + Lock Chamber + Pond + Reservoir + Basin + River + Land + Meadow + Orchard + Peak + Mountain Saddle + Rock + Scrub + Natural Spring + Strait + Tree Row + Vineyard + Volcano + Water + Wetland + Bog + Marsh + Dead End + Office + Company Office + Estate Agent + Government Office + Insurance Office + Lawyer + Non-Governmental Organization + Telecom Company + Organic + Organic + City + Capital + City + City + Capital + City + City + City + City + City + City + City + Continent + Country + County + Farm + Hamlet + Island + Islet + Isolated Dwelling + Locality + Neighbourhood + Ocean + Region + Sea + Square + State + State + Suburb + Town + Village + Power + Power Generator + Solar Generator + Wind Generator + Gas Turbine Power Plant + Hydroelectric Power Plant + Power Line + Underground Power Line + Minor Power Line + Power Plant + Coal Power Plant + Gas Turbine Power Plant + Hydroelectric Power Plant + Solar Power Plant + Wind Power Plant + Power Station + Substation + Power Tower + psurface + psurface-paved_bad + psurface-paved_good + psurface-unpaved_bad + psurface-unpaved_good + Public Transport + Platform + Railway + Abandoned Railway + Abandoned Railway Bridge + Abandoned Railway Tunnel + Railway Construction + Railway Crossing + Disused Railway + Funicular + Funicular Bridge + Funicular Tunnel + Rail Halt + Level Crossing + Light Rail + Light Rail Bridge + Light Rail Tunnel + Monorail + Monorail Bridge + Monorail Tunnel + Narrow Gauge Rail + Narrow Gauge Rail Bridge + Narrow Gauge Rail Tunnel + Railway Platform + Preserved Rail + Preserved Rail Bridge + Preserved Rail Tunnel + Railway + High-Speed Railway + Touristic Railway + Railway + + Railway Branch + + Utility Railway + Railway Spur + + Service Rail Track + Railway Bridge + Railway Bridge + Railway Bridge + Railway Bridge + Railway Bridge + Railway Bridge + Railway Bridge + Railway Bridge + Railway Tunnel + Railway Tunnel + Railway Tunnel + Railway Tunnel + Railway Tunnel + Railway Tunnel + Railway Tunnel + Railway Tunnel + Train Station + Funicular + Light Rail Station + DLR Station + Porto Metro + Monorail Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Underground Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Station + Subway Line + Subway Line Bridge + Subway Line Tunnel + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Metro Station Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Subway Entrance + Tram Line + Tram Line Bridge + Tram Line Tunnel + Tram Stop + Route + Ferry + route-shuttle_train + Shop + Liquor Shop + Bakery + Bathroom Furnishings + Beauty Shop + Beverages + Bicycle Shop + Bookmaker + Bookstore + Butcher + Cannabis Shop + Car Dealership + Car Parts Shop + Car Repair Workshop + Tyre Repair + RV Dealership + Carpet Shop + Chemist + Chocolate Shop + Clothes Shop + Coffee Shop + Computer Store + Candy Shop + Convenience Store + Copyshop + Cosmetics Shop + Curtain Shop + Delicatessen + Department Store + Home Improvement Store + Dry Cleaner + Electronics Shop + Erotic Shop + Fabric Shop + Farm Food Shop + Fashion Accessories + Florist + Funeral Directors + Furniture Store + Garden Center + Gas Store + Gift Shop + Greengrocer + Grocery Store + Hairdresser + Hardware Store + Health Food Shop + Herbalist + HiFi Audio Shop + Housewares Store + Jewelry Store + Kiosk + Kitchen Store + Laundry + Mall + Massage Salon + Cell Phone Store + Money Lender + Motorcycle Shop + Motorcycle Repair + Record Store + Musical Instrument Shop + Newspaper Stand + Optician + Outdoor Equipment Shop + Pickup Point + Pastry Shop + Pawnbroker + Pet Store + Pet Grooming + Photo Shop + Rental Shop + Bicycle Rental Shop + Fishmonger + Second Hand Shop + Shoe Shop + Sports Shop + Stationery Shop + Supermarket + Tattoo Parlour + Tea Shop + Ticket Shop + Toy Store + Travel Agency + Tyre Shop + Variety Store + Video Shop + Video Game Shop + Wine Shop + Agricultural Shop + Antiques Shop + Appliance Shop + + Artwork Shop + Baby Goods Shop + Bag Shop + Bed Shop + Boutique + Charity Shop + Cheese Shop + Craft Supplies Store + Dairy Shop + Electrical Supplies Store + Fishing Store + Interior Decorations Store + Lottery Tickets + Medical Supplies Store + Nutrition Supplement Store + Paint Shop + Perfume Shop + Sewing Supplies Shop + Storage Rental + Smoke Shop + Trade Supplies + Watch Store + Wholesale Store + Sport + American Football + Archery + Athletics + Australian Football + Baseball + Basketball + Beach Volleyball + Bowls + Chess + Cricket + Curling + Equestrian Sports + Golf + Gymnastics + Handball + Various Sports + + Scuba Diving Site + Shooting + Skateboarding + Skiing + Soccer + Swimming + Table Tennis + Tennis Court + Volleyball + Bowling + Bowling + Padel + Futsal + Ice Hockey + Field Hockey + Badminton + Basque Pelota + Tourism + Aquarium + + Mountain Lodge + Holiday Apartment + Artwork + Architectural Artwork + Painting + Sculpture + Statue + Attraction + Animal Enclosure + Attraction + Campground + RV Park + + Holiday Cottage + Art Gallery + Guest House + Hostel + Hotel + Tourist Information + Information Board + Guidepost + Tourist Map + Tourist Office + Visitor Centre + Motel + Museum + Picnic Site + Resort + Theme Park + Viewpoint + + Wilderness Hut + Zoo + Petting Zoo + Traffic Calming + Traffic Bump + Traffic Hump + Waterway + Canal + Canal Tunnel + Fish Pass + Dam + Ditch + Drainage Ditch + Culvert + Waterway Dock + Drain + Drain + Culvert + Lock Gate + River + River + Stream + Ephemeral Stream + Intermittent Stream + Stream + Waterfall + Weir + Wheelchair + Limited Wheelchair Access + No Wheelchair Access + Full Wheelchair Access + J-bar Lift + Magic Carpet + Platter Lift + Rope Tow + T-bar Lift + Downhill Ski Run + Downhill Ski Run + Advanced Downhill Ski Run + Advanced Downhill Ski Run + Easy Downhill Ski Run + Easy Downhill Ski Run + Expert Downhill Ski Run + Expert Downhill Ski Run + Freeride Downhill Ski Run + Intermediate Downhill Ski Run + Intermediate Downhill Ski Run + Novice Downhill Ski Run + Novice Downhill Ski Run + Nordic Ski Trail + Sledding Piste + Sledding Piste + Snow Park + Snow Hiking Trail + Piste Connection + Skitour Trail + Events Venue + Auction + Collectables + //todo + Committee for Tourism Development under the Government of the Republic of Tajikistan + Error + Please wait, the map of Tajikistan is loading, stay in the app + Welcome to Tajikistan + Log in + Registration + Entry + Registration + Login + Full name + Country + Repeat the password + Home + Favorites + Account + Edit + Found it + Popular in Tajikistan + Popular in + Description + Photogallery + Feedbacks + Leave feedback + All reviews + Unfold + Show less + Feedback + Click to rate: + Text + Upload photo + Send + Profile + USD + EUR + RUB + Personal information + Language + English + Dark theme + Light theme + Exit + Exit + Are you sure you want to get out? + Edit data + Phone number + Select a language + Try again + Couldn\'t reach the server, please check connection + No image + Tajikistan 🫠🌸☮ + Clear search field + Top 30 places + Sights + Restaurants + Hotels Add to favorites + Show route