do place details (including reviews), favorites UI/UX

This commit is contained in:
Emin 2024-06-26 16:37:33 +05:00
parent ab8677439f
commit 1d6e96e1fe
70 changed files with 4201 additions and 2429 deletions

View file

@ -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

View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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<String> = emptyList(),
val reviews: List<Review> = emptyList(),
val isFavorite: Boolean = false,
)

View file

@ -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<String> = emptyList(),
)

View file

@ -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,
)

View file

@ -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

View file

@ -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,
) {

View file

@ -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

View file

@ -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))
fun VerticalSpace(height: Dp) = Spacer(modifier = Modifier.height(height))

View file

@ -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")
}
)
}

View file

@ -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

View file

@ -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

View file

@ -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<TopBarActionData> = emptyList()
) {
Column(

View file

@ -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),

View file

@ -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))
}
}
}

View file

@ -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,
)
}
}

View file

@ -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,
)
)
}

View file

@ -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<CountryCodePicker>(R.id.ccp)
ccp.contentColor = contentColor
ccp.setCountryForNameCode(countryCodeName)
ccp.showArrow(false)
ccp.setCcpClickable(false)
view
}
)
}

View file

@ -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,

View file

@ -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()
)
}
}
}

View file

@ -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,
)
}

View file

@ -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()
}
}

View file

@ -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,

View file

@ -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

View file

@ -43,7 +43,7 @@ import com.hbb20.CountryCodePicker
@Composable
fun SignUpScreen(
onSignUpComplete: () -> Unit,
onBackClick: () -> Boolean,
onBackClick: () -> Unit,
vm: SignUpViewModel = hiltViewModel(),
) {
val context = LocalContext.current

View file

@ -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

View file

@ -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)

View file

@ -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<Favorites> {
@ -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<Profile> {

View file

@ -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)
}
}

View file

@ -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()
) {

View file

@ -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,

View file

@ -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<SingleChoiceItem>,
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)

View file

@ -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()
}
}
}
}
}

View file

@ -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<UiEvent>()
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<List<PlaceShort>>(emptyList())
val places = _places.asStateFlow()
fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) {
// todo
}
init {
// todo replace with real data
val dummyData = mutableListOf<PlaceShort>()
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
}

View file

@ -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(),

View file

@ -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,

View file

@ -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()

View file

@ -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,

View file

@ -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)
}
}
}
}
}
}
}
}

View file

@ -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
)
}

View file

@ -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<UiEvent>()
val uiEventsChannelFlow = uiChannel.receiveAsFlow()
private val _place = MutableStateFlow<PlaceFull?>(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 =
"""
<!DOCTYPE html>
<html lang="tg">
<head>
<meta charset="UTF-8">
</head>
<body>
<h2>Гиссарская крепость</h2>
<p> 4,8 работает каждый день, с 8:00 по 17:00</p>
<h3>О месте</h3>
<p>Город республиканского подчинения в западной части Таджикистана, в 20 километрах от столицы.</p>
<p>Город славится историческими достопримечательностями, например, Гиссарской крепостью, которая считается одним из самых известных исторических сооружений в Центральной Азии.</p>
<h3>Адрес</h3>
<p>районы республиканского подчинения, город Гиссар с административной территорией, джамоат Хисор, село Гиссар</p>
<h3>Контакты</h3>
<p>+992 998 201 201</p>
</body>
</html>
""".trimIndent()

View file

@ -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() },
)
}
}
}

View file

@ -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<String>, 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
)
}
}
}
}

View file

@ -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<String>) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Gallery) {
composable<Gallery> {
GalleryScreen(
urls = urls,
onMoreClick = {
navController.navigate(AllGallery)
},
)
}
composable<AllGallery> {
AllGalleryScreen(
urls = urls,
onBackClick = {
navController.navigateUp()
},
)
}
}
}

View file

@ -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<String>, 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,
)
}
}
}
}
}
}
}

View file

@ -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)

View file

@ -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<String>) -> 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)
}
}
}
}

View file

@ -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<UiEvent>()
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<List<File>>(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() {
}
}

View file

@ -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<String>, 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
)
}
}
}
}

View file

@ -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<String>)
@Composable
fun ReviewsNavigation(
placeId: Long,
rating: Double?,
reviewsVM: ReviewsViewModel = hiltViewModel(),
) {
val navController = rememberNavController()
val onBackClick: () -> Unit = { navController.navigateUp() }
val onMoreClick: (picsUrls: List<String>) -> Unit = {
navController.navigate(ReviewPics(urls = it))
}
NavHost(navController = navController, startDestination = Reviews) {
composable<Reviews> {
ReviewsScreen(
placeId,
rating,
onSeeAllClick = {
navController.navigate(AllReviews)
},
onMoreClick = onMoreClick,
reviewsVM = reviewsVM,
)
}
composable<AllReviews> {
AllReviewsScreen(
reviewsVM = reviewsVM,
onBackClick = onBackClick,
onMoreClick = onMoreClick
)
}
composable<ReviewPics> { navBackStackEntry ->
val reviewPics = navBackStackEntry.toRoute<ReviewPics>()
ReviewPicsScreen(
urls = reviewPics.urls,
onBackClick = onBackClick
)
}
}
}

View file

@ -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<String>) -> 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()
}
}

View file

@ -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<UiEvent>()
val uiEventsChannelFlow = uiChannel.receiveAsFlow()
private val _reviews = MutableStateFlow<List<Review>>(emptyList())
val reviews = _reviews.asStateFlow()
private val _userReview = MutableStateFlow<Review?>(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)
)
)
}
}

View file

@ -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
}

View file

@ -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)

View file

@ -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<String>) -> 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))

View file

@ -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()

View file

