do home, categories, search UI/UX

This commit is contained in:
Emin 2024-06-24 14:04:48 +05:00
parent ab44d68eac
commit ab8677439f
29 changed files with 1090 additions and 93 deletions

View file

@ -109,7 +109,7 @@ import app.organicmaps.widget.menu.MainMenu;
import app.organicmaps.widget.placepage.PlacePageController;
import app.organicmaps.widget.placepage.PlacePageData;
import app.organicmaps.widget.placepage.PlacePageViewModel;
import app.tourism.data.dto.SiteLocation;
import app.tourism.data.dto.PlaceLocation;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -551,7 +551,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
tjkMapDownloadingHandling();
SiteLocation endPoint = getIntent().getParcelableExtra("end_point");
PlaceLocation endPoint = getIntent().getParcelableExtra("end_point");
if(endPoint != null)
routeForSiteFromMainActivityHandling(endPoint);
}
@ -570,7 +570,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
handler.postDelayed(delayedAction, 1000);
}
private void routeForSiteFromMainActivityHandling(SiteLocation endPoint) {
private void routeForSiteFromMainActivityHandling(PlaceLocation endPoint) {
Handler handler = new Handler(Looper.getMainLooper());
Runnable delayedAction = () -> {
showRouteForSiteFromMainActivity(endPoint);
@ -578,7 +578,7 @@ public class MwmActivity extends BaseMwmFragmentActivity
handler.postDelayed(delayedAction, 1000);
}
private void showRouteForSiteFromMainActivity(SiteLocation endPoint) {
private void showRouteForSiteFromMainActivity(PlaceLocation endPoint) {
startLocationToPoint(endPoint.toMapObject());
}

View file

@ -25,7 +25,6 @@ import app.organicmaps.util.ConnectionState;
import app.organicmaps.util.StringUtils;
import app.organicmaps.util.UiUtils;
import app.tourism.MainActivity;
import app.tourism.data.dto.SiteLocation;
import java.util.List;

View file

@ -6,7 +6,12 @@ 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.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
@ -31,11 +36,32 @@ object Constants {
"https://cdn.pixabay.com/photo/2020/03/24/22/34/illustration-4965674_960_720.jpg"
const val LOGO_URL_EXAMPLE = "https://brandeps.com/logo-download/O/OSCE-logo-vector-01.svg"
// data
val categories = mapOf(
"sights" to R.string.sights,
"restaurants" to R.string.restaurants,
"hotels_tourism" to R.string.hotels_tourism,
)
}
@Composable
fun Modifier.applyAppBorder() = this.border(
width = 1.dp,
color = getBorderColor(),
shape = RoundedCornerShape(20.dp)
).clip(RoundedCornerShape(20.dp))
fun Modifier.applyAppBorder() = this
.border(
width = 1.dp,
color = getBorderColor(),
shape = RoundedCornerShape(20.dp)
)
.clip(RoundedCornerShape(20.dp))
@Composable
fun Modifier.drawOverlayForTextBehind() =
this.drawBehind {
val colors = listOf(
Color.Black,
Color.Transparent
)
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}

View file

@ -6,7 +6,7 @@ import app.organicmaps.bookmarks.data.MapObject
import kotlinx.parcelize.Parcelize
@Parcelize
data class SiteLocation(val name: String, val lat: Double, val lon: Double) : Parcelable {
data class PlaceLocation(val name: String, val lat: Double, val lon: Double) : Parcelable {
fun toMapObject() = MapObject.createMapObject(
FeatureId.EMPTY, MapObject.POI, name, "", lat, lon
);

View file

@ -0,0 +1,3 @@
package app.tourism.domain.models.categories
data class Category(val value: String?, val label: String)

View file

@ -0,0 +1,10 @@
package app.tourism.domain.models.common
data class PlaceShort(
val id: Int,
val name: String,
val pic: String? = null,
val rating: Double? = null,
val excerpt: String? = null,
val isFavorite: Boolean = false,
)

View file

@ -0,0 +1,15 @@
package app.tourism.domain.models.details
import app.tourism.data.dto.PlaceLocation
data class PlaceFull(
val id: Int,
val name: String,
val rating: Double? = null,
val excerpt: String? = null,
val description: String? = null,
val placeLocation: PlaceLocation? = null,
val pic: String? = null,
val pics: List<String> = emptyList(),
val reviews: List<Review> = emptyList(),
)

View file

@ -0,0 +1,11 @@
package app.tourism.domain.models.details
data class Review(
val rating: Double? = null,
val name: String,
val pfpUrl: String? = null,
val countryCodeName: String? = null,
val date: String? = null,
val text: String? = null,
val picsUrls: List<String> = emptyList(),
)

View file

@ -0,0 +1,39 @@
package app.tourism.ui.common
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
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.unit.dp
import app.tourism.ui.theme.TextStyles
@Composable
fun BorderedItem(
modifier: Modifier = Modifier,
label: String,
highlighted: Boolean = false,
onClick: () -> Unit
) {
val shape = RoundedCornerShape(16.dp)
Text(
modifier = Modifier
.background(
color = if (highlighted) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.background,
shape = shape
)
.clip(shape)
.clickable {
onClick()
}
.padding(12.dp)
.then(modifier),
text = label,
style = TextStyles.h4
)
}

View file

@ -0,0 +1,81 @@
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
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.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
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.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import app.organicmaps.R
import app.tourism.ui.theme.TextStyles
import app.tourism.ui.theme.getHintColor
@Composable
fun AppSearchBar(
modifier: Modifier = Modifier,
query: String,
onQueryChanged: (String) -> Unit,
onSearchClicked: (String) -> Unit,
onClearClicked: () -> Unit,
) {
var isActive by remember { mutableStateOf(false) }
val searchLabel = stringResource(id = R.string.search)
OutlinedTextField(
modifier = Modifier
.clickable { isActive = true }
.then(modifier),
value = query,
onValueChange = onQueryChanged,
placeholder = {
Text(
text = searchLabel,
style = TextStyles.h4.copy(color = getHintColor()),
)
},
singleLine = true,
maxLines = 1,
leadingIcon = {
IconButton(onClick = { onSearchClicked(query) }) {
Icon(
painter = painterResource(id = R.drawable.search),
contentDescription = searchLabel,
tint = getHintColor()
)
}
},
trailingIcon = {
if (query.isNotEmpty())
IconButton(onClick = { onClearClicked() }) {
Icon(
painter = painterResource(id = R.drawable.ic_clear_rounded),
contentDescription = stringResource(id = R.string.clear_search_field),
)
}
},
shape = RoundedCornerShape(16.dp),
keyboardActions = KeyboardActions(onSearch = { onSearchClicked(query) }),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedIndicatorColor = MaterialTheme.colorScheme.surface
),
)
}

View file

@ -0,0 +1,109 @@
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
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
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.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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import app.organicmaps.R
import app.tourism.applyAppBorder
import app.tourism.domain.models.common.PlaceShort
import app.tourism.ui.common.HorizontalSpace
import app.tourism.ui.common.LoadImg
import app.tourism.ui.theme.HeartRed
import app.tourism.ui.theme.TextStyles
import app.tourism.ui.theme.getStarColor
@Composable
fun PlacesItem(
modifier: Modifier = Modifier,
place: PlaceShort,
onPlaceClick: () -> Unit,
isFavorite: Boolean,
onFavoriteChanged: (Boolean) -> Unit
) {
val height = 130.dp
val shape = RoundedCornerShape(20.dp)
Row(
Modifier
.fillMaxWidth()
.height(height)
.applyAppBorder()
.clip(shape)
.clickable { onPlaceClick() }
.then(modifier)
) {
LoadImg(modifier = Modifier
.size(height)
.clip(shape), url = place.pic)
Column(
Modifier
.fillMaxHeight(0.9f)
.padding(8.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = place.name,
style = TextStyles.h3,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
IconButton(
modifier = Modifier.size(20.dp),
onClick = {
onFavoriteChanged(!isFavorite)
},
) {
Icon(
painterResource(id = if (isFavorite) R.drawable.heart_selected else R.drawable.heart),
contentDescription = stringResource(id = R.string.add_to_favorites),
tint = HeartRed,
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "%.1f".format(place.rating), style = TextStyles.b1)
HorizontalSpace(width = 8.dp)
Icon(
modifier = Modifier.size(12.dp),
painter = painterResource(id = R.drawable.star),
contentDescription = null,
tint = getStarColor(),
)
}
place.excerpt?.let {
Text(
text = Html.fromHtml(it).toString(),
style = TextStyles.b1,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}

View file

@ -0,0 +1,6 @@
package app.tourism.ui.models
data class SingleChoiceItem(
val key: String?,
val label: String
)

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import app.organicmaps.R
import app.tourism.Constants
import app.tourism.drawOverlayForTextBehind
import app.tourism.ui.common.HorizontalSpace
import app.tourism.ui.common.VerticalSpace
import app.tourism.ui.common.buttons.PrimaryButton
@ -63,16 +64,7 @@ fun WelcomeScreen(
Column(
Modifier
.align(Alignment.BottomStart)
.drawBehind {
val colors = listOf(
Color.Black,
Color.Transparent
)
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn
)
}
.drawOverlayForTextBehind()
.padding(Constants.SCREEN_PADDING)
) {
Text(

View file

@ -2,10 +2,12 @@ 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
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@ -15,12 +17,14 @@ import app.tourism.AuthActivity
import app.tourism.ui.screens.auth.Language
import app.tourism.ui.screens.language.LanguageScreen
import app.tourism.ui.screens.main.categories.categories.CategoriesScreen
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.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.site_details.SiteDetailsScreen
import app.tourism.ui.screens.main.place_details.PlaceDetailsScreen
import app.tourism.utils.navigateToMap
import app.tourism.utils.navigateToMapForRoute
import kotlinx.serialization.Serializable
@ -47,40 +51,55 @@ object Profile
@Serializable
object PersonalData
// site details
// place details
@Serializable
data class SiteDetails(val id: Int)
data class PlaceDetails(val id: Int)
@Composable
fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel) {
val context = LocalContext.current
val onSiteClick: (id: Int) -> Unit = { id ->
rootNavController.navigate(SiteDetails(id = id))
val categoriesVM: CategoriesViewModel = hiltViewModel()
val onPlaceClick: (id: Int) -> Unit = { id ->
rootNavController.navigate(PlaceDetails(id = id))
}
val onMapClick = { navigateToMap(context) }
NavHost(rootNavController, startDestination = "home_tab") {
composable("home_tab") {
HomeNavHost(onSiteClick, onMapClick)
HomeNavHost(
onPlaceClick,
onMapClick,
onCategoryClicked = {
rootNavController.navigate("categories_tab") {
popUpTo(rootNavController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
categoriesVM,
)
}
composable("categories_tab") {
CategoriesNavHost(onSiteClick, onMapClick)
CategoriesNavHost(onPlaceClick, onMapClick, categoriesVM)
}
composable("favorites_tab") {
FavoritesNavHost(onSiteClick)
FavoritesNavHost(onPlaceClick)
}
composable("profile_tab") {
ProfileNavHost(themeVM = themeVM)
}
composable<SiteDetails> { backStackEntry ->
val siteDetails = backStackEntry.toRoute<SiteDetails>()
SiteDetailsScreen(
id = siteDetails.id,
composable<PlaceDetails> { backStackEntry ->
val placeDetails = backStackEntry.toRoute<PlaceDetails>()
PlaceDetailsScreen(
id = placeDetails.id,
onBackClick = { rootNavController.navigateUp() },
onMapClick = onMapClick,
onCreateRoute = { siteLocation ->
navigateToMapForRoute(context, siteLocation)
onCreateRoute = { placeLocation ->
navigateToMapForRoute(context, placeLocation)
}
)
}
@ -88,7 +107,12 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel
}
@Composable
fun HomeNavHost(onSiteClick: (id: Int) -> Unit, onMapClick: () -> Unit) {
fun HomeNavHost(
onPlaceClick: (id: Int) -> Unit,
onMapClick: () -> Unit,
onCategoryClicked: () -> Unit,
categoriesVM: CategoriesViewModel,
) {
val homeNavController = rememberNavController()
NavHost(homeNavController, startDestination = Home) {
composable<Home> {
@ -96,33 +120,43 @@ fun HomeNavHost(onSiteClick: (id: Int) -> Unit, onMapClick: () -> Unit) {
onSearchClick = { query ->
homeNavController.navigate(Search(query = query))
},
onSiteClick = onSiteClick,
onMapClick = onMapClick
onPlaceClick = onPlaceClick,
onMapClick = onMapClick,
onCategoryClicked = onCategoryClicked,
categoriesVM = categoriesVM
)
}
composable<Search> { backStackEntry ->
val search = backStackEntry.toRoute<Search>()
Search(query = search.query)
SearchScreen(
onPlaceClick = onPlaceClick,
onMapClick = onMapClick,
queryArg = search.query,
)
}
}
}
@Composable
fun CategoriesNavHost(onSiteClick: (id: Int) -> Unit, onMapClick: () -> Unit) {
fun CategoriesNavHost(
onPlaceClick: (id: Int) -> Unit,
onMapClick: () -> Unit,
categoriesVM: CategoriesViewModel,
) {
val categoriesNavController = rememberNavController()
NavHost(categoriesNavController, startDestination = Categories) {
composable<Categories> {
CategoriesScreen(onSiteClick, onMapClick)
CategoriesScreen(onPlaceClick, onMapClick, categoriesVM)
}
}
}
@Composable
fun FavoritesNavHost(onSiteClick: (id: Int) -> Unit) {
fun FavoritesNavHost(onPlaceClick: (id: Int) -> Unit) {
val favoritesNavController = rememberNavController()
NavHost(favoritesNavController, startDestination = Favorites) {
composable<Favorites> {
FavoritesScreen(onSiteClick)
FavoritesScreen(onPlaceClick)
}
}
}

View file

@ -1,6 +1,7 @@
package app.tourism.ui.screens.main
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
@ -60,6 +61,7 @@ fun MainSection(themeVM: ThemeViewModel) {
) {
items.forEach { item ->
val isSelected = item.route == navBackStackEntry?.destination?.route
val title = stringResource(id = item.title)
NavigationBarItem(
colors = NavigationBarItemColors(
disabledIconColor = MaterialTheme.colorScheme.onPrimary,
@ -71,15 +73,16 @@ fun MainSection(themeVM: ThemeViewModel) {
selectedIndicatorColor = Color.Transparent,
),
selected = isSelected,
label = {
Text(text = item.title, style = TextStyles.b3)
Text(text = title, style = TextStyles.b3)
},
icon = {
Icon(
painter = painterResource(
if (isSelected) item.selectedIcon else item.unselectedIcon
),
contentDescription = item.title
contentDescription = title,
)
},
onClick = {
@ -103,7 +106,7 @@ fun MainSection(themeVM: ThemeViewModel) {
data class BottomNavigationItem(
val route: String,
val title: String,
@StringRes val title: Int,
@DrawableRes val unselectedIcon: Int,
@DrawableRes val selectedIcon: Int
)
@ -113,25 +116,25 @@ fun getNavItems(): List<BottomNavigationItem> {
return listOf(
BottomNavigationItem(
route = "home_tab",
title = stringResource(id = R.string.home),
title = R.string.home,
selectedIcon = R.drawable.home_selected,
unselectedIcon = R.drawable.home,
),
BottomNavigationItem(
route = "categories_tab",
title = stringResource(id = R.string.categories),
title = R.string.categories,
selectedIcon = R.drawable.categories_selected,
unselectedIcon = R.drawable.categories,
),
BottomNavigationItem(
route = "favorites_tab",
title = stringResource(id = R.string.favorites),
title = R.string.favorites,
selectedIcon = R.drawable.heart_selected,
unselectedIcon = R.drawable.heart,
),
BottomNavigationItem(
route = "profile_tab",
title = stringResource(id = R.string.profile_tourism),
title = R.string.profile_tourism,
selectedIcon = R.drawable.profile_selected,
unselectedIcon = R.drawable.profile,
),

View file

@ -1,38 +1,103 @@
package app.tourism.ui.screens.main.categories.categories
import androidx.compose.foundation.layout.Column
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
import app.organicmaps.R
import app.tourism.Constants
import app.tourism.ui.common.AppSearchBar
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.TopBarActionData
import app.tourism.ui.common.special.PlacesItem
@Composable
fun CategoriesScreen(
onSiteClick: (id: Int) -> Unit,
onPlaceClick: (id: Int) -> Unit,
onMapClick: () -> Unit,
categoriesVM: CategoriesViewModel = hiltViewModel()
) {
Scaffold(
topBar = {
AppTopBar(
title = stringResource(id = R.string.categories),
actions = listOf(
TopBarActionData(
iconDrawable = R.drawable.map,
color = MaterialTheme.colorScheme.primary,
onClick = onMapClick
),
),
)
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
// todo
categoriesVM.apply {
val query = query.collectAsState().value
val categories = categories.collectAsState().value
val selectedCategory = selectedCategory.collectAsState().value
val places = places.collectAsState().value
Scaffold(
topBar = {
AppTopBar(
title = stringResource(id = R.string.categories),
actions = listOf(
TopBarActionData(
iconDrawable = R.drawable.map,
color = MaterialTheme.colorScheme.primary,
onClick = onMapClick
),
),
)
},
contentWindowInsets = Constants.USUAL_WINDOW_INSETS
) { paddingValues ->
LazyColumn(
Modifier
.padding(paddingValues),
) {
item {
Column {
VerticalSpace(height = 16.dp)
AppSearchBar(
modifier = Modifier.fillMaxWidth(),
query = query,
onQueryChanged = ::setQuery,
onSearchClicked = ::search,
onClearClicked = ::clearSearchField,
)
VerticalSpace(height = 16.dp)
HorizontalSingleChoice(
items = categories,
selected = selectedCategory,
onSelectedChanged = ::setSelectedCategory,
)
VerticalSpace(height = 16.dp)
}
}
items(places) { item ->
Column {
PlacesItem(
place = item,
onPlaceClick = { onPlaceClick(item.id) },
isFavorite = item.isFavorite,
onFavoriteChanged = { isFavorite ->
setFavoriteChanged(item, isFavorite)
},
)
VerticalSpace(height = 16.dp)
}
}
item {
Column {
SpaceForNavBar()
}
}
}
}
}
}
}

View file

@ -0,0 +1,82 @@
package app.tourism.ui.screens.main.categories.categories
import androidx.lifecycle.ViewModel
import app.tourism.Constants
import app.tourism.domain.models.common.PlaceShort
import app.tourism.ui.models.SingleChoiceItem
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 CategoriesViewModel @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 _selectedCategory = MutableStateFlow<SingleChoiceItem?>(null)
val selectedCategory = _selectedCategory.asStateFlow()
fun setSelectedCategory(value: SingleChoiceItem?) {
_selectedCategory.value = value
}
private val _categories = MutableStateFlow<List<SingleChoiceItem>>(emptyList())
val categories = _categories.asStateFlow()
private val _places = MutableStateFlow<List<PlaceShort>>(emptyList())
val places = _places.asStateFlow()
fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) {
// todo
}
init {
// todo replace with real data
_selectedCategory.value = SingleChoiceItem("sights", "Sights")
_categories.value = listOf(
SingleChoiceItem("sights", "Sights"),
SingleChoiceItem("restaurants", "Restaurants"),
SingleChoiceItem("hotels", "Hotels"),
)
val dummyData = mutableListOf<PlaceShort>()
repeat(15) {
dummyData.add(
PlaceShort(
id = it,
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

@ -0,0 +1,67 @@
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.unit.dp
import app.tourism.applyAppBorder
import app.tourism.ui.common.HorizontalSpace
import app.tourism.ui.models.SingleChoiceItem
import app.tourism.ui.theme.TextStyles
@Composable
fun HorizontalSingleChoice(
modifier: Modifier = Modifier,
items: List<SingleChoiceItem>,
selected: SingleChoiceItem?,
onSelectedChanged: (SingleChoiceItem) -> Unit
) {
Row(Modifier.then(modifier)) {
items.forEach {
SingleChoiceItem(
item = it,
isSelected = it.key == selected?.key,
onClick = {
onSelectedChanged(it)
},
)
HorizontalSpace(width = 12.dp)
}
}
}
@Composable
private fun SingleChoiceItem(
modifier: Modifier = Modifier,
item: SingleChoiceItem,
isSelected: Boolean,
onClick: () -> Unit
) {
val shape = RoundedCornerShape(16.dp)
Text(
modifier = Modifier
.applyAppBorder()
.clickable {
onClick()
}
.clip(shape)
.background(
color = if (isSelected) MaterialTheme.colorScheme.surface
else MaterialTheme.colorScheme.background,
shape = shape
)
.padding(12.dp)
.then(modifier),
text = item.label,
style = TextStyles.h4
)
}

View file

@ -12,7 +12,7 @@ import app.tourism.ui.common.nav.TopBarActionData
@Composable
fun FavoritesScreen(
onSiteClick: (id: Int) -> Unit,
onPlaceClick: (id: Int) -> Unit,
) {
Scaffold(
topBar = {

View file

@ -1,32 +1,83 @@
package app.tourism.ui.screens.main.home.home
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
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.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
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
import androidx.compose.runtime.collectAsState
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.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import app.organicmaps.R
import app.tourism.Constants
import app.tourism.domain.models.common.PlaceShort
import app.tourism.drawOverlayForTextBehind
import app.tourism.ui.common.AppSearchBar
import app.tourism.ui.common.BorderedItem
import app.tourism.ui.common.HorizontalSpace
import app.tourism.ui.common.LoadImg
import app.tourism.ui.common.SpaceForNavBar
import app.tourism.ui.common.buttons.PrimaryButton
import app.tourism.ui.common.VerticalSpace
import app.tourism.ui.common.nav.AppTopBar
import app.tourism.ui.common.nav.TopBarActionData
import app.tourism.ui.screens.main.categories.categories.CategoriesViewModel
import app.tourism.ui.screens.main.categories.categories.HorizontalSingleChoice
import app.tourism.ui.theme.TextStyles
import app.tourism.ui.theme.getStarColor
@Composable
fun HomeScreen(
onSearchClick: (String) -> Unit,
onSiteClick: (id: Int) -> Unit,
onPlaceClick: (id: Int) -> Unit,
onMapClick: () -> Unit,
onCategoryClicked: () -> Unit,
homeVM: HomeViewModel = hiltViewModel(),
categoriesVM: CategoriesViewModel,
) {
val query = homeVM.query.collectAsState().value
val sights = homeVM.sights.collectAsState().value
val restaurants = homeVM.restaurants.collectAsState().value
LaunchedEffect(true) {
categoriesVM.setSelectedCategory(null)
}
Scaffold(
topBar = {
AppTopBar(
// todo remove hardcoded value
title = "Душанбе",
title = stringResource(id = R.string.tjk),
actions = listOf(
TopBarActionData(
iconDrawable = R.drawable.map,
@ -36,20 +87,187 @@ fun HomeScreen(
),
)
},
contentWindowInsets = Constants.USUAL_WINDOW_INSETS
contentWindowInsets = WindowInsets(left = 0.dp, right = 0.dp, top = 0.dp, bottom = 0.dp)
) { paddingValues ->
Column(
Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
// todo
PrimaryButton(label = "navigate to Site details screen", onClick = { onSiteClick(1) })
Column(Modifier.padding(horizontal = Constants.SCREEN_PADDING)) {
VerticalSpace(height = 16.dp)
repeat(50) {
Text(text = "sldkjfsdlkf")
AppSearchBar(
modifier = Modifier.fillMaxWidth(),
query = query,
onQueryChanged = { homeVM.setQuery(it) },
onSearchClicked = {
// search field will be cleared only here
// when it navigates to SearchScreen query value will be preserved there
homeVM.clearSearchField()
onSearchClick(it)
},
onClearClicked = { homeVM.clearSearchField() },
)
}
VerticalSpace(height = 16.dp)
Categories(categoriesVM, onCategoryClicked)
VerticalSpace(height = 24.dp)
HorizontalPlaces(
title = stringResource(id = R.string.sights),
items = sights,
onPlaceClick = { item ->
onPlaceClick(item.id)
},
setFavoriteChanged = { item, isFavorite ->
},
)
VerticalSpace(height = 24.dp)
HorizontalPlaces(
title = stringResource(id = R.string.restaurants),
items = restaurants,
onPlaceClick = { item ->
onPlaceClick(item.id)
},
setFavoriteChanged = { item, isFavorite ->
},
)
SpaceForNavBar()
}
}
}
@Composable
private fun Categories(categoriesVM: CategoriesViewModel, onCategoryClicked: () -> Unit) {
categoriesVM.apply {
val categories = categories.collectAsState().value
val selectedCategory = selectedCategory.collectAsState().value
Row(
Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
HorizontalSpace(width = 16.dp)
BorderedItem(
label = stringResource(id = R.string.top30),
highlighted = true,
onClick = { /*Nothing... Yes! Nothing!*/ },
)
HorizontalSpace(width = 12.dp)
HorizontalSingleChoice(
items = categories,
selected = selectedCategory,
onSelectedChanged = {
setSelectedCategory(it)
onCategoryClicked()
},
)
}
}
}
@Composable
private fun HorizontalPlaces(
modifier: Modifier = Modifier,
title: String,
items: List<PlaceShort>,
onPlaceClick: (PlaceShort) -> Unit,
setFavoriteChanged: (PlaceShort, Boolean) -> Unit,
) {
Column(Modifier.then(modifier)) {
Column(Modifier.padding(horizontal = Constants.SCREEN_PADDING)) {
Text(text = title, style = TextStyles.h2)
VerticalSpace(height = 12.dp)
}
LazyRow(contentPadding = PaddingValues(horizontal = 16.dp)) {
items(items) {
Row {
Place(
place = it,
onPlaceClick = { onPlaceClick(it) },
isFavorite = false,
onFavoriteChanged = { isFavorite ->
setFavoriteChanged(it, isFavorite)
},
)
HorizontalSpace(width = 12.dp)
}
}
}
}
}
@Composable
private fun Place(
modifier: Modifier = Modifier,
place: PlaceShort,
onPlaceClick: () -> Unit,
isFavorite: Boolean,
onFavoriteChanged: (Boolean) -> Unit
) {
val textStyle = TextStyle(
fontSize = 15.sp,
fontWeight = FontWeight.W600,
color = Color.White
)
Box(
modifier = Modifier
.width(230.dp)
.height(250.dp)
.clip(RoundedCornerShape(16.dp))
.clickable { onPlaceClick() }
.then(modifier),
) {
LoadImg(url = place.pic)
Column(
Modifier
.fillMaxWidth()
.drawOverlayForTextBehind()
.align(Alignment.BottomCenter)
.padding(12.dp),
) {
Text(
text = place.name,
style = textStyle,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "%.1f".format(place.rating), style = textStyle)
HorizontalSpace(width = 2.dp)
Icon(
modifier = Modifier.size(12.dp),
painter = painterResource(id = R.drawable.star),
contentDescription = null,
tint = getStarColor(),
)
}
}
IconButton(
modifier = Modifier
.padding(12.dp)
.background(Color.White.copy(alpha = 0.2f), CircleShape)
.align(Alignment.TopEnd),
onClick = {
onFavoriteChanged(!isFavorite)
},
) {
Icon(
painterResource(id = if (isFavorite) R.drawable.heart_selected else R.drawable.heart),
contentDescription = stringResource(id = R.string.add_to_favorites),
tint = Color.White,
)
}
}
}

View file

@ -0,0 +1,68 @@
package app.tourism.ui.screens.main.home.home
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 HomeViewModel @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 _sights = MutableStateFlow<List<PlaceShort>>(emptyList())
val sights = _sights.asStateFlow()
private val _restaurants = MutableStateFlow<List<PlaceShort>>(emptyList())
val restaurants = _restaurants.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,
name = "Гора Эмина",
pic = Constants.IMAGE_URL_EXAMPLE,
rating = 5.0,
excerpt = "завтрак включен, бассейн, сауна, с видом на озеро"
)
)
}
_sights.update { dummyData }
_restaurants.update { dummyData }
}
}
sealed interface UiEvent {
data class ShowToast(val message: String) : UiEvent
}

View file

@ -1,25 +1,51 @@
package app.tourism.ui.screens.main.home.search
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
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.AppSearchBar
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.TopBarActionData
import app.tourism.ui.common.special.PlacesItem
import app.tourism.ui.theme.TextStyles
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SearchScreen(
onSiteClick: (id: Int) -> Unit,
onPlaceClick: (id: Int) -> Unit,
onMapClick: () -> Unit,
queryArg: String,
searchVM: SearchViewModel = hiltViewModel()
) {
val query = searchVM.query.collectAsState().value
val places = searchVM.places.collectAsState().value
val itemsNumber = searchVM.itemsNumber.collectAsState().value
LaunchedEffect(Unit) {
searchVM.setQuery(queryArg)
}
Scaffold(
topBar = {
AppTopBar(
// todo remove hardcoded value
title = "Search",
title = stringResource(id = R.string.tjk),
actions = listOf(
TopBarActionData(
iconDrawable = R.drawable.map,
@ -28,11 +54,56 @@ fun SearchScreen(
),
),
)
}
},
contentWindowInsets = Constants.USUAL_WINDOW_INSETS
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
// todo
LazyColumn(Modifier.padding(paddingValues)) {
stickyHeader {
Column {
VerticalSpace(height = 16.dp)
AppSearchBar(
modifier = Modifier.fillMaxWidth(),
query = query,
onQueryChanged = { searchVM.setQuery(it) },
onSearchClicked = { searchVM.search(it) },
onClearClicked = { searchVM.clearSearchField() },
)
VerticalSpace(height = 16.dp)
}
}
item {
itemsNumber?.let {
Column {
Text(
text = "${stringResource(id = R.string.found)} $it",
style = TextStyles.h3,
)
VerticalSpace(height = 16.dp)
}
}
}
items(places) { item ->
Column {
PlacesItem(
place = item,
onPlaceClick = { onPlaceClick(item.id) },
isFavorite = item.isFavorite,
onFavoriteChanged = { isFavorite ->
searchVM.setFavoriteChanged(item, isFavorite)
},
)
VerticalSpace(height = 16.dp)
}
}
item {
Column {
SpaceForNavBar()
}
}
}
}
}

View file

@ -0,0 +1,67 @@
package app.tourism.ui.screens.main.home.search
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 SearchViewModel @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()
private val _itemsNumber = MutableStateFlow<Int?>(null)
val itemsNumber = _itemsNumber.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,
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

@ -1,4 +1,4 @@
package app.tourism.ui.screens.main.site_details
package app.tourism.ui.screens.main.place_details
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -8,15 +8,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import app.organicmaps.R
import app.tourism.data.dto.SiteLocation
import app.tourism.data.dto.PlaceLocation
import app.tourism.ui.common.nav.AppTopBar
@Composable
fun SiteDetailsScreen(
fun PlaceDetailsScreen(
id: Int,
onBackClick: () -> Boolean,
onMapClick: () -> Unit,
onCreateRoute: (SiteLocation) -> Unit,
onCreateRoute: (PlaceLocation) -> Unit,
) {
Scaffold(
topBar = {

View file

@ -2,6 +2,7 @@ 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)
@ -20,6 +21,13 @@ val WhiteForText = Color(0xFFFFFFFF)
val BorderDay = Color(0xFFC9D4E7)
val BorderNight = Color(0xFFFFFFFF)
@Composable
fun getBorderColor() = if (isSystemInDarkTheme()) BorderNight else BorderDay
val HintDay = Color(0xFFAAABAD)
val HintNight = Color(0xFFAAABAD)
@Composable
fun getHintColor() = if (isSystemInDarkTheme()) HintNight else HintDay
@Composable
fun getBorderColor() = if (isSystemInDarkTheme()) BorderNight else BorderDay
fun getStarColor() = StarYellow

View file

@ -4,7 +4,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import app.organicmaps.MwmActivity
import app.tourism.data.dto.SiteLocation
import app.tourism.data.dto.PlaceLocation
fun navigateToMap(context: Context, clearBackStack: Boolean = false) {
val intent = Intent(context, MwmActivity::class.java)
@ -13,8 +13,8 @@ fun navigateToMap(context: Context, clearBackStack: Boolean = false) {
ContextCompat.startActivity(context, intent, null)
}
fun navigateToMapForRoute(context: Context, siteLocation: SiteLocation) {
fun navigateToMapForRoute(context: Context, placeLocation: PlaceLocation) {
val intent = Intent(context, MwmActivity::class.java)
intent.putExtra("end_point", siteLocation)
intent.putExtra("end_point", placeLocation)
ContextCompat.startActivity(context, intent, null)
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="11dp"
android:viewportWidth="10"
android:viewportHeight="11">
<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"/>
</vector>

View file

@ -2208,4 +2208,11 @@
<string name="retry">Попробовать заново</string>
<string name="no_network">Не удается соединиться с сервером, проверьте интернет подключение</string>
<string name="no_image">Нет изображения</string>
<string name="tjk">Таджикистан</string>
<string name="clear_search_field">Очистить поле поиска</string>
<string name="top30">Топ-30 мест</string>
<string name="sights">Достопримечательности</string>
<string name="restaurants">Рестораны</string>
<string name="hotels_tourism">Отели</string>
<string name="add_to_favorites">Добавить в избранное</string>
</resources>

View file

@ -2248,5 +2248,12 @@
<string name="chose_language">Select a language</string>
<string name="retry">Try again</string>
<string name="no_network">Couldn\'t reach the server, please check connection</string>
<string name="no_image">No image</string>
<string name="no_image">No image</string>
<string name="tjk">Tajikistan</string>
<string name="clear_search_field">Clear search field</string>
<string name="top30">Top 30 places</string>
<string name="sights">Sights</string>
<string name="restaurants">Restaurants</string>
<string name="hotels_tourism">Hotels</string>
<string name="add_to_favorites">Add to favorites</string>
</resources>