From bba8edbf482f1e361fd07b2c2b305fd628f7f101 Mon Sep 17 00:00:00 2001 From: Emin Date: Mon, 8 Jul 2024 15:51:31 +0500 Subject: [PATCH] backup, ongoing: api/cache/sync --- .../src/main/java/app/tourism/AuthActivity.kt | 10 + .../java/app/tourism/data/db/Converters.kt | 13 ++ .../main/java/app/tourism/data/db/Database.kt | 26 +++ .../{ => data}/db/dao/CurrencyRatesDao.kt | 6 +- .../java/app/tourism/data/db/dao/HashesDao.kt | 22 +++ .../java/app/tourism/data/db/dao/PlacesDao.kt | 39 ++++ .../app/tourism/data/db/dao/ReviewsDao.kt | 31 ++++ .../db/entities/CurrencyRatesEntity.kt | 2 +- .../tourism/data/db/entities/HashEntity.kt | 10 + .../tourism/data/db/entities/PlaceEntity.kt | 50 +++++ .../tourism/data/db/entities/ReviewEntity.kt | 27 +++ .../tourism/data/db/entities/UserEntity.kt | 12 ++ .../java/app/tourism/data/dto/AllDataDto.kt | 13 ++ .../java/app/tourism/data/dto/CategoryDto.kt | 8 + .../java/app/tourism/data/dto/FavoritesDto.kt | 7 + .../app/tourism/data/dto/PlaceLocation.kt | 3 + .../tourism/data/dto/place/CoordinatesDto.kt | 15 ++ .../app/tourism/data/dto/place/PlaceDto.kt | 29 +++ .../app/tourism/data/dto/place/ReviewDto.kt | 23 +++ .../app/tourism/data/dto/place/UserDto.kt | 18 ++ .../tourism/data/dto/profile/LanguageDto.kt | 3 + .../app/tourism/data/dto/profile/ThemeDto.kt | 3 + .../app/tourism/data/remote/NetworkUtils.kt | 37 +++- .../app/tourism/data/remote/TourismApi.kt | 54 ++++++ .../data/repositories/AuthRepository.kt | 8 +- .../repositories/CurrencyRepositoryImpl.kt | 10 +- .../data/repositories/PlacesRepository.kt | 174 ++++++++++++++++++ .../data/repositories/ProfileRepository.kt | 12 +- .../data/repositories/ReviewsRepository.kt | 77 ++++++++ .../src/main/java/app/tourism/db/Database.kt | 16 -- .../java/app/tourism/db/dao/FeedbackDao.kt | 21 --- .../main/java/app/tourism/db/dao/MarkDao.kt | 31 ---- .../java/app/tourism/db/entities/Feedback.kt | 15 -- .../main/java/app/tourism/db/entities/Mark.kt | 26 --- .../main/java/app/tourism/db/entities/User.kt | 12 -- .../java/app/tourism/di/DatabaseModule.kt | 2 +- .../main/java/app/tourism/di/NetworkModule.kt | 2 +- .../java/app/tourism/di/RepositoriesModule.kt | 25 ++- .../domain/models/categories/Category.kt | 6 + .../domain/models/common/PlaceShort.kt | 2 +- .../domain/models/details/PlaceFull.kt | 39 +++- .../tourism/domain/models/details/Review.kt | 17 +- .../domain/models/details/ReviewToPost.kt | 10 + .../app/tourism/domain/models/details/User.kt | 10 +- .../domain/models/profile/CurrencyRates.kt | 2 +- .../java/app/tourism/ui/common/LoadImage.kt | 17 +- .../java/app/tourism/ui/common/SearchBar.kt | 6 +- .../app/tourism/ui/common/nav/PlaceTopBar.kt | 5 +- .../app/tourism/ui/common/nav/SearchTopBar.kt | 4 +- .../tourism/ui/common/special/PlacesItem.kt | 9 +- .../tourism/ui/common/special/RatingBar.kt | 2 +- .../app/tourism/ui/models/SingleChoiceItem.kt | 2 +- .../tourism/ui/screens/main/MainNavigation.kt | 72 ++++---- .../tourism/ui/screens/main/ThemeViewModel.kt | 3 + .../categories/categories/CategoriesScreen.kt | 44 +++-- .../categories/CategoriesViewModel.kt | 60 +++--- .../categories/HorizontalSingleChoice.kt | 4 +- .../favorites/favorites/FavoritesScreen.kt | 13 +- .../favorites/favorites/FavoritesViewModel.kt | 44 ++--- .../main/home/{home => }/HomeScreen.kt | 21 +-- .../ui/screens/main/home/HomeViewModel.kt | 93 ++++++++++ .../screens/main/home/home/HomeViewModel.kt | 68 ------- .../screens/main/place_details/PlaceScreen.kt | 13 +- .../screens/main/place_details/PlaceTabRow.kt | 13 +- .../main/place_details/PlaceViewModel.kt | 57 ++---- .../reviews/PostReviewViewModel.kt | 39 +++- .../reviews/ReviewsNavigation.kt | 5 + .../place_details/reviews/ReviewsScreen.kt | 23 ++- .../place_details/reviews/ReviewsViewModel.kt | 37 ++-- .../reviews/components/PostReview.kt | 27 ++- .../reviews/components/Review.kt | 18 +- .../main/profile/profile/ProfileScreen.kt | 1 + .../main/{home => }/search/SearchScreen.kt | 5 +- .../main/{home => }/search/SearchViewModel.kt | 46 ++--- .../java/app/tourism/utils/isNotAbsent.kt | 3 + .../app/src/main/res/values-ru/strings.xml | 3 +- android/app/src/main/res/values/strings.xml | 1 + 77 files changed, 1257 insertions(+), 479 deletions(-) create mode 100644 android/app/src/main/java/app/tourism/data/db/Converters.kt create mode 100644 android/app/src/main/java/app/tourism/data/db/Database.kt rename android/app/src/main/java/app/tourism/{ => data}/db/dao/CurrencyRatesDao.kt (68%) create mode 100644 android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt create mode 100644 android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt create mode 100644 android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt rename android/app/src/main/java/app/tourism/{ => data}/db/entities/CurrencyRatesEntity.kt (90%) create mode 100644 android/app/src/main/java/app/tourism/data/db/entities/HashEntity.kt create mode 100644 android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt create mode 100644 android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt create mode 100644 android/app/src/main/java/app/tourism/data/db/entities/UserEntity.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/CategoryDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/FavoritesDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/place/ReviewDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/place/UserDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/profile/LanguageDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/dto/profile/ThemeDto.kt create mode 100644 android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt create mode 100644 android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt delete mode 100644 android/app/src/main/java/app/tourism/db/Database.kt delete mode 100644 android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt delete mode 100644 android/app/src/main/java/app/tourism/db/dao/MarkDao.kt delete mode 100644 android/app/src/main/java/app/tourism/db/entities/Feedback.kt delete mode 100644 android/app/src/main/java/app/tourism/db/entities/Mark.kt delete mode 100644 android/app/src/main/java/app/tourism/db/entities/User.kt create mode 100644 android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt rename android/app/src/main/java/app/tourism/ui/screens/main/home/{home => }/HomeScreen.kt (94%) create mode 100644 android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt delete mode 100644 android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt rename android/app/src/main/java/app/tourism/ui/screens/main/{home => }/search/SearchScreen.kt (96%) rename android/app/src/main/java/app/tourism/ui/screens/main/{home => }/search/SearchViewModel.kt (58%) create mode 100644 android/app/src/main/java/app/tourism/utils/isNotAbsent.kt diff --git a/android/app/src/main/java/app/tourism/AuthActivity.kt b/android/app/src/main/java/app/tourism/AuthActivity.kt index b450e619f7..6069ae9576 100644 --- a/android/app/src/main/java/app/tourism/AuthActivity.kt +++ b/android/app/src/main/java/app/tourism/AuthActivity.kt @@ -10,16 +10,26 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import app.organicmaps.R +import app.tourism.data.repositories.PlacesRepository import app.tourism.ui.screens.auth.AuthNavigation import app.tourism.ui.theme.OrganicMapsTheme import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class AuthActivity : ComponentActivity() { + @Inject + lateinit var placesRepository: PlacesRepository + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycleScope.launch { + placesRepository.downloadAllDataIfFirstTime() + } enableEdgeToEdge( statusBarStyle = SystemBarStyle.dark(resources.getColor(R.color.black_primary)), navigationBarStyle = SystemBarStyle.dark(resources.getColor(R.color.black_primary)) diff --git a/android/app/src/main/java/app/tourism/data/db/Converters.kt b/android/app/src/main/java/app/tourism/data/db/Converters.kt new file mode 100644 index 0000000000..2c7ed1cc2a --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/Converters.kt @@ -0,0 +1,13 @@ +package app.tourism.data.db + +import androidx.room.TypeConverter +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class Converters { + @TypeConverter + fun fromList(value : List) = Json.encodeToString(value) + + @TypeConverter + fun toList(value: String) = Json.decodeFromString>(value) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/db/Database.kt b/android/app/src/main/java/app/tourism/data/db/Database.kt new file mode 100644 index 0000000000..7d34f0990a --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/Database.kt @@ -0,0 +1,26 @@ +package app.tourism.data.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import app.tourism.data.db.dao.CurrencyRatesDao +import app.tourism.data.db.dao.HashesDao +import app.tourism.data.db.dao.PlacesDao +import app.tourism.data.db.dao.ReviewsDao +import app.tourism.data.db.entities.CurrencyRatesEntity +import app.tourism.data.db.entities.HashEntity +import app.tourism.data.db.entities.PlaceEntity +import app.tourism.data.db.entities.ReviewEntity + +@Database( + entities = [PlaceEntity::class, ReviewEntity::class, HashEntity::class, CurrencyRatesEntity::class], + version = 2, + exportSchema = false +) +@TypeConverters(Converters::class) +abstract class Database : RoomDatabase() { + abstract val currencyRatesDao: CurrencyRatesDao + abstract val placesDao: PlacesDao + abstract val hashesDao: HashesDao + abstract val reviewsDao: ReviewsDao +} diff --git a/android/app/src/main/java/app/tourism/db/dao/CurrencyRatesDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/CurrencyRatesDao.kt similarity index 68% rename from android/app/src/main/java/app/tourism/db/dao/CurrencyRatesDao.kt rename to android/app/src/main/java/app/tourism/data/db/dao/CurrencyRatesDao.kt index 21f928d0e5..c56d3e4808 100644 --- a/android/app/src/main/java/app/tourism/db/dao/CurrencyRatesDao.kt +++ b/android/app/src/main/java/app/tourism/data/db/dao/CurrencyRatesDao.kt @@ -1,16 +1,16 @@ -package app.tourism.db.dao +package app.tourism.data.db.dao import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import app.tourism.db.entities.CurrencyRatesEntity +import app.tourism.data.db.entities.CurrencyRatesEntity @Dao interface CurrencyRatesDao { @Query("SELECT * FROM currency_rates") - fun getCurrencyRates(): CurrencyRatesEntity? + suspend fun getCurrencyRates(): CurrencyRatesEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun updateCurrencyRates(entity: CurrencyRatesEntity) diff --git a/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt new file mode 100644 index 0000000000..34d31739a4 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/dao/HashesDao.kt @@ -0,0 +1,22 @@ +package app.tourism.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import app.tourism.data.db.entities.HashEntity + +@Dao +interface HashesDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertHash(hash: HashEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertHashes(hashes: List) + + @Query("SELECT * FROM hashes WHERE categoryId = :id") + suspend fun getHash(id: Long): HashEntity + + @Query("SELECT * FROM hashes") + suspend fun getHashes(): List +} diff --git a/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt new file mode 100644 index 0000000000..3c6caa35ca --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/dao/PlacesDao.kt @@ -0,0 +1,39 @@ +package app.tourism.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import app.tourism.data.db.entities.PlaceEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface PlacesDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertPlaces(places: List) + + @Query("DELETE FROM places") + suspend fun deleteAllPlaces() + + @Query("DELETE FROM places WHERE categoryId = :categoryId") + suspend fun deleteAllPlacesByCategory(categoryId: Long) + + @Query("SELECT * FROM places WHERE categoryId = :categoryId") + fun getPlacesByCategoryId(categoryId: Long): Flow> + + @Query("SELECT * FROM places WHERE categoryId =:categoryId ORDER BY rating DESC LIMIT 15") + fun getTopPlacesByCategoryId(categoryId: Long): Flow> + + @Query("SELECT * FROM places WHERE id = :placeId") + fun getPlaceById(placeId: Long): Flow + + @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)") + fun getFavoritePlaces(q: String = ""): Flow> + + @Query("UPDATE places SET isFavorite = :isFavorite WHERE id = :placeId") + suspend fun setFavorite(placeId: Long, isFavorite: Boolean) + + @Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q)") + fun search(q: String= ""): Flow> +} diff --git a/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt b/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt new file mode 100644 index 0000000000..5d67e70190 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/dao/ReviewsDao.kt @@ -0,0 +1,31 @@ +package app.tourism.data.db.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import app.tourism.data.db.entities.ReviewEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ReviewsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertReview(review: ReviewEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertReviews(review: List) + + @Delete + suspend fun deleteReview(review: ReviewEntity) + + @Query("DELETE FROM reviews") + suspend fun deleteAllReviews() + + @Query("DELETE FROM reviews WHERE placeId = :placeId") + suspend fun deleteAllPlaceReviews(placeId: Long) + + @Query("SELECT * FROM reviews WHERE placeId = :placeId") + fun getReviewsForPlace(placeId: Long): Flow> +} diff --git a/android/app/src/main/java/app/tourism/db/entities/CurrencyRatesEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/CurrencyRatesEntity.kt similarity index 90% rename from android/app/src/main/java/app/tourism/db/entities/CurrencyRatesEntity.kt rename to android/app/src/main/java/app/tourism/data/db/entities/CurrencyRatesEntity.kt index d8c80c68af..34b960c089 100644 --- a/android/app/src/main/java/app/tourism/db/entities/CurrencyRatesEntity.kt +++ b/android/app/src/main/java/app/tourism/data/db/entities/CurrencyRatesEntity.kt @@ -1,4 +1,4 @@ -package app.tourism.db.entities +package app.tourism.data.db.entities import androidx.room.Entity import androidx.room.PrimaryKey diff --git a/android/app/src/main/java/app/tourism/data/db/entities/HashEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/HashEntity.kt new file mode 100644 index 0000000000..d78a4e6107 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/entities/HashEntity.kt @@ -0,0 +1,10 @@ +package app.tourism.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "hashes") +data class HashEntity( + @PrimaryKey val categoryId: Long, + val value: String, +) diff --git a/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt new file mode 100644 index 0000000000..ba428d3339 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/entities/PlaceEntity.kt @@ -0,0 +1,50 @@ +package app.tourism.data.db.entities + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import app.tourism.data.dto.PlaceLocation +import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.details.PlaceFull + +@Entity(tableName = "places") +data class PlaceEntity( + @PrimaryKey val id: Long, + val categoryId: Long, + val name: String, + val excerpt: String, + val description: String, + val cover: String, + val gallery: List, + @Embedded val coordinates: CoordinatesEntity, + val rating: Double, + val isFavorite: Boolean +) { + fun toPlaceFull() = PlaceFull( + id = id, + name = name, + rating = rating, + excerpt = excerpt, + description = description, + placeLocation = coordinates.toPlaceLocation(name), + cover = cover, + pics = gallery, + isFavorite = isFavorite + ) + + fun toPlaceShort() = PlaceShort( + id = id, + name = name, + cover = cover, + rating = rating, + excerpt = excerpt, + isFavorite = isFavorite + ) +} + +data class CoordinatesEntity( + val latitude: Double, + val longitude: Double +) { + fun toPlaceLocation(name: String) = PlaceLocation(name, latitude, longitude) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt new file mode 100644 index 0000000000..33c54b6346 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/entities/ReviewEntity.kt @@ -0,0 +1,27 @@ +package app.tourism.data.db.entities + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import app.tourism.domain.models.details.Review + +@Entity(tableName = "reviews") +data class ReviewEntity( + @PrimaryKey val id: Long, + val placeId: Long, + @Embedded val user: JustUser, + val comment: String, + val date: String, + val rating: Int, + val images: List +) { + fun toReview() = Review( + id = id, + placeId = placeId, + rating = rating, + user = user.toUser(), + date = date, + comment = comment, + picsUrls = images, + ) +} diff --git a/android/app/src/main/java/app/tourism/data/db/entities/UserEntity.kt b/android/app/src/main/java/app/tourism/data/db/entities/UserEntity.kt new file mode 100644 index 0000000000..f583f03f42 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/db/entities/UserEntity.kt @@ -0,0 +1,12 @@ +package app.tourism.data.db.entities + +import app.tourism.domain.models.details.User + +data class JustUser( + val userId: Long, + val fullName: String, + val avatar: String?, + val country: String +) { + fun toUser() = User(id = userId, name = fullName, pfpUrl = avatar, countryCodeName = country) +} diff --git a/android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt b/android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt new file mode 100644 index 0000000000..fe63dedd34 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt @@ -0,0 +1,13 @@ +package app.tourism.data.dto + +import app.tourism.data.dto.place.PlaceDto + +data class AllDataDto( + val attractions: List, + val restaurants: List, + val accommodations: List, + val attractions_hash: String, + val restaurants_hash: String, + val accommodations_hash: String, +) + diff --git a/android/app/src/main/java/app/tourism/data/dto/CategoryDto.kt b/android/app/src/main/java/app/tourism/data/dto/CategoryDto.kt new file mode 100644 index 0000000000..dcc6bde8fd --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/CategoryDto.kt @@ -0,0 +1,8 @@ +package app.tourism.data.dto + +import app.tourism.data.dto.place.PlaceDto + +data class CategoryDto( + val data: List, + val hash: String +) diff --git a/android/app/src/main/java/app/tourism/data/dto/FavoritesDto.kt b/android/app/src/main/java/app/tourism/data/dto/FavoritesDto.kt new file mode 100644 index 0000000000..589d662128 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/FavoritesDto.kt @@ -0,0 +1,7 @@ +package app.tourism.data.dto + +import app.tourism.data.dto.place.PlaceDto + +data class FavoritesDto( + val data: List, +) diff --git a/android/app/src/main/java/app/tourism/data/dto/PlaceLocation.kt b/android/app/src/main/java/app/tourism/data/dto/PlaceLocation.kt index 90b8cbd2c1..d62fdf9d93 100644 --- a/android/app/src/main/java/app/tourism/data/dto/PlaceLocation.kt +++ b/android/app/src/main/java/app/tourism/data/dto/PlaceLocation.kt @@ -3,6 +3,7 @@ package app.tourism.data.dto import android.os.Parcelable import app.organicmaps.bookmarks.data.FeatureId import app.organicmaps.bookmarks.data.MapObject +import app.tourism.data.db.entities.CoordinatesEntity import kotlinx.parcelize.Parcelize @Parcelize @@ -10,4 +11,6 @@ data class PlaceLocation(val name: String, val lat: Double, val lon: Double) : P fun toMapObject() = MapObject.createMapObject( FeatureId.EMPTY, MapObject.POI, name, "", lat, lon ); + + fun toCoordinatesEntity() = CoordinatesEntity(lat, lon) } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt new file mode 100644 index 0000000000..f2b5bd3584 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/place/CoordinatesDto.kt @@ -0,0 +1,15 @@ +package app.tourism.data.dto.place + +import app.tourism.data.dto.PlaceLocation + +data class CoordinatesDto( + val latitude: String, + val longitude: String +) { + fun toPlaceLocation(name: String) = + PlaceLocation( + name, + latitude.toDouble(), + longitude.toDouble() + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt new file mode 100644 index 0000000000..52b7c2426e --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/place/PlaceDto.kt @@ -0,0 +1,29 @@ +package app.tourism.data.dto.place + +import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.details.PlaceFull + +data class PlaceDto( + val id: Long, + val name: String, + val coordinates: CoordinatesDto, + val cover: String, + val feedbacks: List = emptyList(), + val gallery: List, + val rating: String, + val short_description: String, + val long_description: String, +) { + fun toPlaceFull(isFavorite: Boolean) = PlaceFull( + id = id, + name = name, + rating = rating.toDouble(), + excerpt = short_description, + description = long_description, + placeLocation = coordinates.toPlaceLocation(name), + cover = cover, + pics = gallery, + isFavorite = isFavorite, + reviews = feedbacks.map { it.toReview() } + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/place/ReviewDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/ReviewDto.kt new file mode 100644 index 0000000000..98ceab9456 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/place/ReviewDto.kt @@ -0,0 +1,23 @@ +package app.tourism.data.dto.place + +import app.tourism.domain.models.details.Review + +data class ReviewDto( + val id: Long, + val mark_id: Long, + val images: List, + val message: String, + val points: Int, + val created_at: String, + val user: UserDto +) { + fun toReview() = Review( + id = id, + placeId = mark_id, + rating = points, + user = user.toUser(), + date = created_at, + comment = message, + picsUrls = images + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/place/UserDto.kt b/android/app/src/main/java/app/tourism/data/dto/place/UserDto.kt new file mode 100644 index 0000000000..4e98c490e3 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/place/UserDto.kt @@ -0,0 +1,18 @@ +package app.tourism.data.dto.place + +import app.tourism.domain.models.details.User + + +data class UserDto( + val id: Long, + val avatar: String, + val country: String, + val full_name: String, +) { + fun toUser() = User( + id = id, + name = full_name, + countryCodeName = country, + pfpUrl = avatar, + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/LanguageDto.kt b/android/app/src/main/java/app/tourism/data/dto/profile/LanguageDto.kt new file mode 100644 index 0000000000..15ada740d1 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/profile/LanguageDto.kt @@ -0,0 +1,3 @@ +package app.tourism.data.dto.profile + +data class LanguageDto(val language: String) diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/ThemeDto.kt b/android/app/src/main/java/app/tourism/data/dto/profile/ThemeDto.kt new file mode 100644 index 0000000000..5dc2256e0d --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/profile/ThemeDto.kt @@ -0,0 +1,3 @@ +package app.tourism.data.dto.profile + +data class ThemeDto(val theme: String) diff --git a/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt b/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt index c7bc23805c..a5815041ec 100644 --- a/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt +++ b/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt @@ -11,28 +11,49 @@ import retrofit2.HttpException import retrofit2.Response import java.io.IOException -suspend inline fun FlowCollector>.handleCall( +suspend inline fun FlowCollector>.handleGenericCall( call: () -> Response, mapper: (T) -> R, emitLoadingStatusBeforeCall: Boolean = true ) { - if(emitLoadingStatusBeforeCall) emit(Resource.Loading()) + if (emitLoadingStatusBeforeCall) emit(Resource.Loading()) try { val response = call() val body = response.body()?.let { mapper(it) } if (response.isSuccessful) emit(Resource.Success(body)) - else emit(response.parseError()) - } catch(e: HttpException) { + } catch (e: HttpException) { emit( Resource.Error( - message = "Упс! Что-то пошло не так." - )) - } catch(e: IOException) { + message = "Упс! Что-то пошло не так." + ) + ) + } catch (e: IOException) { emit( Resource.Error( + message = "Не удается соединиться с сервером, проверьте интернет подключение" + ) + ) + } catch (e: Exception) { + emit(Resource.Error(message = "Упс! Что-то пошло не так.")) + } +} + +suspend inline fun handleResponse(call: () -> Response): Resource { + try { + val response = call() + if (response.isSuccessful) { + val body = response.body()!! + return Resource.Success(body) + } else return response.parseError() + } catch (e: HttpException) { + return Resource.Error(message = "Упс! Что-то пошло не так.") + } catch (e: IOException) { + return Resource.Error( message = "Не удается соединиться с сервером, проверьте интернет подключение" - )) + ) + } catch (e: Exception) { + return Resource.Error(message = "Упс! Что-то пошло не так.") } } diff --git a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt index 3a7e360570..153530f1cb 100644 --- a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt +++ b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt @@ -1,17 +1,27 @@ package app.tourism.data.remote +import app.tourism.data.dto.AllDataDto +import app.tourism.data.dto.CategoryDto +import app.tourism.data.dto.FavoritesDto import app.tourism.data.dto.auth.AuthResponseDto +import app.tourism.data.dto.place.ReviewDto +import app.tourism.data.dto.profile.LanguageDto +import app.tourism.data.dto.profile.ThemeDto import app.tourism.data.dto.profile.UserData import app.tourism.domain.models.SimpleResponse import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Part +import retrofit2.http.Path interface TourismApi { // region auth @@ -52,6 +62,50 @@ interface TourismApi { @Part("_method") _method: RequestBody? = "PUT".toFormDataRequestBody(), @Part avatar: MultipartBody.Part? = null ): Response + + @PUT("profile/lang") + suspend fun updateLanguage(@Body language: LanguageDto): Response + + @PUT("profile/theme") + suspend fun updateTheme(@Body theme: ThemeDto): Response // endregion profile + // region places + @GET("marks/{id}") + suspend fun getPlacesByCategory(@Path("id") id: Long): Response + + @GET("marks/all") + suspend fun getAllPlaces(): Response + // endregion places + + // region favorites + @GET("favourite-marks") + suspend fun getFavorites(): Response + + @POST("favourite-marks") + suspend fun addFavorites(@Body ids: List): Response + + @DELETE("favourite-marks") + suspend fun removeFromFavorites(@Body ids: List): Response + // endregion favorites + + // region reviews + @GET("feedbacks/{id}") + suspend fun getReviewsByPlaceId(id: Long): Response> + + @Multipart + @POST("feedbacks") + suspend fun postReview( + @Part("message") comment: RequestBody? = null, + @Part("mark_id") placeId: RequestBody? = null, + @Part("points") points: RequestBody? = null, + @Part images: List? = null + ): Response + + @DELETE("feedbacks/{mark_id}") + suspend fun deleteReview( + @Path("mark_id") placeId: Long, + @Body reviewsIds: List, + ): Response + // endregion reviews } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt index c08d9755dc..1b48eb3e07 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt @@ -1,7 +1,7 @@ package app.tourism.data.repositories import app.tourism.data.remote.TourismApi -import app.tourism.data.remote.handleCall +import app.tourism.data.remote.handleGenericCall import app.tourism.domain.models.SimpleResponse import app.tourism.domain.models.auth.AuthResponse import app.tourism.domain.models.auth.RegistrationData @@ -11,14 +11,14 @@ import kotlinx.coroutines.flow.flow class AuthRepository(private val api: TourismApi) { fun signIn(email: String, password: String): Flow> = flow { - handleCall( + handleGenericCall( call = { api.signIn(email, password) }, mapper = { it.toAuthResponse() } ) } fun signUp(registrationData: RegistrationData) = flow { - handleCall( + handleGenericCall( call = { api.signUp( registrationData.fullName, @@ -33,7 +33,7 @@ class AuthRepository(private val api: TourismApi) { } fun signOut(): Flow> = flow { - handleCall( + handleGenericCall( call = { api.signOut() }, mapper = { it } ) diff --git a/android/app/src/main/java/app/tourism/data/repositories/CurrencyRepositoryImpl.kt b/android/app/src/main/java/app/tourism/data/repositories/CurrencyRepositoryImpl.kt index 15ea8057ed..635278f436 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/CurrencyRepositoryImpl.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/CurrencyRepositoryImpl.kt @@ -1,17 +1,13 @@ package app.tourism.data.repositories +import app.tourism.data.db.Database import app.tourism.data.dto.currency.CurrenciesList import app.tourism.data.remote.CurrencyApi -import app.tourism.data.remote.handleCall -import app.tourism.db.Database +import app.tourism.data.remote.handleGenericCall import app.tourism.domain.models.profile.CurrencyRates import app.tourism.domain.models.resource.Resource -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import kotlin.Double.Companion.NaN class CurrencyRepository(private val api: CurrencyApi, private val db: Database) { @@ -22,7 +18,7 @@ class CurrencyRepository(private val api: CurrencyApi, private val db: Database) emit(Resource.Success(it.toCurrencyRates())) } - handleCall( + handleGenericCall( call = { api.getCurrency() }, mapper = { val currencyRates = getCurrencyRatesFromXml(it) diff --git a/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt new file mode 100644 index 0000000000..6f7a8f1ed8 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt @@ -0,0 +1,174 @@ +package app.tourism.data.repositories + +import android.content.Context +import app.organicmaps.R +import app.tourism.data.db.Database +import app.tourism.data.db.entities.HashEntity +import app.tourism.data.db.entities.PlaceEntity +import app.tourism.data.db.entities.ReviewEntity +import app.tourism.data.dto.place.PlaceDto +import app.tourism.data.remote.TourismApi +import app.tourism.data.remote.handleGenericCall +import app.tourism.data.remote.handleResponse +import app.tourism.domain.models.SimpleResponse +import app.tourism.domain.models.categories.PlaceCategory +import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.details.PlaceFull +import app.tourism.domain.models.details.Review +import app.tourism.domain.models.resource.Resource +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flow + +class PlacesRepository( + private val api: TourismApi, + private val db: Database, + @ApplicationContext private val context: Context, +) { + private val placesDao = db.placesDao + private val reviewsDao = db.reviewsDao + private val hashesDao = db.hashesDao + + fun downloadAllDataIfFirstTime(): Flow> = flow { + val hashes = hashesDao.getHashes() + + val favoritesResponse = handleResponse { api.getFavorites() } + + if (hashes.isEmpty()) { + handleGenericCall( + call = { api.getAllPlaces() }, + mapper = { data -> + // get data + val favorites = + if (favoritesResponse is Resource.Success) favoritesResponse.data?.data?.map { + it.toPlaceFull(true) + } else null + + val reviews = mutableListOf() + + fun PlaceDto.toEntity(placeCategory: PlaceCategory): PlaceEntity { + var placeFull = this.toPlaceFull(false) + placeFull = + placeFull.copy( + isFavorite = favorites?.any { it.id == placeFull.id } ?: false + ) + + placeFull.reviews?.let { it1 -> reviews.addAll(it1) } + return placeFull.toPlaceEntity(placeCategory.id) + } + + val sightsEntities = data.attractions.map { placeDto -> + placeDto.toEntity(PlaceCategory.Sights) + } + val restaurantsEntities = data.restaurants.map { placeDto -> + placeDto.toEntity(PlaceCategory.Restaurants) + } + val hotelsEntities = data.accommodations.map { placeDto -> + placeDto.toEntity(PlaceCategory.Hotels) + } + + // update places + placesDao.deleteAllPlaces() + placesDao.insertPlaces(sightsEntities) + placesDao.insertPlaces(restaurantsEntities) + placesDao.insertPlaces(hotelsEntities) + + // update reviews + val reviewsEntities = reviews.map { it.toReviewEntity() } + reviewsDao.deleteAllReviews() + reviewsDao.insertReviews(reviewsEntities) + + // update hashes + hashesDao.insertHashes( + listOf( + HashEntity(PlaceCategory.Sights.id, data.attractions_hash), + HashEntity(PlaceCategory.Restaurants.id, data.restaurants_hash), + HashEntity(PlaceCategory.Hotels.id, data.accommodations_hash), + ) + ) + + // return response + SimpleResponse(message = context.getString(R.string.great_success)) + } + ) + } + } + + fun search(q: String): Flow>> = channelFlow { + placesDao.search("%$q%").collectLatest { placeEntities -> + val places = placeEntities.map { it.toPlaceShort() } + send(Resource.Success(places)) + } + } + + fun getPlacesByCategory(id: Long): Flow>> = channelFlow { + val hash = hashesDao.getHash(id) + + if (hash.value.isNotBlank()) { + placesDao.getPlacesByCategoryId(categoryId = id) + .collectLatest { placeEntities -> + send(Resource.Success(placeEntities.map { it.toPlaceShort() })) + } + } + + var favorites = listOf() + placesDao.getFavoritePlaces("").collectLatest { + favorites = it + } + + val resource = handleResponse { api.getPlacesByCategory(id) } + if (resource is Resource.Success) { + resource.data?.let { categoryDto -> + if (hash.value != categoryDto.hash) { + // update places + hashesDao.insertHash(hash.copy(value = categoryDto.hash)) + placesDao.deleteAllPlacesByCategory(categoryId = id) + + val places = categoryDto.data.map { placeDto -> + var placeFull = placeDto.toPlaceFull(false) + placeFull = + placeFull.copy(isFavorite = favorites.any { it.id == placeFull.id }) + placeFull + } + placesDao.insertPlaces(places.map { it.toPlaceEntity(id) }) + + // update reviews + val reviewsEntities = mutableListOf() + places.forEach { place -> + place.reviews?.map { review -> review.toReviewEntity() } + ?.also { reviewEntity -> reviewsEntities.addAll(reviewEntity) } + } + reviewsDao.deleteAllReviews() + reviewsDao.insertReviews(reviewsEntities) + } + } + } + } + + fun getTopPlaces(id: Long): Flow>> = channelFlow { + placesDao.getTopPlacesByCategoryId(categoryId = id) + .collectLatest { placeEntities -> + send(Resource.Success(placeEntities.map { it.toPlaceShort() })) + } + } + + fun getPlaceById(id: Long): Flow> = channelFlow { + placesDao.getPlaceById(id) + .collectLatest { placeEntity -> + send(Resource.Success(placeEntity.toPlaceFull())) + } + } + + fun getFavorites(q: String): Flow>> = channelFlow { + placesDao.getFavoritePlaces("%$q%") + .collectLatest { placeEntities -> + send(Resource.Success(placeEntities.map { it.toPlaceShort() })) + } + } + + suspend fun setFavorite(placeId: Long, isFavorite: Boolean) { + placesDao.setFavorite(placeId, isFavorite) + } +} diff --git a/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt index 106e2b8876..5d80e753ee 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt @@ -1,9 +1,11 @@ package app.tourism.data.repositories import android.content.Context +import app.tourism.data.dto.profile.LanguageDto +import app.tourism.data.dto.profile.ThemeDto import app.tourism.data.prefs.UserPreferences import app.tourism.data.remote.TourismApi -import app.tourism.data.remote.handleCall +import app.tourism.data.remote.handleGenericCall import app.tourism.data.remote.toFormDataRequestBody import app.tourism.domain.models.profile.PersonalData import app.tourism.domain.models.resource.Resource @@ -21,7 +23,7 @@ class ProfileRepository( @ApplicationContext private val context: Context ) { fun getPersonalData(): Flow> = flow { - handleCall( + handleGenericCall( call = { api.getUser() }, mapper = { it.data.toPersonalData() @@ -46,7 +48,7 @@ class ProfileRepository( val language = userPreferences.getLanguage()?.code val theme = userPreferences.getTheme()?.code - handleCall( + handleGenericCall( call = { api.updateProfile( fullName = fullName.toFormDataRequestBody(), @@ -63,7 +65,7 @@ class ProfileRepository( suspend fun updateLanguage(code: String) { try { - api.updateProfile(language = code.toFormDataRequestBody()) + api.updateLanguage(language = LanguageDto(code)) } catch (e: Exception) { println(e.message) } @@ -71,7 +73,7 @@ class ProfileRepository( suspend fun updateTheme(code: String) { try { - api.updateProfile(theme = code.toFormDataRequestBody()) + api.updateTheme(theme = ThemeDto(code)) } catch (e: Exception) { println(e.message) } diff --git a/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt new file mode 100644 index 0000000000..0a8c894666 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/repositories/ReviewsRepository.kt @@ -0,0 +1,77 @@ +package app.tourism.data.repositories + +import app.tourism.data.db.Database +import app.tourism.data.remote.TourismApi +import app.tourism.data.remote.handleResponse +import app.tourism.data.remote.toFormDataRequestBody +import app.tourism.domain.models.SimpleResponse +import app.tourism.domain.models.details.Review +import app.tourism.domain.models.details.ReviewToPost +import app.tourism.domain.models.resource.Resource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flow +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody + +class ReviewsRepository( + private val api: TourismApi, + private val db: Database, +) { + private val reviewsDao = db.reviewsDao + + fun getReviewsForPlace(id: Long): Flow>> = channelFlow { + reviewsDao.getReviewsForPlace(id).collectLatest { reviewsEntities -> + val reviews = reviewsEntities.map { it.toReview() } + send(Resource.Success(reviews)) + } + } + + fun postReview(review: ReviewToPost): Flow> = flow { + val imageMultiparts = mutableListOf() + review.images.forEach { + val requestBody = it.asRequestBody("image/*".toMediaType()) + val imageMultipart = + MultipartBody.Part.createFormData("images[]", it.name, requestBody) + imageMultiparts.add(imageMultipart) + } + + emit(Resource.Loading()) + val postReviewResponse = handleResponse { + api.postReview( + placeId = review.placeId.toString().toFormDataRequestBody(), + comment = review.comment.toFormDataRequestBody(), + points = review.rating.toString().toFormDataRequestBody(), + images = imageMultiparts + ) + } + emit(postReviewResponse) + + if (postReviewResponse is Resource.Success) { + updateReviewsForDb(review.placeId) + } + } + + fun deleteReviews(placeId: Long, reviewsIds: List): Flow> = + flow { + val deleteReviewsResponse = handleResponse { + api.deleteReview(placeId, reviewsIds) + } + emit(deleteReviewsResponse) + + if (deleteReviewsResponse is Resource.Success) { + updateReviewsForDb(placeId) + } + } + + private suspend fun updateReviewsForDb(id: Long) { + val getReviewsResponse = handleResponse { + api.getReviewsByPlaceId(id) + } + if (getReviewsResponse is Resource.Success) { + reviewsDao.deleteAllPlaceReviews(id) + } + } +} diff --git a/android/app/src/main/java/app/tourism/db/Database.kt b/android/app/src/main/java/app/tourism/db/Database.kt deleted file mode 100644 index e631a49265..0000000000 --- a/android/app/src/main/java/app/tourism/db/Database.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.tourism.db - -import androidx.room.Database -import androidx.room.RoomDatabase -import app.tourism.db.dao.CurrencyRatesDao -import app.tourism.db.entities.CurrencyRatesEntity - -@Database( - entities = [CurrencyRatesEntity::class], - version = 1, - exportSchema = false -) - -abstract class Database: RoomDatabase() { - abstract val currencyRatesDao: CurrencyRatesDao -} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt b/android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt deleted file mode 100644 index 94c8d03427..0000000000 --- a/android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.tourism.db.dao - -import androidx.room.Dao -import androidx.room.Delete -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import app.tourism.db.entities.Feedback - -@Dao -interface FeedbackDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertFeedback(feedback: Feedback) - - @Delete - suspend fun deleteFeedback(feedback: Feedback) - - @Query("SELECT * FROM feedbacks WHERE placeId = :placeId") - suspend fun getFeedbacksForPlace(placeId: Long): List -} diff --git a/android/app/src/main/java/app/tourism/db/dao/MarkDao.kt b/android/app/src/main/java/app/tourism/db/dao/MarkDao.kt deleted file mode 100644 index d400370dfc..0000000000 --- a/android/app/src/main/java/app/tourism/db/dao/MarkDao.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.tourism.db.dao - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import androidx.room.Update -import app.tourism.db.entities.Place -import kotlinx.coroutines.flow.Flow - -@Dao -interface PlaceDao { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertPlaces(places: List) - - @Query("DELETE FROM Places") - suspend fun deleteAllPlaces() - - @Query("SELECT * FROM Places") - suspend fun getAllPlaces(): Flow> - - @Query("SELECT * FROM Places WHERE id = :placeId") - suspend fun getPlaceById(placeId: Long): Flow - - @Query("SELECT * FROM Places WHERE isFavorite == 1") - suspend fun getFavoritePlaces(): Flow> - - @Update - suspend fun setFavorite(placeId: Long, isFavorite: Boolean) -} diff --git a/android/app/src/main/java/app/tourism/db/entities/Feedback.kt b/android/app/src/main/java/app/tourism/db/entities/Feedback.kt deleted file mode 100644 index 114325d833..0000000000 --- a/android/app/src/main/java/app/tourism/db/entities/Feedback.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.tourism.db.entities - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "feedbacks") -data class Feedback( - @PrimaryKey(autoGenerate = true) val id: Long, - val userId: Long, - val message: String, - val placeId: Long, - val points: Int, - val images: List -) diff --git a/android/app/src/main/java/app/tourism/db/entities/Mark.kt b/android/app/src/main/java/app/tourism/db/entities/Mark.kt deleted file mode 100644 index 45bbb27abd..0000000000 --- a/android/app/src/main/java/app/tourism/db/entities/Mark.kt +++ /dev/null @@ -1,26 +0,0 @@ -package app.tourism.db.entities - -import androidx.room.Entity -import androidx.room.PrimaryKey -import androidx.room.Relation - -@Entity(tableName = "Places") -data class Place( - @PrimaryKey(autoGenerate = true) val id: Long, - val name: String, - val phone: String, - val shortDescription: String, - val longDescription: String, - val cover: String, - val gallery: List, - @Relation(parentColumn = "id", entityColumn = "placeId", entity = Feedback::class) - val feedbacks: List, - val coordinates: Coordinates, - val rating: Double, - val isFavorite: Boolean -) - -data class Coordinates( - val latitude: String, - val longitude: String -) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/db/entities/User.kt b/android/app/src/main/java/app/tourism/db/entities/User.kt deleted file mode 100644 index 97e6d1fec8..0000000000 --- a/android/app/src/main/java/app/tourism/db/entities/User.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.tourism.db.entities - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "users") -data class User( - @PrimaryKey(autoGenerate = true) val id: Long, - val fullName: String, - val avatar: String, - val country: String -) diff --git a/android/app/src/main/java/app/tourism/di/DatabaseModule.kt b/android/app/src/main/java/app/tourism/di/DatabaseModule.kt index b3a15ec763..c2f951d6bd 100644 --- a/android/app/src/main/java/app/tourism/di/DatabaseModule.kt +++ b/android/app/src/main/java/app/tourism/di/DatabaseModule.kt @@ -2,7 +2,7 @@ package app.tourism.di import android.app.Application import androidx.room.Room -import app.tourism.db.Database +import app.tourism.data.db.Database import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/android/app/src/main/java/app/tourism/di/NetworkModule.kt b/android/app/src/main/java/app/tourism/di/NetworkModule.kt index 52fce299e2..3101dadf36 100644 --- a/android/app/src/main/java/app/tourism/di/NetworkModule.kt +++ b/android/app/src/main/java/app/tourism/di/NetworkModule.kt @@ -6,7 +6,7 @@ import app.tourism.data.prefs.UserPreferences import app.tourism.data.remote.CurrencyApi import app.tourism.data.remote.TourismApi import app.tourism.data.repositories.CurrencyRepository -import app.tourism.db.Database +import app.tourism.data.db.Database import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt index 7c20a92ba4..9507bd4146 100644 --- a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt +++ b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt @@ -7,7 +7,9 @@ import app.tourism.data.remote.TourismApi import app.tourism.data.repositories.AuthRepository import app.tourism.data.repositories.CurrencyRepository import app.tourism.data.repositories.ProfileRepository -import app.tourism.db.Database +import app.tourism.data.db.Database +import app.tourism.data.repositories.PlacesRepository +import app.tourism.data.repositories.ReviewsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,6 +26,27 @@ object RepositoriesModule { return AuthRepository(api) } + @Provides + @Singleton + fun providePlacesRepository( + api: TourismApi, + db: Database, + @ApplicationContext context: Context, + ): PlacesRepository { + return PlacesRepository(api, db, context) + } + + @Provides + @Singleton + fun provideReviewsRepository( + api: TourismApi, + db: Database, + @ApplicationContext context: Context, + ): ReviewsRepository { + return ReviewsRepository(api, db) + } + + @Provides @Singleton fun provideProfileRepository( diff --git a/android/app/src/main/java/app/tourism/domain/models/categories/Category.kt b/android/app/src/main/java/app/tourism/domain/models/categories/Category.kt index 7de7acb6f3..314d050b0e 100644 --- a/android/app/src/main/java/app/tourism/domain/models/categories/Category.kt +++ b/android/app/src/main/java/app/tourism/domain/models/categories/Category.kt @@ -1,3 +1,9 @@ package app.tourism.domain.models.categories data class Category(val value: String?, val label: String) + +enum class PlaceCategory(val id: Long) { + Sights(1), // called attractions in the server + Restaurants(2), + Hotels(3) // called accommodations in the server +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt b/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt index 4b2095bb9e..262a8df43b 100644 --- a/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt +++ b/android/app/src/main/java/app/tourism/domain/models/common/PlaceShort.kt @@ -3,7 +3,7 @@ package app.tourism.domain.models.common data class PlaceShort( val id: Long, val name: String, - val pic: String? = null, + val cover: String? = null, val rating: Double? = null, val excerpt: String? = null, val isFavorite: Boolean = false, diff --git a/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt b/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt index dd13ba5f9b..9c85ed4078 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/PlaceFull.kt @@ -1,15 +1,40 @@ package app.tourism.domain.models.details +import app.tourism.data.db.entities.PlaceEntity import app.tourism.data.dto.PlaceLocation +import app.tourism.domain.models.common.PlaceShort data class PlaceFull( val id: Long, val name: String, - val rating: Double? = null, - val excerpt: String? = null, - val description: String? = null, - val placeLocation: PlaceLocation? = null, - val pic: String? = null, + val rating: Double, + val excerpt: String, + val description: String, + val placeLocation: PlaceLocation, + val cover: String, val pics: List = emptyList(), - val isFavorite: Boolean = false, -) + val reviews: List? = null, + val isFavorite: Boolean, +) { + fun toPlaceShort() = PlaceShort( + id = id, + name = name, + cover = cover, + rating = rating, + excerpt = excerpt, + isFavorite = isFavorite + ) + + fun toPlaceEntity(categoryId: Long) = PlaceEntity( + id = id, + categoryId = categoryId, + name = name, + rating = rating, + excerpt = excerpt, + description = description, + gallery = pics, + coordinates = placeLocation.toCoordinatesEntity(), + cover = cover, + isFavorite = isFavorite, + ) +} diff --git a/android/app/src/main/java/app/tourism/domain/models/details/Review.kt b/android/app/src/main/java/app/tourism/domain/models/details/Review.kt index 95eec0324d..ceaee1a727 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/Review.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/Review.kt @@ -1,10 +1,23 @@ package app.tourism.domain.models.details +import app.tourism.data.db.entities.ReviewEntity + data class Review( val id: Long, - val rating: Double? = null, + val placeId: Long, + val rating: Int, val user: User, val date: String? = null, val comment: String? = null, val picsUrls: List = emptyList(), -) +) { + fun toReviewEntity() = ReviewEntity( + id = id, + user = user.toUserEntity(), + comment = comment ?: "", + placeId = placeId, + date = date ?: "", + rating = rating, + images = picsUrls + ) +} diff --git a/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt b/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt new file mode 100644 index 0000000000..23e6e2b6ac --- /dev/null +++ b/android/app/src/main/java/app/tourism/domain/models/details/ReviewToPost.kt @@ -0,0 +1,10 @@ +package app.tourism.domain.models.details + +import java.io.File + +data class ReviewToPost( + val placeId: Long, + val comment: String, + val rating: Int, + val images: List, +) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/domain/models/details/User.kt b/android/app/src/main/java/app/tourism/domain/models/details/User.kt index a4181307ab..a6f425ed3a 100644 --- a/android/app/src/main/java/app/tourism/domain/models/details/User.kt +++ b/android/app/src/main/java/app/tourism/domain/models/details/User.kt @@ -1,8 +1,14 @@ package app.tourism.domain.models.details +import app.tourism.data.db.entities.JustUser + data class User( val id: Long, val name: String, val pfpUrl: String? = null, - val countryCodeName: String? = null, -) + val countryCodeName: String, +) { + fun toUserEntity() = JustUser( + userId = id, fullName = name, avatar = pfpUrl, country = countryCodeName + ) +} diff --git a/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt b/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt index ebb11ad43f..afd19ede98 100644 --- a/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt +++ b/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt @@ -1,6 +1,6 @@ package app.tourism.domain.models.profile -import app.tourism.db.entities.CurrencyRatesEntity +import app.tourism.data.db.entities.CurrencyRatesEntity data class CurrencyRates(val usd: Double, val eur: Double, val rub: Double) { fun toCurrencyRatesEntity() = CurrencyRatesEntity(1, usd, eur, rub) diff --git a/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt b/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt index e0cb7eef4f..e923aed2d5 100644 --- a/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt +++ b/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt @@ -1,23 +1,21 @@ package app.tourism.ui.common -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme 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.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import app.organicmaps.R import app.tourism.ui.theme.TextStyles import coil.compose.AsyncImage @@ -27,10 +25,10 @@ import coil.request.ImageRequest fun LoadImg( url: String?, modifier: Modifier = Modifier, - backgroundColor: Color = MaterialTheme.colorScheme.surface, + backgroundColor: Color = Color.Transparent, contentScale: ContentScale = ContentScale.Crop ) { - if (url != null && url.isNotBlank()) + if (!url.isNullOrBlank()) CoilImg( modifier = modifier, url = url, @@ -47,7 +45,7 @@ fun LoadImg( ) { Text( text = stringResource(id = R.string.no_image), - style = TextStyles.b2, + style = TextStyles.b3, textAlign = TextAlign.Center ) } @@ -61,10 +59,13 @@ fun CoilImg( contentScale: ContentScale ) { AsyncImage( - modifier = modifier.background(color = backgroundColor), + modifier = Modifier + .background(color = backgroundColor) + .then(modifier) + .fillMaxSize(), model = ImageRequest.Builder(LocalContext.current) .data(url) - .crossfade(500) + .crossfade(200) .error(R.drawable.error_centered) .build(), placeholder = painterResource(R.drawable.placeholder), diff --git a/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt b/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt index a3b52c3600..8714e62a73 100644 --- a/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt +++ b/android/app/src/main/java/app/tourism/ui/common/SearchBar.kt @@ -29,7 +29,7 @@ fun AppSearchBar( modifier: Modifier = Modifier, query: String, onQueryChanged: (String) -> Unit, - onSearchClicked: (String) -> Unit, + onSearchClicked: ((String) -> Unit)? = null, onClearClicked: () -> Unit, ) { var isActive by remember { mutableStateOf(false) } @@ -51,7 +51,7 @@ fun AppSearchBar( singleLine = true, maxLines = 1, leadingIcon = { - IconButton(onClick = { onSearchClicked(query) }) { + IconButton(onClick = { onSearchClicked?.invoke(query) }) { Icon( painter = painterResource(id = R.drawable.search), contentDescription = searchLabel, @@ -69,7 +69,7 @@ fun AppSearchBar( } }, shape = RoundedCornerShape(16.dp), - keyboardActions = KeyboardActions(onSearch = { onSearchClicked(query) }), + keyboardActions = KeyboardActions(onSearch = { onSearchClicked?.invoke(query) }), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), colors = TextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.surface, diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt b/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt index 6eb99936c6..4de71737b0 100644 --- a/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt +++ b/android/app/src/main/java/app/tourism/ui/common/nav/PlaceTopBar.kt @@ -21,6 +21,7 @@ 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.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.organicmaps.R import app.tourism.ui.common.LoadImg @@ -96,7 +97,9 @@ fun PlaceTopBar( .padding(start = padding, end = padding, bottom = padding), text = title, style = TextStyles.h2, - color = Color.White + color = Color.White, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt b/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt index ddbb1abafb..581aa5a855 100644 --- a/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt +++ b/android/app/src/main/java/app/tourism/ui/common/nav/SearchTopBar.kt @@ -23,7 +23,7 @@ fun SearchTopBar( modifier: Modifier = Modifier, query: String, onQueryChanged: (String) -> Unit, - onSearchClicked: (String) -> Unit, + onSearchClicked: ((String) -> Unit)? = null, onClearClicked: () -> Unit, onBackClicked: () -> Unit ) { @@ -60,7 +60,7 @@ fun SearchTopBar( ) } }, - keyboardActions = KeyboardActions(onSearch = { onSearchClicked(query) }), + keyboardActions = KeyboardActions(onSearch = { onSearchClicked?.invoke(query) }), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), colors = TextFieldDefaults.colors( focusedContainerColor = MaterialTheme.colorScheme.background, diff --git a/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt b/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt index f2f4ad5b37..f25c893236 100644 --- a/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt +++ b/android/app/src/main/java/app/tourism/ui/common/special/PlacesItem.kt @@ -50,9 +50,12 @@ fun PlacesItem( .clickable { onPlaceClick() } .then(modifier) ) { - LoadImg(modifier = Modifier - .size(height) - .clip(shape), url = place.pic) + LoadImg( + modifier = Modifier + .size(height) + .clip(shape), + url = place.cover, + ) Column( Modifier .fillMaxHeight(0.9f) diff --git a/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt b/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt index a32571f706..279ee8ed94 100644 --- a/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt +++ b/android/app/src/main/java/app/tourism/ui/common/special/RatingBar.kt @@ -17,7 +17,7 @@ import app.tourism.ui.theme.getStarColor @Composable fun RatingBar( - rating: Float, + rating: Int, size: Dp = 30.dp, maxRating: Int = 5, onRatingChanged: ((Float) -> Unit)? = null, diff --git a/android/app/src/main/java/app/tourism/ui/models/SingleChoiceItem.kt b/android/app/src/main/java/app/tourism/ui/models/SingleChoiceItem.kt index c560656242..ad7f825977 100644 --- a/android/app/src/main/java/app/tourism/ui/models/SingleChoiceItem.kt +++ b/android/app/src/main/java/app/tourism/ui/models/SingleChoiceItem.kt @@ -1,6 +1,6 @@ package app.tourism.ui.models data class SingleChoiceItem( - val key: String?, + val key: Any, val label: String ) \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt index 0c1dc139b0..78fdf23190 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt @@ -18,12 +18,12 @@ 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.home.HomeScreen 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.search.SearchScreen import app.tourism.utils.navigateToMap import app.tourism.utils.navigateToMapForRoute import kotlinx.serialization.Serializable @@ -63,27 +63,32 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel val onPlaceClick: (id: Long) -> Unit = { id -> rootNavController.navigate(PlaceDetails(id = id)) } + val onSearchClick: (q: String) -> Unit = { q -> + rootNavController.navigate(Search(query = q)) + } val onMapClick = { navigateToMap(context) } + val onBackClick: () -> Unit = { rootNavController.navigateUp() } NavHost(rootNavController, startDestination = "home_tab") { composable("home_tab") { HomeNavHost( onPlaceClick, + onSearchClick, onMapClick, onCategoryClicked = { rootNavController.navigate("categories_tab") { - popUpTo(rootNavController.graph.findStartDestination().id) { - saveState = true + popUpTo(rootNavController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true - } }, categoriesVM, ) } composable("categories_tab") { - CategoriesNavHost(onPlaceClick, onMapClick, categoriesVM) + CategoriesNavHost(onPlaceClick, onSearchClick, onMapClick, categoriesVM) } composable("favorites_tab") { FavoritesNavHost(onPlaceClick) @@ -95,40 +100,18 @@ fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel val placeDetails = backStackEntry.toRoute() PlaceDetailsScreen( id = placeDetails.id, - onBackClick = { rootNavController.navigateUp() }, + onBackClick = onBackClick, onMapClick = onMapClick, onCreateRoute = { placeLocation -> navigateToMapForRoute(context, placeLocation) } ) } - } -} - -@Composable -fun HomeNavHost( - onPlaceClick: (id: Long) -> Unit, - onMapClick: () -> Unit, - onCategoryClicked: () -> Unit, - categoriesVM: CategoriesViewModel, -) { - val homeNavController = rememberNavController() - NavHost(homeNavController, startDestination = Home) { - composable { - HomeScreen( - onSearchClick = { query -> - homeNavController.navigate(Search(query = query)) - }, - onPlaceClick = onPlaceClick, - onMapClick = onMapClick, - onCategoryClicked = onCategoryClicked, - categoriesVM = categoriesVM - ) - } composable { backStackEntry -> val search = backStackEntry.toRoute() SearchScreen( onPlaceClick = onPlaceClick, + onBackClick = onBackClick, onMapClick = onMapClick, queryArg = search.query, ) @@ -136,16 +119,39 @@ fun HomeNavHost( } } +@Composable +fun HomeNavHost( + onPlaceClick: (id: Long) -> Unit, + onSearchClick: (String) -> Unit, + onMapClick: () -> Unit, + onCategoryClicked: () -> Unit, + categoriesVM: CategoriesViewModel, +) { + val homeNavController = rememberNavController() + NavHost(homeNavController, startDestination = Home) { + composable { + HomeScreen( + onSearchClick = onSearchClick, + onPlaceClick = onPlaceClick, + onMapClick = onMapClick, + onCategoryClicked = onCategoryClicked, + categoriesVM = categoriesVM + ) + } + } +} + @Composable fun CategoriesNavHost( onPlaceClick: (id: Long) -> Unit, + onSearchClick: (String) -> Unit, onMapClick: () -> Unit, categoriesVM: CategoriesViewModel, ) { val categoriesNavController = rememberNavController() NavHost(categoriesNavController, startDestination = Categories) { composable { - CategoriesScreen(onPlaceClick, onMapClick, categoriesVM) + CategoriesScreen(onPlaceClick, onSearchClick, onMapClick, categoriesVM) } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt index 7f98a323a6..386268dc29 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt @@ -21,6 +21,9 @@ class ThemeViewModel @Inject constructor( fun setTheme(themeCode: String) { _theme.value = userPreferences.themes.first { it.code == themeCode } userPreferences.setTheme(themeCode) + } + + fun updateThemeOnServer(themeCode: String){ viewModelScope.launch { profileRepository.updateTheme(themeCode) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt index a47ceadfa4..137b33bc0c 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt @@ -1,13 +1,17 @@ package app.tourism.ui.screens.main.categories.categories +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets 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.foundation.rememberScrollState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold 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 @@ -25,6 +29,7 @@ import app.tourism.ui.common.special.PlacesItem @Composable fun CategoriesScreen( onPlaceClick: (id: Long) -> Unit, + onSearchClick: (String) -> Unit, onMapClick: () -> Unit, categoriesVM: CategoriesViewModel = hiltViewModel() ) { @@ -34,6 +39,11 @@ fun CategoriesScreen( val selectedCategory = selectedCategory.collectAsState().value val places = places.collectAsState().value + LaunchedEffect(true) { + if (selectedCategory == null) + categoriesVM.setSelectedCategory(categories.first()) + } + Scaffold( topBar = { AppTopBar( @@ -47,26 +57,28 @@ fun CategoriesScreen( ), ) }, - contentWindowInsets = Constants.USUAL_WINDOW_INSETS + contentWindowInsets = WindowInsets(bottom = 0.dp) ) { paddingValues -> - LazyColumn( - Modifier - .padding(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) + Column(modifier = Modifier.padding(horizontal = Constants.SCREEN_PADDING)) { + VerticalSpace(height = 16.dp) + AppSearchBar( + modifier = Modifier.fillMaxWidth(), + query = query, + onQueryChanged = ::setQuery, + onSearchClicked = onSearchClick, + onClearClicked = ::clearSearchField, + ) + VerticalSpace(height = 16.dp) + } HorizontalSingleChoice( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = Constants.SCREEN_PADDING), items = categories, selected = selectedCategory, onSelectedChanged = ::setSelectedCategory, @@ -76,7 +88,7 @@ fun CategoriesScreen( } items(places) { item -> - Column { + Column(modifier = Modifier.padding(horizontal = Constants.SCREEN_PADDING)) { PlacesItem( place = item, onPlaceClick = { onPlaceClick(item.id) }, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt index 9f306c0329..dc15c1a2d5 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesViewModel.kt @@ -1,19 +1,29 @@ package app.tourism.ui.screens.main.categories.categories +import android.content.Context import androidx.lifecycle.ViewModel -import app.tourism.Constants +import androidx.lifecycle.viewModelScope +import app.organicmaps.R +import app.tourism.data.repositories.PlacesRepository +import app.tourism.domain.models.categories.PlaceCategory import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.resource.Resource import app.tourism.ui.models.SingleChoiceItem import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class CategoriesViewModel @Inject constructor( + @ApplicationContext context: Context, + private val placesRepository: PlacesRepository, ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -26,10 +36,6 @@ class CategoriesViewModel @Inject constructor( _query.value = value } - fun search(value: String) { - // todo - } - fun clearSearchField() { _query.value = "" } @@ -51,30 +57,34 @@ class CategoriesViewModel @Inject constructor( val places = _places.asStateFlow() fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { - // todo + viewModelScope.launch(Dispatchers.IO) { + placesRepository.setFavorite(item.id, isFavorite) + } + } + + private fun onCategoryChangeGetPlaces() { + viewModelScope.launch(Dispatchers.IO) { + _selectedCategory.collectLatest { item -> + item?.key?.let { id -> + val categoryId = id as Long + placesRepository.getPlacesByCategory(categoryId).collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { _places.value = it } + } + } + } + } + } } init { - // todo replace with real data - _selectedCategory.value = SingleChoiceItem("sights", "Sights") _categories.value = listOf( - SingleChoiceItem("sights", "Sights"), - SingleChoiceItem("restaurants", "Restaurants"), - SingleChoiceItem("hotels", "Hotels"), + SingleChoiceItem(PlaceCategory.Sights.id, context.getString(R.string.sights)), + SingleChoiceItem(PlaceCategory.Restaurants.id, context.getString(R.string.restaurants)), + SingleChoiceItem(PlaceCategory.Hotels.id, context.getString(R.string.hotels)), ) - val dummyData = mutableListOf() - repeat(15) { - dummyData.add( - PlaceShort( - id = it.toLong(), - name = "Гора Эмина", - pic = Constants.IMAGE_URL_EXAMPLE, - rating = 5.0, - excerpt = "завтрак включен, бассейн, сауна, с видом на озеро" - ) - ) - } - _places.update { dummyData } + _selectedCategory.value = categories.value.first() + onCategoryChangeGetPlaces() } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt index 8a757a67de..017dd25248 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/HorizontalSingleChoice.kt @@ -2,6 +2,7 @@ package app.tourism.ui.screens.main.categories.categories 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.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -27,7 +28,7 @@ fun HorizontalSingleChoice( unselectedColor: Color = MaterialTheme.colorScheme.background, itemModifier: Modifier = Modifier, ) { - Row(Modifier.then(modifier)) { + Row(Modifier.then(modifier), horizontalArrangement = Arrangement.spacedBy(10.dp)) { items.forEach { SingleChoiceItem( modifier = itemModifier, @@ -39,7 +40,6 @@ fun HorizontalSingleChoice( selectedColor = selectedColor, unselectedColor = unselectedColor ) - HorizontalSpace(width = 12.dp) } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt index d12e0865ae..9e671b29f7 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt @@ -1,9 +1,11 @@ package app.tourism.ui.screens.main.favorites.favorites +import androidx.compose.foundation.ExperimentalFoundationApi 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.ListItem import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -30,6 +32,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@OptIn(ExperimentalFoundationApi::class) @Composable fun FavoritesScreen( onPlaceClick: (id: Long) -> Unit, @@ -51,9 +54,11 @@ fun FavoritesScreen( modifier = Modifier.focusRequester(focusRequester), query = query, onQueryChanged = { favoritesVM.setQuery(it) }, - onSearchClicked = { favoritesVM.search(it) }, onClearClicked = { favoritesVM.clearSearchField() }, - onBackClicked = { isSearchActive = false }, + onBackClicked = { + isSearchActive = false + favoritesVM.setQuery("") + }, ) } } else { @@ -83,8 +88,8 @@ fun FavoritesScreen( VerticalSpace(16.dp) } - items(places) { item -> - Column { + items(places, key = { it.id }) { item -> + Column(Modifier.animateItem()) { PlacesItem( place = item, onPlaceClick = { onPlaceClick(item.id) }, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt index 9958827839..84a4be7a9b 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesViewModel.kt @@ -1,18 +1,23 @@ package app.tourism.ui.screens.main.favorites.favorites import androidx.lifecycle.ViewModel -import app.tourism.Constants +import androidx.lifecycle.viewModelScope +import app.tourism.data.repositories.PlacesRepository import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class FavoritesViewModel @Inject constructor( + private val placesRepository: PlacesRepository ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -25,10 +30,6 @@ class FavoritesViewModel @Inject constructor( _query.value = value } - fun search(value: String) { - // todo - } - fun clearSearchField() { _query.value = "" } @@ -38,24 +39,25 @@ class FavoritesViewModel @Inject constructor( val places = _places.asStateFlow() fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { - // todo + viewModelScope.launch(Dispatchers.IO) { + placesRepository.setFavorite(item.id, isFavorite) + } + } + + private fun getFavorites() { + viewModelScope.launch(Dispatchers.IO) { + _query.collectLatest { + placesRepository.getFavorites(it).collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { _places.value = it } + } + } + } + } } init { - // todo replace with real data - val dummyData = mutableListOf() - repeat(15) { - dummyData.add( - PlaceShort( - id = it.toLong(), - name = "Гиссарская крепость", - pic = Constants.IMAGE_URL_EXAMPLE, - rating = 5.0, - excerpt = "завтрак включен, бассейн, сауна, с видом на озеро" - ) - ) - } - _places.update { dummyData } + getFavorites() } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt similarity index 94% rename from android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt rename to android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt index 7baf317dda..c44300bbb0 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeScreen.kt @@ -1,4 +1,4 @@ -package app.tourism.ui.screens.main.home.home +package app.tourism.ui.screens.main.home import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -100,12 +100,7 @@ fun HomeScreen( 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) - }, + onSearchClicked = onSearchClick, onClearClicked = { homeVM.clearSearchField() }, ) } @@ -121,7 +116,7 @@ fun HomeScreen( onPlaceClick(item.id) }, setFavoriteChanged = { item, isFavorite -> - + homeVM.setFavoriteChanged(item, isFavorite) }, ) VerticalSpace(height = 24.dp) @@ -133,7 +128,7 @@ fun HomeScreen( onPlaceClick(item.id) }, setFavoriteChanged = { item, isFavorite -> - + homeVM.setFavoriteChanged(item, isFavorite) }, ) @@ -192,7 +187,7 @@ private fun HorizontalPlaces( Place( place = it, onPlaceClick = { onPlaceClick(it) }, - isFavorite = false, + isFavorite = it.isFavorite, onFavoriteChanged = { isFavorite -> setFavoriteChanged(it, isFavorite) }, @@ -226,7 +221,7 @@ private fun Place( .clickable { onPlaceClick() } .then(modifier), ) { - LoadImg(url = place.pic) + LoadImg(url = place.cover) Column( Modifier @@ -241,6 +236,7 @@ private fun Place( maxLines = 2, overflow = TextOverflow.Ellipsis, ) + VerticalSpace(height = 4.dp) Row(verticalAlignment = Alignment.CenterVertically) { Text(text = "%.1f".format(place.rating), style = textStyle) HorizontalSpace(width = 2.dp) @@ -263,7 +259,8 @@ private fun Place( }, ) { Icon( - painterResource(id = if (isFavorite) R.drawable.heart_selected else R.drawable.heart), + modifier = Modifier.size(20.dp), + painter = painterResource(id = if (isFavorite) R.drawable.heart_selected else R.drawable.heart), contentDescription = stringResource(id = R.string.add_to_favorites), tint = Color.White, ) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt new file mode 100644 index 0000000000..9ab15f40bf --- /dev/null +++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/HomeViewModel.kt @@ -0,0 +1,93 @@ +package app.tourism.ui.screens.main.home + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.tourism.data.repositories.PlacesRepository +import app.tourism.domain.models.SimpleResponse +import app.tourism.domain.models.categories.PlaceCategory +import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.resource.Resource +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HomeViewModel @Inject constructor( + private val placesRepository: PlacesRepository +) : ViewModel() { + private val uiChannel = Channel() + val uiEventsChannelFlow = uiChannel.receiveAsFlow() + + // region search query + private val _query = MutableStateFlow("") + val query = _query.asStateFlow() + + fun setQuery(value: String) { + _query.value = value + } + + fun clearSearchField() { + _query.value = "" + } + // endregion search query + + + private val _sights = MutableStateFlow>(emptyList()) + val sights = _sights.asStateFlow() + private fun getTopSights() { + viewModelScope.launch(Dispatchers.IO) { + placesRepository.getTopPlaces(id = PlaceCategory.Sights.id) + .collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { _sights.value = it } + } + } + } + } + + + private val _restaurants = MutableStateFlow>(emptyList()) + val restaurants = _restaurants.asStateFlow() + private fun getTopRestaurants() { + viewModelScope.launch(Dispatchers.IO) { + placesRepository.getTopPlaces(id = PlaceCategory.Restaurants.id) + .collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { _restaurants.value = it } + } + } + } + } + + private val _downloadResponse = MutableStateFlow?>(null) + val downloadResponse = _downloadResponse.asStateFlow() + private fun downloadAllDataIfFirstTime() { + viewModelScope.launch(Dispatchers.IO) { + placesRepository.downloadAllDataIfFirstTime().collectLatest { + _downloadResponse.value = it + } + } + } + + fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + placesRepository.setFavorite(item.id, isFavorite) + } + } + + init { + downloadAllDataIfFirstTime() + getTopSights() + getTopRestaurants() + } +} + +sealed interface UiEvent { + data class ShowToast(val message: String) : UiEvent +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt deleted file mode 100644 index e858424254..0000000000 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeViewModel.kt +++ /dev/null @@ -1,68 +0,0 @@ -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() - 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>(emptyList()) - val sights = _sights.asStateFlow() - - private val _restaurants = MutableStateFlow>(emptyList()) - val restaurants = _restaurants.asStateFlow() - - fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { - // todo - } - - init { - // todo replace with real data - val dummyData = mutableListOf() - repeat(15) { - dummyData.add( - PlaceShort( - id = it.toLong(), - name = "Гора Эмина", - pic = Constants.IMAGE_URL_EXAMPLE, - rating = 5.0, - excerpt = "завтрак включен, бассейн, сауна, с видом на озеро" - ) - ) - } - _sights.update { dummyData } - _restaurants.update { dummyData } - } -} - -sealed interface UiEvent { - data class ShowToast(val message: String) : UiEvent -} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt index 878a11c69f..3e919fcf60 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -37,14 +38,18 @@ fun PlaceDetailsScreen( val place = placeVM.place.collectAsState().value + LaunchedEffect(true) { + placeVM.observePlace(id) + } + Scaffold( topBar = { place?.let { PlaceTopBar( title = it.name, - picUrl = it.pic, + picUrl = it.cover, isFavorite = it.isFavorite, - onFavoriteChanged = { placeVM.setFavoriteChanged(id, it) }, + onFavoriteChanged = { isFavorite -> placeVM.setFavoriteChanged(id, isFavorite) }, onMapClick = onMapClick, onBackClick = onBackClick, ) @@ -57,7 +62,7 @@ fun PlaceDetailsScreen( val pagerState = rememberPagerState(pageCount = { 3 }) VerticalSpace(height = 16.dp) - Box(modifier = Modifier.padding(horizontal = Constants.SCREEN_PADDING)){ + Box(modifier = Modifier.padding(horizontal = Constants.SCREEN_PADDING)) { PlaceTabRow( tabIndex = pagerState.currentPage, onTabIndexChanged = { @@ -79,7 +84,7 @@ fun PlaceDetailsScreen( DescriptionScreen( description = place.description, onCreateRoute = { - place.placeLocation?.let { onCreateRoute(it) } + onCreateRoute(place.placeLocation) }, ) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt index f3021aaf7f..9a0c46e99e 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceTabRow.kt @@ -22,9 +22,9 @@ 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)), + 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) @@ -40,12 +40,9 @@ fun PlaceTabRow(modifier: Modifier = Modifier, tabIndex: Int, onTabIndexChanged: tabs.forEach { SingleChoiceItem( item = it, - isSelected = it.key?.toInt() == tabIndex, + isSelected = it.key == tabIndex, onClick = { - val key = it.key?.toInt() - if (key != null) { - onTabIndexChanged(key) - } + onTabIndexChanged(it.key as Int) }, ) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt index 080b1c828f..3e40e40bad 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/PlaceViewModel.kt @@ -1,20 +1,27 @@ package app.tourism.ui.screens.main.place_details import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import app.tourism.Constants import app.tourism.data.dto.PlaceLocation +import app.tourism.data.repositories.PlacesRepository import app.tourism.domain.models.details.PlaceFull +import app.tourism.domain.models.resource.Resource import app.tourism.utils.makeLongListOfTheSameItem import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class PlaceViewModel @Inject constructor( + private val placesRepository: PlacesRepository ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -23,26 +30,18 @@ class PlaceViewModel @Inject constructor( val place = _place.asStateFlow() fun setFavoriteChanged(itemId: Long, isFavorite: Boolean) { - // todo + viewModelScope.launch(Dispatchers.IO) { + placesRepository.setFavorite(itemId, isFavorite) + } } - 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, - ) + fun observePlace(id: Long) { + viewModelScope.launch(Dispatchers.IO) { + placesRepository.getPlaceById(id).collectLatest { + if(it is Resource.Success) { + _place.value = it.data + } + } } } } @@ -50,25 +49,3 @@ class PlaceViewModel @Inject constructor( sealed interface UiEvent { data class ShowToast(val message: String) : UiEvent } - -val htmlExample = - """ - - - - - - -

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

-

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

-

О месте

-

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

-

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

-

Адрес

-

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

-

Контакты

-

+992 998 201 201

- - - - """.trimIndent() \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt index 3f89684a57..a7763362a4 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/PostReviewViewModel.kt @@ -1,17 +1,30 @@ package app.tourism.ui.screens.main.place_details.reviews +import android.content.Context import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.organicmaps.R +import app.tourism.data.repositories.ReviewsRepository +import app.tourism.domain.models.SimpleResponse +import app.tourism.domain.models.details.ReviewToPost +import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import java.io.File import javax.inject.Inject @HiltViewModel class PostReviewViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val reviewsRepository: ReviewsRepository ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -51,7 +64,31 @@ class PostReviewViewModel @Inject constructor( } } - fun postReview() { + private val _postReviewResponse = MutableStateFlow?>(null) + val postReviewResponse = _postReviewResponse.asStateFlow() + fun postReview(id: Long) { + viewModelScope.launch(Dispatchers.Unconfined) { + reviewsRepository.postReview( + ReviewToPost( + placeId = id, + comment = _comment.value, + rating = _rating.value.toInt(), + images = _files.value + ) + ).collectLatest { + _postReviewResponse.value = it + if (it is Resource.Success) { + uiChannel.send( + UiEvent.ShowToast(it.message ?: context.getString(R.string.great_success)) + ) + uiChannel.send(UiEvent.CloseReviewBottomSheet) + } else if (it is Resource.Error) { + uiChannel.send( + UiEvent.ShowToast(it.message ?: context.getString(R.string.smth_went_wrong)) + ) + } + } + } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt index f00cea8270..ac2a680cc0 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsNavigation.kt @@ -1,6 +1,7 @@ package app.tourism.ui.screens.main.place_details.reviews import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -30,6 +31,10 @@ fun ReviewsNavigation( navController.navigate(ReviewPics(urls = it)) } + LaunchedEffect(Unit) { + reviewsVM.getReviews(placeId) + } + NavHost(navController = navController, startDestination = Reviews) { composable { ReviewsScreen( diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt index 04d4c7737b..3422f7f972 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsScreen.kt @@ -38,6 +38,7 @@ import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getStarColor import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -93,7 +94,7 @@ fun ReviewsScreen( Text(text = stringResource(id = R.string.compose_review)) } - RatingBar(rating = it.toFloat()) + RatingBar(rating = it.roundToInt()) } VerticalSpace(height = 24.dp) @@ -111,13 +112,18 @@ fun ReviewsScreen( userReview?.let { item { - Review(review = userReview, onMoreClick = onMoreClick) + Review( + review = userReview, + onMoreClick = onMoreClick, + onDeleteClick = {} + ) } } - items(3) { - Review(review = reviews[it], onMoreClick = onMoreClick) - } + if (reviews.firstOrNull() != null) + item { + Review(review = reviews[0], onMoreClick = onMoreClick) + } } if (showReviewBottomSheet) @@ -126,6 +132,11 @@ fun ReviewsScreen( containerColor = MaterialTheme.colorScheme.background, onDismissRequest = { showReviewBottomSheet = false }, ) { - PostReview() + PostReview( + placeId, + onPostReviewSuccess = { + showReviewBottomSheet = false + }, + ) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt index 4b6ca8189a..ef3cdb665d 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/ReviewsViewModel.kt @@ -1,20 +1,23 @@ package app.tourism.ui.screens.main.place_details.reviews import androidx.lifecycle.ViewModel -import app.tourism.Constants +import androidx.lifecycle.viewModelScope +import app.tourism.data.repositories.ReviewsRepository 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 app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ReviewsViewModel @Inject constructor( + private val reviewsRepository: ReviewsRepository ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -25,23 +28,13 @@ class ReviewsViewModel @Inject constructor( private val _userReview = MutableStateFlow(null) val userReview = _userReview.asStateFlow() - init { - //todo replace with real data - _reviews.value = makeLongListOfTheSameItem( - Review( - id = 1, - rating = 5.0, - user = User( - id = 1, - name = "Эмин Уайт", - pfpUrl = Constants.IMAGE_URL_EXAMPLE, - countryCodeName = "tj", - ), - date = "2024-06-06".toUserFriendlyDate(), - comment = "Это было прекрасное место! Мне очень понравилось, обязательно поситите это место gnjfhjgefkjgnjcsld\n" + - "Это было прекрасное место! Мне очень понравилось, обязательно поситите это место.", - picsUrls = makeLongListOfTheSameItem(Constants.IMAGE_URL_EXAMPLE, 5) - ) - ) + fun getReviews(id: Long) { + viewModelScope.launch(Dispatchers.IO) { + reviewsRepository.getReviewsForPlace(id).collectLatest { + if (it is Resource.Success) { + it.data?.let { _reviews.value = it } + } + } + } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt index 3d772f6e16..708362d969 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/PostReview.kt @@ -26,39 +26,57 @@ 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.platform.LocalFocusManager 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.domain.models.resource.Resource +import app.tourism.ui.ObserveAsEvents 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.screens.main.place_details.reviews.UiEvent import app.tourism.ui.theme.TextStyles import app.tourism.ui.theme.getBorderColor +import app.tourism.ui.utils.showToast import app.tourism.utils.FileUtils import coil.compose.AsyncImage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File +import kotlin.math.roundToInt @OptIn(ExperimentalLayoutApi::class) @Composable fun PostReview( + placeId: Long, modifier: Modifier = Modifier, + onPostReviewSuccess: () -> Unit, postReviewVM: PostReviewViewModel = hiltViewModel(), ) { val scope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current val context = LocalContext.current val rating = postReviewVM.rating.collectAsState().value val comment = postReviewVM.comment.collectAsState().value val files = postReviewVM.files.collectAsState().value + val postReviewResponse = postReviewVM.postReviewResponse.collectAsState().value + + ObserveAsEvents(flow = postReviewVM.uiEventsChannelFlow) { event -> + when (event) { + UiEvent.CloseReviewBottomSheet -> onPostReviewSuccess() + is UiEvent.ShowToast -> context.showToast(event.message) + } + } + Column( modifier = Modifier .fillMaxWidth() @@ -74,7 +92,10 @@ fun PostReview( ) { Text(text = stringResource(id = R.string.tap_to_rate), style = TextStyles.b3) VerticalSpace(height = 4.dp) - RatingBar(rating = rating, onRatingChanged = { postReviewVM.setRating(it) }) + RatingBar( + rating = rating.roundToInt(), + onRatingChanged = { postReviewVM.setRating(it) }, + ) } VerticalSpace(height = 16.dp) @@ -105,6 +126,7 @@ fun PostReview( File(FileUtils(context).getPath(uri)) ) } + focusManager.clearFocus() } ) { AddPhoto() @@ -114,7 +136,8 @@ fun PostReview( PrimaryButton( label = stringResource(id = R.string.send), - onClick = { postReviewVM.postReview() }, + onClick = { postReviewVM.postReview(placeId) }, + isLoading = postReviewResponse is Resource.Loading ) VerticalSpace(height = 64.dp) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt index a941119b25..6e78448df5 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/place_details/reviews/components/Review.kt @@ -55,7 +55,8 @@ import app.tourism.ui.theme.getHintColor fun Review( modifier: Modifier = Modifier, review: Review, - onMoreClick: (picsUrls: List) -> Unit + onMoreClick: (picsUrls: List) -> Unit, + onDeleteClick: ((reviewId: Long) -> Unit)? = null, ) { Column { HorizontalDivider(color = MaterialTheme.colorScheme.surface) @@ -73,9 +74,9 @@ fun Review( } VerticalSpace(height = 16.dp) - review.rating?.let { + review.rating.let { RatingBar( - rating = it.toFloat(), + rating = it, size = 24.dp, ) VerticalSpace(height = 16.dp) @@ -128,9 +129,14 @@ fun User(modifier: Modifier = Modifier, user: User) { ) HorizontalSpace(width = 8.dp) Column { - - Text(text = user.name, style = TextStyles.h4, fontWeight = FontWeight.W600) - user.countryCodeName?.let { + Text( + text = user.name, + style = TextStyles.h4, + fontWeight = FontWeight.W600, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + user.countryCodeName.let { CountryAsLabel( Modifier.fillMaxWidth(), user.countryCodeName, diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt index 8571e9de3e..4a3707b56f 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt @@ -254,6 +254,7 @@ fun ThemeSwitch(modifier: Modifier = Modifier, themeVM: ThemeViewModel) { onCheckedChange = { isDark -> val themeCode = if (isDark) "dark" else "light" themeVM.setTheme(themeCode) + themeVM.updateThemeOnServer(themeCode) }, colors = SwitchDefaults.colors(uncheckedTrackColor = MaterialTheme.colorScheme.background) ) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/search/SearchScreen.kt similarity index 96% rename from android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt rename to android/app/src/main/java/app/tourism/ui/screens/main/search/SearchScreen.kt index f58aa15847..752cf7510c 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/search/SearchScreen.kt @@ -1,4 +1,4 @@ -package app.tourism.ui.screens.main.home.search +package app.tourism.ui.screens.main.search import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column @@ -30,6 +30,7 @@ import app.tourism.ui.theme.TextStyles @Composable fun SearchScreen( onPlaceClick: (id: Long) -> Unit, + onBackClick: () -> Unit, onMapClick: () -> Unit, queryArg: String, searchVM: SearchViewModel = hiltViewModel() @@ -53,6 +54,7 @@ fun SearchScreen( onClick = onMapClick ), ), + onBackClick = onBackClick ) }, contentWindowInsets = Constants.USUAL_WINDOW_INSETS @@ -66,7 +68,6 @@ fun SearchScreen( modifier = Modifier.fillMaxWidth(), query = query, onQueryChanged = { searchVM.setQuery(it) }, - onSearchClicked = { searchVM.search(it) }, onClearClicked = { searchVM.clearSearchField() }, ) VerticalSpace(height = 16.dp) diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/search/SearchViewModel.kt similarity index 58% rename from android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt rename to android/app/src/main/java/app/tourism/ui/screens/main/search/SearchViewModel.kt index f6da2d57f7..114aca1c37 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/search/SearchViewModel.kt @@ -1,18 +1,23 @@ -package app.tourism.ui.screens.main.home.search +package app.tourism.ui.screens.main.search import androidx.lifecycle.ViewModel -import app.tourism.Constants +import androidx.lifecycle.viewModelScope +import app.tourism.data.repositories.PlacesRepository import app.tourism.domain.models.common.PlaceShort +import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( + private val placesRepository: PlacesRepository ) : ViewModel() { private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() @@ -25,10 +30,6 @@ class SearchViewModel @Inject constructor( _query.value = value } - fun search(value: String) { - // todo - } - fun clearSearchField() { _query.value = "" } @@ -41,24 +42,25 @@ class SearchViewModel @Inject constructor( val itemsNumber = _itemsNumber.asStateFlow() fun setFavoriteChanged(item: PlaceShort, isFavorite: Boolean) { - // todo + viewModelScope.launch(Dispatchers.IO) { + placesRepository.setFavorite(item.id, isFavorite) + } + } + + private fun observeSearch() { + viewModelScope.launch(Dispatchers.IO) { + _query.collectLatest { + placesRepository.search(it).collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { _places.value = it } + } + } + } + } } init { - // todo replace with real data - val dummyData = mutableListOf() - repeat(15) { - dummyData.add( - PlaceShort( - id = it.toLong(), - name = "Гиссарская крепость", - pic = Constants.IMAGE_URL_EXAMPLE, - rating = 5.0, - excerpt = "завтрак включен, бассейн, сауна, с видом на озеро" - ) - ) - } - _places.update { dummyData } + observeSearch() } } diff --git a/android/app/src/main/java/app/tourism/utils/isNotAbsent.kt b/android/app/src/main/java/app/tourism/utils/isNotAbsent.kt new file mode 100644 index 0000000000..96dd4bf594 --- /dev/null +++ b/android/app/src/main/java/app/tourism/utils/isNotAbsent.kt @@ -0,0 +1,3 @@ +package app.tourism.utils + +fun String?.isNotAbsent() = !this.isNullOrBlank() diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 8fed6a4304..0551a7f914 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -2208,7 +2208,7 @@ Выберите язык Попробовать заново Не удается соединиться с сервером, проверьте интернет подключение - Нет изображения + Нет фото Таджикистан Очистить поле поиска Топ-30 мест @@ -2220,4 +2220,5 @@ Пароли не схожи Неправильный формат имейла Сохранено + Мне нраится😄 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 72e604abc9..4d48a9875a 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2261,4 +2261,5 @@ Passwords are not the same Wrong email format Saved + Great success😄