android: make translations offline

This commit is contained in:
Emin 2025-02-10 01:17:15 +05:00
parent fa4e191fab
commit 661807cde8
14 changed files with 140 additions and 87 deletions

View file

@ -23,7 +23,7 @@ import app.tourism.data.db.entities.ReviewPlannedToPostEntity
HashEntity::class,
CurrencyRatesEntity::class
],
version = 1,
version = 2,
exportSchema = false
)
@TypeConverters(Converters::class)

View file

@ -19,4 +19,7 @@ interface HashesDao {
@Query("SELECT * FROM hashes")
suspend fun getHashes(): List<HashEntity>
@Query("DELETE FROM hashes")
suspend fun deleteHashes()
}

View file

@ -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<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q) AND language =:language")
fun search(q: String = "", language: String): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE categoryId = :categoryId")
fun getPlacesByCategoryId(categoryId: Long): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE categoryId = :categoryId AND language =:language")
fun getPlacesByCategoryId(categoryId: Long, language: String): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE categoryId =:categoryId ORDER BY rating DESC LIMIT 15")
fun getTopPlacesByCategoryId(categoryId: Long): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE categoryId =:categoryId AND language =:language ORDER BY rating DESC LIMIT 15")
fun getTopPlacesByCategoryId(categoryId: Long, language: String): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE id = :placeId")
fun getPlaceById(placeId: Long): Flow<PlaceEntity?>
@Query("SELECT * FROM places WHERE id = :placeId AND language =:language")
fun getPlaceById(placeId: Long, language: String): Flow<PlaceEntity?>
@Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)")
fun getFavoritePlacesFlow(q: String = ""): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q) AND language =:language")
fun getFavoritePlacesFlow(q: String = "", language: String): Flow<List<PlaceEntity>>
@Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q)")
fun getFavoritePlaces(q: String = ""): List<PlaceEntity>
@Query("SELECT * FROM places WHERE isFavorite = 1 AND UPPER(name) LIKE UPPER(:q) AND language =:language")
fun getFavoritePlaces(q: String = "", language: String): List<PlaceEntity>
@Query("UPDATE places SET isFavorite = :isFavorite WHERE id = :placeId")
suspend fun setFavorite(placeId: Long, isFavorite: Boolean)

View file

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

View file

@ -3,9 +3,12 @@ package app.tourism.data.dto
import app.tourism.data.dto.place.PlaceDto
data class AllDataDto(
val attractions: List<PlaceDto>,
val restaurants: List<PlaceDto>,
val accommodations: List<PlaceDto>,
val attractions_ru: List<PlaceDto>,
val attractions_en: List<PlaceDto>,
val restaurants_ru: List<PlaceDto>,
val restaurants_en: List<PlaceDto>,
val accommodations_ru: List<PlaceDto>,
val accommodations_en: List<PlaceDto>,
val attractions_hash: String,
val restaurants_hash: String,
val accommodations_hash: String,

View file

@ -3,6 +3,7 @@ package app.tourism.data.dto
import app.tourism.data.dto.place.PlaceDto
data class CategoryDto(
val data: List<PlaceDto>,
val hash: String
val hash: String,
val ru: List<PlaceDto>,
val en: List<PlaceDto>,
)

View file

@ -3,5 +3,6 @@ package app.tourism.data.dto
import app.tourism.data.dto.place.PlaceDto
data class FavoritesDto(
val data: List<PlaceDto>,
val ru: List<PlaceDto>,
val en: List<PlaceDto>,
)

View file

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

View file

@ -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<Resource<SimpleResponse>> = 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<Review>()
val reviewsEntities = mutableListOf<ReviewEntity>()
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<Resource<List<PlaceShort>>> = 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<Resource<List<PlaceShort>>> = 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<ReviewEntity>()
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<PlaceFull>()
allPlaces.addAll(placesEn)
allPlaces.addAll(placesRu)
placesDao.insertPlaces(allPlaces.map { it.toPlaceEntity(id) })
// update reviews
val reviewsEntities = mutableListOf<ReviewEntity>()
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<Resource<List<PlaceShort>>> = 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<Resource<PlaceFull>> = 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<Resource<List<PlaceShort>>> = channelFlow {
placesDao.getFavoritePlacesFlow("%$q%")
placesDao.getFavoritePlacesFlow("%$q%", language)
.collectLatest { placeEntities ->
send(Resource.Success(placeEntities.map { it.toPlaceShort() }))
}

View file

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

View file

@ -15,6 +15,7 @@ data class PlaceFull(
val pics: List<String> = emptyList(),
val reviews: List<Review>? = 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,
)
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB