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 index 8a259124c4..6d5d693258 100644 --- a/android/app/src/main/java/app/tourism/data/db/Database.kt +++ b/android/app/src/main/java/app/tourism/data/db/Database.kt @@ -23,7 +23,7 @@ import app.tourism.data.db.entities.ReviewPlannedToPostEntity HashEntity::class, CurrencyRatesEntity::class ], - version = 1, + version = 2, exportSchema = false ) @TypeConverters(Converters::class) 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 index a6c757adf4..cf156a79d7 100644 --- 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 @@ -19,4 +19,7 @@ interface HashesDao { @Query("SELECT * FROM hashes") suspend fun getHashes(): List + + @Query("DELETE FROM hashes") + suspend fun deleteHashes() } 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 index 80c59494ae..0b0e07db39 100644 --- 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 @@ -17,26 +17,26 @@ interface PlacesDao { @Query("DELETE FROM places") suspend fun deleteAllPlaces() - @Query("DELETE FROM places WHERE categoryId = :categoryId") - suspend fun deleteAllPlacesByCategory(categoryId: Long) + @Query("DELETE FROM places WHERE categoryId = :categoryId AND language =:language") + suspend fun deleteAllPlacesByCategory(categoryId: Long, language: String) - @Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q)") - fun search(q: String = ""): Flow> + @Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q) AND language =:language") + fun search(q: String = "", language: String): Flow> - @Query("SELECT * FROM places WHERE categoryId = :categoryId") - fun getPlacesByCategoryId(categoryId: Long): Flow> + @Query("SELECT * FROM places WHERE categoryId = :categoryId AND language =:language") + fun getPlacesByCategoryId(categoryId: Long, language: String): Flow> - @Query("SELECT * FROM places WHERE categoryId =:categoryId ORDER BY rating DESC LIMIT 15") - fun getTopPlacesByCategoryId(categoryId: Long): Flow> + @Query("SELECT * FROM places WHERE categoryId =:categoryId AND language =:language ORDER BY rating DESC LIMIT 15") + fun getTopPlacesByCategoryId(categoryId: Long, language: String): Flow> - @Query("SELECT * FROM places WHERE id = :placeId") - fun getPlaceById(placeId: Long): Flow + @Query("SELECT * FROM places WHERE id = :placeId AND language =:language") + fun getPlaceById(placeId: Long, language: String): Flow - @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)") - fun getFavoritePlacesFlow(q: String = ""): Flow> + @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q) AND language =:language") + fun getFavoritePlacesFlow(q: String = "", language: String): Flow> - @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)") - fun getFavoritePlaces(q: String = ""): List + @Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q) AND language =:language") + fun getFavoritePlaces(q: String = "", language: String): List @Query("UPDATE places SET isFavorite = :isFavorite WHERE id = :placeId") suspend fun setFavorite(placeId: Long, isFavorite: Boolean) 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 index 7b211de18d..305397cf3f 100644 --- 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 @@ -2,14 +2,13 @@ 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") +@Entity(tableName = "places", primaryKeys = ["id", "language"] ) data class PlaceEntity( - @PrimaryKey val id: Long, + val id: Long, val categoryId: Long, val name: String, val excerpt: String, @@ -18,7 +17,8 @@ data class PlaceEntity( val gallery: List, @Embedded val coordinates: CoordinatesEntity?, val rating: Double, - val isFavorite: Boolean + val isFavorite: Boolean, + val language: String, ) { fun toPlaceFull() = PlaceFull( id = id, @@ -29,7 +29,8 @@ data class PlaceEntity( placeLocation = coordinates?.toPlaceLocation(name), cover = cover, pics = gallery, - isFavorite = isFavorite + isFavorite = isFavorite, + language = language ) fun toPlaceShort() = PlaceShort( 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 index fe63dedd34..12255937d8 100644 --- a/android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt +++ b/android/app/src/main/java/app/tourism/data/dto/AllDataDto.kt @@ -3,9 +3,12 @@ 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_ru: List, + val attractions_en: List, + val restaurants_ru: List, + val restaurants_en: List, + val accommodations_ru: List, + val accommodations_en: 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 index dcc6bde8fd..164f3cc56e 100644 --- a/android/app/src/main/java/app/tourism/data/dto/CategoryDto.kt +++ b/android/app/src/main/java/app/tourism/data/dto/CategoryDto.kt @@ -3,6 +3,7 @@ package app.tourism.data.dto import app.tourism.data.dto.place.PlaceDto data class CategoryDto( - val data: List, - val hash: String + val hash: String, + val ru: List, + val en: List, ) 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 index 589d662128..e6148e89f1 100644 --- a/android/app/src/main/java/app/tourism/data/dto/FavoritesDto.kt +++ b/android/app/src/main/java/app/tourism/data/dto/FavoritesDto.kt @@ -3,5 +3,6 @@ package app.tourism.data.dto import app.tourism.data.dto.place.PlaceDto data class FavoritesDto( - val data: List, + val ru: List, + val en: List, ) 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 index bbbe6565da..7b9708b6b0 100644 --- 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 @@ -13,7 +13,7 @@ data class PlaceDto( val short_description: String, val long_description: String, ) { - fun toPlaceFull(isFavorite: Boolean) = PlaceFull( + fun toPlaceFull(isFavorite: Boolean, language: String) = PlaceFull( id = id, name = name, rating = rating.toDouble(), @@ -23,6 +23,7 @@ data class PlaceDto( cover = cover, pics = gallery, isFavorite = isFavorite, - reviews = feedbacks?.map { it.toReview() } ?: emptyList() + reviews = feedbacks?.map { it.toReview() } ?: emptyList(), + language = language, ) } \ No newline at end of file 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 index fb4474e59d..1bae979384 100644 --- a/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt +++ b/android/app/src/main/java/app/tourism/data/repositories/PlacesRepository.kt @@ -1,6 +1,7 @@ package app.tourism.data.repositories import android.content.Context +import android.util.Log import app.organicmaps.R import app.tourism.data.db.Database import app.tourism.data.db.entities.FavoriteSyncEntity @@ -9,6 +10,7 @@ import app.tourism.data.db.entities.PlaceEntity import app.tourism.data.db.entities.ReviewEntity import app.tourism.data.dto.FavoritesIdsDto import app.tourism.data.dto.place.PlaceDto +import app.tourism.data.prefs.UserPreferences import app.tourism.data.remote.TourismApi import app.tourism.data.remote.handleGenericCall import app.tourism.data.remote.handleResponse @@ -16,7 +18,6 @@ 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 @@ -27,13 +28,17 @@ import kotlinx.coroutines.flow.flow class PlacesRepository( private val api: TourismApi, db: Database, + val userPreferences: UserPreferences, @ApplicationContext private val context: Context, ) { private val placesDao = db.placesDao private val reviewsDao = db.reviewsDao private val hashesDao = db.hashesDao + private val language = userPreferences.getLanguage()?.code ?: "ru" fun downloadAllData(): Flow> = flow { + // this is for test +// hashesDao.deleteHashes() val hashes = hashesDao.getHashes() val favoritesResponse = handleResponse(call = { api.getFavorites() }, context) @@ -42,48 +47,64 @@ class PlacesRepository( call = { api.getAllPlaces() }, mapper = { data -> // get data - val favorites = + val favoritesEn = if (favoritesResponse is Resource.Success) - favoritesResponse.data?.data?.map { - it.toPlaceFull(true) + favoritesResponse.data?.en?.map { + it.toPlaceFull(true, language = "en") } else null - val reviews = mutableListOf() + val reviewsEntities = mutableListOf() - fun PlaceDto.toEntity(placeCategory: PlaceCategory): PlaceEntity { - var placeFull = this.toPlaceFull(false) + fun PlaceDto.toEntity( + placeCategory: PlaceCategory, + language: String + ): PlaceEntity { + var placeFull = this.toPlaceFull(false, language) placeFull = placeFull.copy( - isFavorite = favorites?.any { it.id == placeFull.id } ?: false + isFavorite = favoritesEn?.any { it.id == placeFull.id } ?: false ) - placeFull.reviews?.let { it1 -> reviews.addAll(it1) } + placeFull.reviews?.let { it1 -> + reviewsEntities.addAll(it1.map { it.toReviewEntity() }) + } return placeFull.toPlaceEntity(placeCategory.id) } - val sightsEntities = data.attractions.map { placeDto -> - placeDto.toEntity(PlaceCategory.Sights) + val sightsEntitiesEn = data.attractions_en.map { placeDto -> + placeDto.toEntity(PlaceCategory.Sights, "en") } - val restaurantsEntities = data.restaurants.map { placeDto -> - placeDto.toEntity(PlaceCategory.Restaurants) + val restaurantsEntitiesEn = data.restaurants_en.map { placeDto -> + placeDto.toEntity(PlaceCategory.Restaurants, "en") } - val hotelsEntities = data.accommodations.map { placeDto -> - placeDto.toEntity(PlaceCategory.Hotels) + val hotelsEntitiesEn = data.accommodations_en.map { placeDto -> + placeDto.toEntity(PlaceCategory.Hotels, "en") + } + val sightsEntitiesRu = data.attractions_ru.map { placeDto -> + placeDto.toEntity(PlaceCategory.Sights, "ru") + } + val restaurantsEntitiesRu = data.restaurants_ru.map { placeDto -> + placeDto.toEntity(PlaceCategory.Restaurants, "ru") + } + val hotelsEntitiesRu = data.accommodations_ru.map { placeDto -> + placeDto.toEntity(PlaceCategory.Hotels, "ru") } // update places placesDao.deleteAllPlaces() - placesDao.insertPlaces(sightsEntities) - placesDao.insertPlaces(restaurantsEntities) - placesDao.insertPlaces(hotelsEntities) + placesDao.insertPlaces(sightsEntitiesEn) + placesDao.insertPlaces(restaurantsEntitiesEn) + placesDao.insertPlaces(hotelsEntitiesEn) + placesDao.insertPlaces(sightsEntitiesRu) + placesDao.insertPlaces(restaurantsEntitiesRu) + placesDao.insertPlaces(hotelsEntitiesRu) // update reviews - val reviewsEntities = reviews.map { it.toReviewEntity() } reviewsDao.deleteAllReviews() reviewsDao.insertReviews(reviewsEntities) // update favorites - favorites?.forEach { + favoritesEn?.forEach { placesDao.setFavorite(it.id, it.isFavorite) } @@ -97,24 +118,24 @@ class PlacesRepository( ) // return response - SimpleResponse(message = context.getString(R.string.great_success)) + SimpleResponse(message = context.getString(R.string.download_successful)) }, context ) } else { - emit(Resource.Success(SimpleResponse(message = context.getString(R.string.great_success)))) + emit(Resource.Success(SimpleResponse(message = context.getString(R.string.download_successful)))) } } fun search(q: String): Flow>> = channelFlow { - placesDao.search("%$q%").collectLatest { placeEntities -> + placesDao.search("%$q%", language).collectLatest { placeEntities -> val places = placeEntities.map { it.toPlaceShort() } send(Resource.Success(places)) } } fun getPlacesByCategoryFromDbFlow(id: Long): Flow>> = channelFlow { - placesDao.getPlacesByCategoryId(categoryId = id) + placesDao.getPlacesByCategoryId(categoryId = id, language) .collectLatest { placeEntities -> send(Resource.Success(placeEntities.map { it.toPlaceShort() })) } @@ -123,50 +144,59 @@ class PlacesRepository( suspend fun getPlacesByCategoryFromApiIfThereIsChange(id: Long) { val hash = hashesDao.getHash(id) - val favorites = placesDao.getFavoritePlaces("") + val favorites = placesDao.getFavoritePlaces("", language) val resource = handleResponse(call = { api.getPlacesByCategory(id, hash?.value ?: "") }, context) - if (hash != null && resource is Resource.Success) + if (hash != null && resource is Resource.Success) { resource.data?.let { categoryDto -> - if (categoryDto.data.isNotEmpty()) { - // update places - 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) - - // update hash - hashesDao.insertHash(hash.copy(value = categoryDto.hash)) + if (categoryDto.hash.isBlank()) return + // update places + placesDao.deleteAllPlacesByCategory(categoryId = id, language) + Log.d("dsf", "Before update places, categoryDto: $categoryDto") + val placesEn = categoryDto.en.map { placeDto -> + var placeFull = placeDto.toPlaceFull(false, "en") + placeFull = + placeFull.copy(isFavorite = favorites.any { it.id == placeFull.id }) + placeFull + } + val placesRu = categoryDto.ru.map { placeDto -> + var placeFull = placeDto.toPlaceFull(false, "ru") + placeFull = + placeFull.copy(isFavorite = favorites.any { it.id == placeFull.id }) + placeFull } - } + val allPlaces = mutableListOf() + allPlaces.addAll(placesEn) + allPlaces.addAll(placesRu) + placesDao.insertPlaces(allPlaces.map { it.toPlaceEntity(id) }) + + // update reviews + val reviewsEntities = mutableListOf() + allPlaces.forEach { place -> + reviewsDao.deleteAllPlaceReviews(place.id) + place.reviews?.map { review -> review.toReviewEntity() } + ?.also { reviewEntity -> reviewsEntities.addAll(reviewEntity) } + } + reviewsDao.insertReviews(reviewsEntities) + + // update hash + hashesDao.insertHash(hash.copy(value = categoryDto.hash)) + } + } } fun getTopPlaces(id: Long): Flow>> = channelFlow { - placesDao.getTopPlacesByCategoryId(categoryId = id) + placesDao.getTopPlacesByCategoryId(categoryId = id, language = language) .collectLatest { placeEntities -> send(Resource.Success(placeEntities.map { it.toPlaceShort() })) } } fun getPlaceById(id: Long): Flow> = channelFlow { - placesDao.getPlaceById(id).collectLatest { placeEntity -> - if(placeEntity != null) + placesDao.getPlaceById(id, language).collectLatest { placeEntity -> + if (placeEntity != null) send(Resource.Success(placeEntity.toPlaceFull())) else send(Resource.Error(message = "Не найдено")) @@ -174,7 +204,7 @@ class PlacesRepository( } fun getFavorites(q: String): Flow>> = channelFlow { - placesDao.getFavoritePlacesFlow("%$q%") + placesDao.getFavoritePlacesFlow("%$q%", language) .collectLatest { placeEntities -> send(Resource.Success(placeEntities.map { it.toPlaceShort() })) } 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 ccaa30a48e..a940a5b222 100644 --- a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt +++ b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt @@ -34,9 +34,10 @@ object RepositoriesModule { fun providePlacesRepository( api: TourismApi, db: Database, + userPreferences: UserPreferences, @ApplicationContext context: Context, ): PlacesRepository { - return PlacesRepository(api, db, context) + return PlacesRepository(api, db, userPreferences, context) } @Provides 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 826a2badff..f76041a361 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 @@ -15,6 +15,7 @@ data class PlaceFull( val pics: List = emptyList(), val reviews: List? = null, val isFavorite: Boolean, + val language: String ) { fun toPlaceShort() = PlaceShort( id = id, @@ -36,5 +37,6 @@ data class PlaceFull( coordinates = placeLocation?.toCoordinatesEntity(), cover = cover, isFavorite = isFavorite, + language = language, ) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt index 0f2c87b73b..ae1f13f994 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -18,6 +19,8 @@ import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.nav.AppTopBar import app.tourism.utils.LocaleHelper import com.jakewharton.processphoenix.ProcessPhoenix +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @Composable fun LanguageScreen( @@ -25,8 +28,10 @@ fun LanguageScreen( vm: LanguageViewModel = hiltViewModel() ) { val context = LocalContext.current + val scope = rememberCoroutineScope() val languages by vm.languages.collectAsState() val selectedLanguage by vm.selectedLanguage.collectAsState() + Scaffold( topBar = { AppTopBar( @@ -39,7 +44,6 @@ fun LanguageScreen( containerColor = MaterialTheme.colorScheme.background, ) { paddingValues -> Column(Modifier.padding(paddingValues)) { - // todo VerticalSpace(height = 16.dp) SingleChoiceCheckBoxes( itemNames = languages.map { it.name }, @@ -47,8 +51,12 @@ fun LanguageScreen( onItemChecked = { name -> val language = languages.first { it.name == name } vm.updateLanguage(language) - LocaleHelper.setLocale(context, language.code) - ProcessPhoenix.triggerRebirth(context) + scope.launch { + LocaleHelper.setLocale(context, language.code) + // this delay is here to make sure that language changes in time + delay(timeMillis = 500L) + ProcessPhoenix.triggerRebirth(context) + } } ) } 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 ff3f8a2c23..002a72e559 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 @@ -79,7 +79,9 @@ class PostReviewViewModel @Inject constructor( _postReviewResponse.value = it if (it is Resource.Success) { uiChannel.send( - UiEvent.ShowToast(it.message ?: context.getString(R.string.great_success)) + UiEvent.ShowToast( + it.message ?: context.getString(R.string.post_review_success) + ) ) uiChannel.send(UiEvent.CloseReviewBottomSheet) } else if (it is Resource.Error) { diff --git a/android/app/src/main/res/drawable/splash_background.png b/android/app/src/main/res/drawable/splash_background.png new file mode 100644 index 0000000000..bab37574db Binary files /dev/null and b/android/app/src/main/res/drawable/splash_background.png differ