@ -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<CountryCodePicker>(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

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -0,0 +1,7 @@
package app.tourism.utils
fun <T> makeLongListOfTheSameItem(item: T, itemsNum: Int = 20): List<T> {
val list = mutableListOf<T>()
repeat(itemsNum) { list.add(item) }
return list
}

View file

@ -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
)
}
}
}

View file

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="11dp"
android:viewportWidth="10"
android:viewportHeight="11">
android:width="30dp"
android:height="26dp"
android:viewportWidth="30"
android:viewportHeight="26">
<path
android:pathData="M0.168,4.668C0.132,4.636 0.107,4.594 0.095,4.548C0.083,4.502 0.084,4.453 0.099,4.408C0.114,4.362 0.141,4.322 0.178,4.292C0.215,4.261 0.26,4.242 0.308,4.236L3.31,3.881C3.352,3.876 3.393,3.86 3.427,3.835C3.462,3.809 3.49,3.776 3.508,3.737L4.774,0.992C4.794,0.949 4.826,0.912 4.866,0.886C4.906,0.86 4.953,0.847 5.001,0.847C5.048,0.847 5.095,0.86 5.135,0.886C5.175,0.912 5.208,0.949 5.228,0.992L6.494,3.737C6.511,3.776 6.539,3.809 6.574,3.835C6.608,3.86 6.649,3.875 6.691,3.881L9.693,4.236C9.74,4.242 9.785,4.261 9.822,4.292C9.859,4.322 9.887,4.362 9.901,4.408C9.916,4.453 9.917,4.502 9.905,4.548C9.893,4.594 9.868,4.636 9.833,4.668L7.614,6.721C7.583,6.75 7.559,6.787 7.546,6.827C7.533,6.868 7.531,6.911 7.539,6.953L8.128,9.918C8.137,9.965 8.133,10.014 8.115,10.058C8.098,10.102 8.068,10.141 8.029,10.169C7.991,10.197 7.945,10.214 7.897,10.217C7.849,10.219 7.802,10.208 7.76,10.185L5.123,8.708C5.085,8.688 5.043,8.677 5.001,8.677C4.958,8.677 4.916,8.688 4.879,8.708L2.241,10.184C2.199,10.208 2.151,10.219 2.104,10.216C2.056,10.213 2.01,10.197 1.972,10.169C1.933,10.141 1.903,10.102 1.886,10.058C1.868,10.013 1.864,9.965 1.873,9.918L2.462,6.953C2.47,6.911 2.468,6.868 2.455,6.827C2.442,6.787 2.418,6.75 2.387,6.721L0.168,4.668Z"
android:fillColor="#F8D749"/>
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"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="26dp"
android:viewportWidth="30"
android:viewportHeight="26">
<path
android:strokeWidth="1"
android:pathData="M27.417,12.678L27.424,12.677L22.907,16.179L22.646,16.381L22.73,16.701L24.567,23.692L24.568,23.694C24.641,23.967 24.622,24.253 24.513,24.519C24.404,24.786 24.205,25.026 23.933,25.204C23.66,25.382 23.33,25.486 22.984,25.499C22.638,25.511 22.298,25.431 22.009,25.272C22.009,25.272 22.008,25.272 22.008,25.272L15.242,21.531L15,21.397L14.758,21.531L7.989,25.271C7.989,25.272 7.989,25.272 7.988,25.272C7.699,25.43 7.36,25.509 7.015,25.496C6.669,25.483 6.339,25.378 6.068,25.201C5.796,25.023 5.598,24.783 5.488,24.517C5.379,24.251 5.361,23.966 5.433,23.693L5.433,23.692L7.277,16.701L7.362,16.381L7.1,16.179L1.07,11.504C1.07,11.504 1.07,11.504 1.07,11.504C0.818,11.309 0.644,11.057 0.559,10.785C0.475,10.514 0.481,10.228 0.577,9.96C0.674,9.691 0.86,9.444 1.122,9.257L0.837,8.859L1.122,9.257C1.383,9.07 1.706,8.952 2.05,8.924C2.051,8.924 2.051,8.924 2.052,8.924L9.955,8.352L10.25,8.33L10.373,8.062L13.423,1.431L13.424,1.43C13.544,1.166 13.754,0.931 14.034,0.762L13.778,0.336L14.034,0.762C14.315,0.593 14.65,0.5 14.997,0.5C15.344,0.5 15.679,0.593 15.96,0.762C16.24,0.931 16.45,1.166 16.57,1.43L16.571,1.431L19.619,8.062L19.743,8.33L20.038,8.352L27.942,8.924C27.942,8.924 27.943,8.925 27.943,8.925C28.288,8.951 28.612,9.068 28.875,9.255C29.137,9.442 29.324,9.689 29.422,9.958C29.519,10.227 29.526,10.514 29.441,10.785C29.357,11.057 29.182,11.309 28.929,11.505L27.417,12.678Z"
android:fillColor="#00000000"
android:strokeColor="#F9C236"/>
</vector>

View file

@ -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"

View file

@ -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"

View file

@ -2183,8 +2183,9 @@
<string name="gallery">Фотогаллерея</string>
<string name="reviews">Отзывы</string>
<string name="compose_review">Оставить отзыв</string>
<string name="see_all">Посмотреть все</string>
<string name="see_all">Все отзывы</string>
<string name="more">Развернуть</string>
<string name="less">Свернуть</string>
<string name="review_title">Отзыв</string>
<string name="tap_to_rate">Нажмите, чтобы оценить:</string>
<string name="text">Текст</string>
@ -2215,4 +2216,5 @@
<string name="restaurants">Рестораны</string>
<string name="hotels_tourism">Отели</string>
<string name="add_to_favorites">Добавить в избранное</string>
<string name="show_route">Посмотреть маршрут</string>
</resources>

File diff suppressed because it is too large Load diff