From b7eeeb2ed79b7e61c62e4ae0445ceb7791bfd4d7 Mon Sep 17 00:00:00 2001 From: Emin Date: Wed, 3 Jul 2024 11:23:57 +0500 Subject: [PATCH] adjust auth, profile, do currency, ongoing: places API/DB --- android/app/build.gradle | 3 + .../src/main/java/app/tourism/MainActivity.kt | 10 +++ .../data/dto/currency/CurrenciesList.java | 19 +++++ .../tourism/data/dto/currency/Currency.java | 16 +++++ .../java/app/tourism/data/dto/profile/User.kt | 19 +++-- .../app/tourism/data/dto/profile/UserData.kt | 3 + .../app/tourism/data/remote/CurrencyApi.kt | 25 +++++++ .../app/tourism/data/remote/NetworkUtils.kt | 7 +- .../app/tourism/data/remote/TourismApi.kt | 24 +++++-- .../data/repositories/AuthRepository.kt | 6 +- .../repositories/CurrencyRepositoryImpl.kt | 47 ++++++++++++ .../data/repositories/ProfileRepository.kt | 72 ++++++++++++++++--- .../src/main/java/app/tourism/db/Database.kt | 16 +++++ .../app/tourism/db/dao/CurrencyRatesDao.kt | 17 +++++ .../java/app/tourism/db/dao/FeedbackDao.kt | 21 ++++++ .../main/java/app/tourism/db/dao/MarkDao.kt | 31 ++++++++ .../db/entities/CurrencyRatesEntity.kt | 16 +++++ .../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 | 15 ++++ .../main/java/app/tourism/di/NetworkModule.kt | 48 ++++++++++++- .../java/app/tourism/di/RepositoriesModule.kt | 23 +++++- .../domain/models/auth/RegistrationData.kt | 2 +- .../domain/models/profile/CurrencyRates.kt | 6 +- .../domain/models/profile/PersonalData.kt | 7 +- .../domain/models/resource/Resource.kt | 2 +- .../java/app/tourism/ui/common/LoadImage.kt | 18 +++-- .../java/app/tourism/ui/common/WebView.kt | 3 + .../app/tourism/ui/common/nav/BackButton.kt | 2 +- .../tourism/ui/common/textfields/EditText.kt | 8 +-- .../ui/common/textfields/PasswordEditText.kt | 11 ++- .../ui/screens/auth/sign_in/SignInScreen.kt | 6 +- .../screens/auth/sign_in/SignInViewModel.kt | 10 +-- .../ui/screens/auth/sign_up/SignUpScreen.kt | 14 ++-- .../screens/auth/sign_up/SignUpViewModel.kt | 35 +++++++-- .../ui/screens/language/LanguageViewModel.kt | 7 ++ .../tourism/ui/screens/main/ThemeViewModel.kt | 7 ++ .../personal_data/PersonalDataScreen.kt | 31 ++++++-- .../main/profile/profile/ProfileScreen.kt | 14 ++-- .../main/profile/profile/ProfileViewModel.kt | 52 ++++++++++++-- .../main/java/app/tourism/utils/DateUtils.kt | 5 ++ .../app/tourism/utils/getCurrentLocale.kt | 9 +++ .../app/src/main/res/layout/ccp_profile.xml | 1 - .../app/src/main/res/values-ru/strings.xml | 3 + android/app/src/main/res/values/strings.xml | 3 + 46 files changed, 668 insertions(+), 79 deletions(-) create mode 100644 android/app/src/main/java/app/tourism/data/dto/currency/CurrenciesList.java create mode 100644 android/app/src/main/java/app/tourism/data/dto/currency/Currency.java create mode 100644 android/app/src/main/java/app/tourism/data/dto/profile/UserData.kt create mode 100644 android/app/src/main/java/app/tourism/data/remote/CurrencyApi.kt create mode 100644 android/app/src/main/java/app/tourism/data/repositories/CurrencyRepositoryImpl.kt create mode 100644 android/app/src/main/java/app/tourism/db/Database.kt create mode 100644 android/app/src/main/java/app/tourism/db/dao/CurrencyRatesDao.kt create mode 100644 android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt create mode 100644 android/app/src/main/java/app/tourism/db/dao/MarkDao.kt create mode 100644 android/app/src/main/java/app/tourism/db/entities/CurrencyRatesEntity.kt create mode 100644 android/app/src/main/java/app/tourism/db/entities/Feedback.kt create mode 100644 android/app/src/main/java/app/tourism/db/entities/Mark.kt create mode 100644 android/app/src/main/java/app/tourism/db/entities/User.kt create mode 100644 android/app/src/main/java/app/tourism/utils/getCurrentLocale.kt diff --git a/android/app/build.gradle b/android/app/build.gradle index a406507a85..93b5e6af5a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -396,6 +396,8 @@ dependencies { implementation "com.github.skydoves:cloudy:0.1.2" // countries implementation 'com.hbb20:ccp:2.7.3' + // webview + implementation "androidx.webkit:webkit:1.11.0" //Background processing def coroutines = '1.8.1' @@ -409,6 +411,7 @@ dependencies { def retrofit = '2.11.0' implementation "com.squareup.retrofit2:retrofit:$retrofit" implementation "com.squareup.retrofit2:converter-gson:$retrofit" + implementation "com.squareup.retrofit2:converter-simplexml:$retrofit" def okhttp = '5.0.0-alpha.14' implementation "com.squareup.okhttp3:okhttp:$okhttp" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp" diff --git a/android/app/src/main/java/app/tourism/MainActivity.kt b/android/app/src/main/java/app/tourism/MainActivity.kt index 2f15757cb0..a962c99320 100644 --- a/android/app/src/main/java/app/tourism/MainActivity.kt +++ b/android/app/src/main/java/app/tourism/MainActivity.kt @@ -19,6 +19,7 @@ import app.tourism.ui.screens.main.MainSection import app.tourism.ui.screens.main.ThemeViewModel import app.tourism.ui.screens.main.profile.profile.ProfileViewModel import app.tourism.ui.theme.OrganicMapsTheme +import app.tourism.utils.changeSystemAppLanguage import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -68,6 +69,15 @@ class MainActivity : ComponentActivity() { profileVM.getPersonalData() lifecycleScope.launch { profileVM.profileDataResource.collectLatest { + if (it is Resource.Success) { + it.data?.language?.let { lang -> + changeSystemAppLanguage(this@MainActivity, lang) + userPreferences.setLanguage(lang) + } + it.data?.theme?.let { theme -> + themeVM.setTheme(theme) + } + } if (it is Resource.Error) { if (it.message?.contains("unauth", ignoreCase = true) == true) navigateToAuth() diff --git a/android/app/src/main/java/app/tourism/data/dto/currency/CurrenciesList.java b/android/app/src/main/java/app/tourism/data/dto/currency/CurrenciesList.java new file mode 100644 index 0000000000..54025fa75f --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/currency/CurrenciesList.java @@ -0,0 +1,19 @@ +package app.tourism.data.dto.currency; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.ElementList; +import org.simpleframework.xml.Root; + +import java.util.List; + +import app.tourism.domain.models.profile.CurrencyRates; + +@Root(name = "ValCurs") +public class CurrenciesList { + @Attribute(required = false, name = "Date") public String date; + @Attribute(required = false) public String name; + + @ElementList(name = "Valute", inline = true) + public List currencies; +} + diff --git a/android/app/src/main/java/app/tourism/data/dto/currency/Currency.java b/android/app/src/main/java/app/tourism/data/dto/currency/Currency.java new file mode 100644 index 0000000000..381c9208a0 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/currency/Currency.java @@ -0,0 +1,16 @@ +package app.tourism.data.dto.currency; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +@Root(name = "Valute") +public class Currency{ + @Attribute(required = false) public String ID; + + @Element(name = "CharCode") public String charCode; + @Element(name = "Nominal") public Integer nominal; + @Element(name = "Name") public String name; + @Element(name = "Value") public Double value; +} + diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt index a5455eed9a..acf98ff20c 100644 --- a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt +++ b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt @@ -1,12 +1,23 @@ package app.tourism.data.dto.profile +import app.tourism.domain.models.profile.PersonalData + data class User( val id: Long, val avatar: String?, val country: String, val full_name: String, - val language: String, - val phone: String?, - val theme: String, + val email: String, + val language: String?, + val theme: String?, val username: String -) \ No newline at end of file +) { + fun toPersonalData() = PersonalData( + fullName = full_name, + country = country, + pfpUrl = avatar, + email = email, + language = language, + theme = theme, + ) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/UserData.kt b/android/app/src/main/java/app/tourism/data/dto/profile/UserData.kt new file mode 100644 index 0000000000..169883e5e9 --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/dto/profile/UserData.kt @@ -0,0 +1,3 @@ +package app.tourism.data.dto.profile + +data class UserData(val data: User) diff --git a/android/app/src/main/java/app/tourism/data/remote/CurrencyApi.kt b/android/app/src/main/java/app/tourism/data/remote/CurrencyApi.kt new file mode 100644 index 0000000000..415476e1ed --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/remote/CurrencyApi.kt @@ -0,0 +1,25 @@ +package app.tourism.data.remote + +import app.tourism.data.dto.currency.CurrenciesList +import app.tourism.domain.models.resource.Resource +import app.tourism.utils.getCurrentDate +import app.tourism.utils.getCurrentLocale +import com.google.gson.JsonParseException +import retrofit2.HttpException +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Query +import java.io.IOException + +interface CurrencyApi { + + @GET("en/kurs/export_xml.php") + suspend fun getCurrency( + @Query("date") date: String = getCurrentDate(), + @Query("export") export: String = "xmlout" + ): Response + + companion object { + const val BASE_URL = "http://nbt.tj/" + } +} 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 77b02299ff..c7bc23805c 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 @@ -4,6 +4,8 @@ import app.tourism.domain.models.SimpleResponse import app.tourism.domain.models.resource.Resource import com.google.gson.Gson import kotlinx.coroutines.flow.FlowCollector +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONException import retrofit2.HttpException import retrofit2.Response @@ -47,4 +49,7 @@ inline fun Response.parseError(): Resource { println(e.message) Resource.Error(e.toString()) } -} \ No newline at end of file +} + +fun String.toFormDataRequestBody() = this.toRequestBody("multipart/form-data".toMediaTypeOrNull()) + 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 0976616e06..3a7e360570 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,20 +1,24 @@ package app.tourism.data.remote import app.tourism.data.dto.auth.AuthResponseDto -import app.tourism.data.dto.profile.User +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.Field import retrofit2.http.FormUrlEncoded import retrofit2.http.GET +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part interface TourismApi { // region auth @FormUrlEncoded @POST("login") suspend fun signIn( - @Field("username") username: String, + @Field("email") email: String, @Field("password") password: String, ): Response @@ -22,7 +26,7 @@ interface TourismApi { @POST("register") suspend fun signUp( @Field("full_name") fullName: String, - @Field("username") username: String, + @Field("email") email: String, @Field("password") password: String, @Field("password_confirmation") passwordConfirmation: String, @Field("country") country: String, @@ -35,7 +39,19 @@ interface TourismApi { // region profile // todo api request not finished yet @GET("user") - suspend fun getUser(): Response + suspend fun getUser(): Response + + @Multipart + @POST("profile") + suspend fun updateProfile( + @Part("full_name") fullName: RequestBody? = null, + @Part("email") email: RequestBody? = null, + @Part("country") country: RequestBody? = null, + @Part("language") language: RequestBody? = null, + @Part("theme") theme: RequestBody? = null, + @Part("_method") _method: RequestBody? = "PUT".toFormDataRequestBody(), + @Part avatar: MultipartBody.Part? = null + ): Response // endregion profile } \ 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 9436af71e7..c08d9755dc 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 @@ -10,9 +10,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow class AuthRepository(private val api: TourismApi) { - fun signIn(username: String, password: String): Flow> = flow { + fun signIn(email: String, password: String): Flow> = flow { handleCall( - call = { api.signIn(username, password) }, + call = { api.signIn(email, password) }, mapper = { it.toAuthResponse() } ) } @@ -22,7 +22,7 @@ class AuthRepository(private val api: TourismApi) { call = { api.signUp( registrationData.fullName, - registrationData.username, + registrationData.email, registrationData.password, registrationData.passwordConfirmation, registrationData.country 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 new file mode 100644 index 0000000000..15ea8057ed --- /dev/null +++ b/android/app/src/main/java/app/tourism/data/repositories/CurrencyRepositoryImpl.kt @@ -0,0 +1,47 @@ +package app.tourism.data.repositories + +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.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) { + val currenciesDao = db.currencyRatesDao + + suspend fun getCurrency(): Flow> = flow { + currenciesDao.getCurrencyRates()?.let { + emit(Resource.Success(it.toCurrencyRates())) + } + + handleCall( + call = { api.getCurrency() }, + mapper = { + val currencyRates = getCurrencyRatesFromXml(it) + db.currencyRatesDao.updateCurrencyRates(currencyRates.toCurrencyRatesEntity()) + currencyRates + }, + ) + } + + private fun getCurrencyRatesFromXml(it: CurrenciesList): CurrencyRates { + val currencies = it.currencies + fun findValueByCurrencyCode(code: String): Double { + return currencies.firstOrNull { it.charCode == code }?.value ?: NaN + } + + val usd = findValueByCurrencyCode("USD") + val eur = findValueByCurrencyCode("EUR") + val rub = findValueByCurrencyCode("RUB") + + return CurrencyRates(usd, eur, rub) + } +} \ No newline at end of file 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 4e8bc15572..106e2b8876 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,27 +1,79 @@ package app.tourism.data.repositories -import app.tourism.Constants +import android.content.Context +import app.tourism.data.prefs.UserPreferences import app.tourism.data.remote.TourismApi import app.tourism.data.remote.handleCall +import app.tourism.data.remote.toFormDataRequestBody import app.tourism.domain.models.profile.PersonalData import app.tourism.domain.models.resource.Resource +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import java.io.File -class ProfileRepository(private val api: TourismApi) { +class ProfileRepository( + private val api: TourismApi, + private val userPreferences: UserPreferences, + @ApplicationContext private val context: Context +) { fun getPersonalData(): Flow> = flow { handleCall( call = { api.getUser() }, mapper = { - // todo api request not finished yet - PersonalData( - fullName = "Emin A.", - country = "TJ", - pfpUrl = Constants.IMAGE_URL_EXAMPLE, - phone = "+992 987654321", - email = "ohhhcmooonmaaaaaaaaaaan@gmail.com", - ) + it.data.toPersonalData() } ) } + + fun updateProfile( + fullName: String, + country: String, + email: String?, + pfpFile: File? + ): Flow> = + flow { + var pfpMultipart: MultipartBody.Part? = null + if (pfpFile != null) { + val requestBody = pfpFile.asRequestBody("image/*".toMediaType()) + pfpMultipart = + MultipartBody.Part.createFormData("avatar", pfpFile.name, requestBody) + } + + val language = userPreferences.getLanguage()?.code + val theme = userPreferences.getTheme()?.code + + handleCall( + call = { + api.updateProfile( + fullName = fullName.toFormDataRequestBody(), + email = email?.toFormDataRequestBody(), + country = country.toFormDataRequestBody(), + language = language.toString().toFormDataRequestBody(), + theme = theme.toString().toFormDataRequestBody(), + avatar = pfpMultipart + ) + }, + mapper = { it.data.toPersonalData() } + ) + } + + suspend fun updateLanguage(code: String) { + try { + api.updateProfile(language = code.toFormDataRequestBody()) + } catch (e: Exception) { + println(e.message) + } + } + + suspend fun updateTheme(code: String) { + try { + api.updateProfile(theme = code.toFormDataRequestBody()) + } catch (e: Exception) { + println(e.message) + } + } } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/db/Database.kt b/android/app/src/main/java/app/tourism/db/Database.kt new file mode 100644 index 0000000000..e631a49265 --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/Database.kt @@ -0,0 +1,16 @@ +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/CurrencyRatesDao.kt b/android/app/src/main/java/app/tourism/db/dao/CurrencyRatesDao.kt new file mode 100644 index 0000000000..21f928d0e5 --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/dao/CurrencyRatesDao.kt @@ -0,0 +1,17 @@ +package app.tourism.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import app.tourism.db.entities.CurrencyRatesEntity + +@Dao +interface CurrencyRatesDao { + + @Query("SELECT * FROM currency_rates") + fun getCurrencyRates(): CurrencyRatesEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun updateCurrencyRates(entity: CurrencyRatesEntity) +} \ 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 new file mode 100644 index 0000000000..94c8d03427 --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000000..d400370dfc --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/dao/MarkDao.kt @@ -0,0 +1,31 @@ +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/CurrencyRatesEntity.kt b/android/app/src/main/java/app/tourism/db/entities/CurrencyRatesEntity.kt new file mode 100644 index 0000000000..d8c80c68af --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/entities/CurrencyRatesEntity.kt @@ -0,0 +1,16 @@ +package app.tourism.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey +import app.tourism.domain.models.profile.CurrencyRates + +@Entity(tableName = "currency_rates") +data class CurrencyRatesEntity( + @PrimaryKey + val id: Long, + val usd: Double, + val eur: Double, + val rub: Double, +) { + fun toCurrencyRates() = CurrencyRates(usd, eur, rub) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..114325d833 --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/entities/Feedback.kt @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000000..45bbb27abd --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/entities/Mark.kt @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000000..97e6d1fec8 --- /dev/null +++ b/android/app/src/main/java/app/tourism/db/entities/User.kt @@ -0,0 +1,12 @@ +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 f51e467781..b3a15ec763 100644 --- a/android/app/src/main/java/app/tourism/di/DatabaseModule.kt +++ b/android/app/src/main/java/app/tourism/di/DatabaseModule.kt @@ -1,11 +1,26 @@ package app.tourism.di +import android.app.Application +import androidx.room.Room +import app.tourism.db.Database import dagger.Module +import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DatabaseModule { + @Provides + @Singleton + fun provideDatabase(app: Application): Database { + return Room.databaseBuilder( + app, Database::class.java, "tourism_database" + ) + .fallbackToDestructiveMigration() + .allowMainThreadQueries() + .build() + } } \ No newline at end of file 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 41fbaa000d..52fce299e2 100644 --- a/android/app/src/main/java/app/tourism/di/NetworkModule.kt +++ b/android/app/src/main/java/app/tourism/di/NetworkModule.kt @@ -3,7 +3,10 @@ package app.tourism.di import android.content.Context import app.tourism.BASE_URL 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 dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -13,6 +16,9 @@ import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.simplexml.SimpleXmlConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Named import javax.inject.Singleton @Module @@ -20,7 +26,7 @@ import javax.inject.Singleton object NetworkModule { @Provides @Singleton - fun provideApi(okHttpClient: OkHttpClient): TourismApi { + fun provideApi(@Named(MAIN_OKHTTP_LABEL) okHttpClient: OkHttpClient): TourismApi { return Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) @@ -31,6 +37,7 @@ object NetworkModule { @Provides @Singleton + @Named(MAIN_OKHTTP_LABEL) fun provideHttpClient(@ApplicationContext context: Context, userPreferences: UserPreferences): OkHttpClient { return OkHttpClient.Builder() .addInterceptor( @@ -55,4 +62,41 @@ object NetworkModule { }.build() } -} \ No newline at end of file + + + @Provides + @Singleton + @Named(CURRENCY_OKHTTP_LABEL) + fun provideHttpClientForCurrencyRetrofit(): OkHttpClient { + val okHttpClient = OkHttpClient.Builder() + okHttpClient.readTimeout(1, TimeUnit.MINUTES) + okHttpClient.connectTimeout(1, TimeUnit.MINUTES) + .addInterceptor( + HttpLoggingInterceptor() + .setLevel(HttpLoggingInterceptor.Level.BODY) + ) + + return okHttpClient.build() + } + + @Provides + @Singleton + @Named(CURRENCY_RETROFIT_LABEL) + fun provideCurrencyRetrofit(@Named(CURRENCY_OKHTTP_LABEL) client: OkHttpClient): Retrofit { + return Retrofit.Builder() + .baseUrl(CurrencyApi.BASE_URL) + .addConverterFactory(SimpleXmlConverterFactory.create()) + .client(client) + .build() + } + + @Provides + @Singleton + fun provideCurrencyApi(@Named(CURRENCY_RETROFIT_LABEL) retrofit: Retrofit): CurrencyApi { + return retrofit.create(CurrencyApi::class.java) + } +} + +const val MAIN_OKHTTP_LABEL = "main okhttp" +const val CURRENCY_RETROFIT_LABEL = "currency retrofit" +const val CURRENCY_OKHTTP_LABEL = "currency okhttp" \ No newline at end of file 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 e4dfca0f8e..7c20a92ba4 100644 --- a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt +++ b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt @@ -1,11 +1,17 @@ package app.tourism.di +import android.content.Context +import app.tourism.data.prefs.UserPreferences +import app.tourism.data.remote.CurrencyApi 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 dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -20,7 +26,20 @@ object RepositoriesModule { @Provides @Singleton - fun provideProfileRepository(api: TourismApi): ProfileRepository { - return ProfileRepository(api) + fun provideProfileRepository( + api: TourismApi, + userPreferences: UserPreferences, + @ApplicationContext context: Context, + ): ProfileRepository { + return ProfileRepository(api, userPreferences, context) + } + + @Provides + @Singleton + fun provideCurrencyRepository( + api: CurrencyApi, + db: Database + ): CurrencyRepository { + return CurrencyRepository(api, db) } } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt b/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt index e9213cc993..f734bae550 100644 --- a/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt +++ b/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt @@ -2,7 +2,7 @@ package app.tourism.domain.models.auth data class RegistrationData( val fullName: String, - val username: String, + val email: String, val password: String, val passwordConfirmation: String, val country: String, 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 6a49e6aed0..ebb11ad43f 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,3 +1,7 @@ package app.tourism.domain.models.profile -data class CurrencyRates(val usd: Double, val eur: Double, val rub: Double) +import app.tourism.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/domain/models/profile/PersonalData.kt b/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt index 6994858d4f..1ed95df0e4 100644 --- a/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt +++ b/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt @@ -3,7 +3,8 @@ package app.tourism.domain.models.profile data class PersonalData( val fullName: String, val country: String, - val pfpUrl: String, - val phone: String, - val email: String + val pfpUrl: String?, + val email: String, + val language: String?, + val theme: String?, ) diff --git a/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt b/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt index b78bc9b939..a3fcd8f2c6 100644 --- a/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt +++ b/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt @@ -1,8 +1,8 @@ package app.tourism.domain.models.resource sealed class Resource(val data: T? = null, val message: String? = null) { - class Loading(data: T? = null): Resource(data) class Idle: Resource() + class Loading(data: T? = null): Resource(data) class Success(data: T?, message: String? = null): Resource(data) class Error(message: String, data: T? = null): Resource(data, message) } 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 9f093505ed..e0cb7eef4f 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 @@ -4,17 +4,22 @@ 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.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 import coil.request.ImageRequest @@ -25,7 +30,7 @@ fun LoadImg( backgroundColor: Color = MaterialTheme.colorScheme.surface, contentScale: ContentScale = ContentScale.Crop ) { - if (url != null) + if (url != null && url.isNotBlank()) CoilImg( modifier = modifier, url = url, @@ -34,12 +39,17 @@ fun LoadImg( ) else Column( - modifier, + Modifier + .background(color = MaterialTheme.colorScheme.surface, shape = CircleShape) + .then(modifier), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Image(painter = painterResource(id = R.drawable.image), contentDescription = null) - Text(text = stringResource(id = R.string.no_image)) + Text( + text = stringResource(id = R.string.no_image), + style = TextStyles.b2, + textAlign = TextAlign.Center + ) } } diff --git a/android/app/src/main/java/app/tourism/ui/common/WebView.kt b/android/app/src/main/java/app/tourism/ui/common/WebView.kt index ab60b13be8..427b2e7337 100644 --- a/android/app/src/main/java/app/tourism/ui/common/WebView.kt +++ b/android/app/src/main/java/app/tourism/ui/common/WebView.kt @@ -1,8 +1,11 @@ package app.tourism.ui.common import android.webkit.WebView +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.ui.viewinterop.AndroidView +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewFeature @Composable fun WebView(data: String) { diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt index 0c64f0bb67..d6fdc03fb4 100644 --- a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt +++ b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt @@ -19,7 +19,7 @@ fun BackButton( ) { Icon( modifier = Modifier - .size(30.dp) + .size(24.dp) .clickable { onBackClick() } .then(modifier), painter = painterResource(id = R.drawable.back), diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt index 18255bebf4..c975af5685 100644 --- a/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt +++ b/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt @@ -109,9 +109,9 @@ fun EditText( keyboardActions = keyboardActions, visualTransformation = visualTransformation, decorationBox = { - Row { + Row(verticalAlignment = Alignment.Bottom) { leadingIcon?.invoke() - Column(Modifier.fillMaxSize()) { + Column(Modifier.fillMaxSize().weight(1f)) { Text( modifier = Modifier.offset(hintOffset.x.dp, hintOffset.y.dp), text = hint, @@ -119,10 +119,8 @@ fun EditText( color = hintColor, ) it() - Box(Modifier.align(Alignment.End)) { - trailingIcon?.invoke() - } } + trailingIcon?.invoke() } } ) diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt index 1360521ec9..a71d689cf6 100644 --- a/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt +++ b/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt @@ -1,3 +1,4 @@ +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon @@ -7,10 +8,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp import app.organicmaps.R import app.tourism.ui.common.textfields.AuthEditText @@ -31,9 +34,13 @@ fun PasswordEditText( keyboardOptions = keyboardOptions, visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { - IconButton(onClick = { passwordVisible = !passwordVisible }) { + IconButton( + modifier = Modifier.size(24.dp), + onClick = { passwordVisible = !passwordVisible }, + ) { Icon( - painter = painterResource(id = if (passwordVisible) R.drawable.baseline_visibility_24 else R.drawable.baseline_visibility_off_24), + modifier = Modifier.size(24.dp), + painter = painterResource(id = if (passwordVisible) R.drawable.baseline_visibility_24 else com.google.android.material.R.drawable.design_ic_visibility_off), tint = Color.White, contentDescription = null ) diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt index cf8575e728..9ca2519376 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt @@ -45,7 +45,7 @@ fun SignInScreen( val context = LocalContext.current val focusManager = LocalFocusManager.current - val userName = vm.username.collectAsState().value + val userName = vm.email.collectAsState().value val password = vm.password.collectAsState().value val signInResponse = vm.signInResponse.collectAsState().value @@ -93,8 +93,8 @@ fun SignInScreen( VerticalSpace(height = 32.dp) AuthEditText( value = userName, - onValueChange = { vm.setUsername(it) }, - hint = stringResource(id = R.string.username), + onValueChange = { vm.setEmail(it) }, + hint = stringResource(id = R.string.email), keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Next) diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt index c3548bd518..6ec81a4148 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt @@ -23,11 +23,11 @@ class SignInViewModel @Inject constructor( private val uiChannel = Channel() val uiEventsChannelFlow = uiChannel.receiveAsFlow() - private val _username = MutableStateFlow("") - val username = _username.asStateFlow() + private val _email = MutableStateFlow("") + val email = _email.asStateFlow() - fun setUsername(value: String) { - _username.value = value + fun setEmail(value: String) { + _email.value = value } private val _password = MutableStateFlow("") @@ -43,7 +43,7 @@ class SignInViewModel @Inject constructor( fun signIn() { viewModelScope.launch { - authRepository.signIn(username.value, password.value) + authRepository.signIn(email.value, password.value) .collectLatest { resource -> _signInResponse.value = resource if (resource is Resource.Success) { diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt index d4c52b241e..004e284e34 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.viewinterop.AndroidView 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.BlurryContainer import app.tourism.ui.common.VerticalSpace @@ -52,10 +53,12 @@ fun SignUpScreen( val registrationData = vm.registrationData.collectAsState().value val fullName = registrationData?.fullName var countryNameCode = registrationData?.country - val username = registrationData?.username + val email = registrationData?.email val password = registrationData?.password val confirmPassword = registrationData?.passwordConfirmation + val signUpResponse = vm.signUpResponse.collectAsState().value + ObserveAsEvents(flow = vm.uiEventsChannelFlow) { event -> when (event) { is UiEvent.NavigateToMainActivity -> navigateToMainActivity(context) @@ -130,9 +133,9 @@ fun SignUpScreen( ) VerticalSpace(height = 16.dp) AuthEditText( - value = username ?: "", - onValueChange = { vm.setUsername(it) }, - hint = stringResource(id = R.string.username), + value = email ?: "", + onValueChange = { vm.setEmail(it) }, + hint = stringResource(id = R.string.email), keyboardActions = KeyboardActions( onNext = { focusManager.moveFocus(FocusDirection.Next) @@ -157,13 +160,14 @@ fun SignUpScreen( value = confirmPassword ?: "", onValueChange = { vm.setConfirmPassword(it) }, hint = stringResource(id = R.string.confirm_password), - keyboardActions = KeyboardActions(onDone = { onSignUpComplete() }), + keyboardActions = KeyboardActions(onDone = { vm.signUp() }), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), ) VerticalSpace(height = 48.dp) PrimaryButton( modifier = Modifier.fillMaxWidth(), label = stringResource(id = R.string.sign_up), + isLoading = signUpResponse is Resource.Loading, onClick = { vm.signUp() }, ) } diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt index f7e98209e7..7950093de4 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt @@ -1,13 +1,17 @@ package app.tourism.ui.screens.auth.sign_up +import android.content.Context +import android.util.Patterns.EMAIL_ADDRESS import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.organicmaps.R import app.tourism.data.prefs.UserPreferences import app.tourism.data.repositories.AuthRepository import app.tourism.domain.models.auth.AuthResponse import app.tourism.domain.models.auth.RegistrationData import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -18,6 +22,7 @@ import javax.inject.Inject @HiltViewModel class SignUpViewModel @Inject constructor( + @ApplicationContext private val context: Context, private val authRepository: AuthRepository, private val userPreferences: UserPreferences ) : ViewModel() { @@ -44,8 +49,8 @@ class SignUpViewModel @Inject constructor( _registrationData.value = _registrationData.value?.copy(country = value) } - fun setUsername(value: String) { - _registrationData.value = _registrationData.value?.copy(username = value) + fun setEmail(value: String) { + _registrationData.value = _registrationData.value?.copy(email = value) } fun setPassword(value: String) { @@ -62,7 +67,7 @@ class SignUpViewModel @Inject constructor( fun signUp() { viewModelScope.launch { registrationData.value?.let { - if (validatePasswordIsTheSame()) { + if (validateEverything()) { authRepository.signUp(it).collectLatest { resource -> _signUpResponse.value = resource if (resource is Resource.Success) { @@ -77,8 +82,30 @@ class SignUpViewModel @Inject constructor( } } + private fun validateEverything(): Boolean { + return validatePasswordIsTheSame() && validateEmail() + } + private fun validatePasswordIsTheSame(): Boolean { - return registrationData.value?.password == registrationData.value?.passwordConfirmation + if (registrationData.value?.password == registrationData.value?.passwordConfirmation) { + return true + } else { + viewModelScope.launch { + uiChannel.send(UiEvent.ShowToast(context.getString(R.string.passwords_not_same))) + } + return false + } + } + + private fun validateEmail(): Boolean { + if (EMAIL_ADDRESS.matcher(registrationData.value?.email ?: "").matches()) + return true + else { + viewModelScope.launch { + uiChannel.send(UiEvent.ShowToast(context.getString(R.string.wrong_email_format))) + } + return false + } } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageViewModel.kt index 1baa08ca0b..461a2ebbeb 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageViewModel.kt @@ -1,15 +1,19 @@ package app.tourism.ui.screens.language import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import app.tourism.data.prefs.Language import app.tourism.data.prefs.UserPreferences +import app.tourism.data.repositories.ProfileRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class LanguageViewModel @Inject constructor( + private val profileRepository: ProfileRepository, private val userPreferences: UserPreferences ) : ViewModel() { private val _languages = MutableStateFlow(userPreferences.languages) @@ -21,5 +25,8 @@ class LanguageViewModel @Inject constructor( fun updateLanguage(value: Language) { _selectedLanguage.value = value userPreferences.setLanguage(value.code) + viewModelScope.launch { + profileRepository.updateLanguage(value.code) + } } } \ No newline at end of file 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 ce32166bfe..7f98a323a6 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 @@ -1,14 +1,18 @@ package app.tourism.ui.screens.main import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import app.tourism.data.prefs.UserPreferences +import app.tourism.data.repositories.ProfileRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ThemeViewModel @Inject constructor( + private val profileRepository: ProfileRepository, private val userPreferences: UserPreferences, ) : ViewModel() { private val _theme = MutableStateFlow(userPreferences.getTheme()) @@ -17,5 +21,8 @@ class ThemeViewModel @Inject constructor( fun setTheme(themeCode: String) { _theme.value = userPreferences.themes.first { it.code == themeCode } userPreferences.setTheme(themeCode) + viewModelScope.launch { + profileRepository.updateTheme(themeCode) + } } } \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt index 6b04cb5144..c8a73f3064 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt @@ -19,7 +19,11 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -41,6 +45,7 @@ import app.tourism.domain.models.resource.Resource import app.tourism.ui.ObserveAsEvents import app.tourism.ui.common.HorizontalSpace import app.tourism.ui.common.ImagePicker +import app.tourism.ui.common.LoadImg import app.tourism.ui.common.SpaceForNavBar import app.tourism.ui.common.VerticalSpace import app.tourism.ui.common.buttons.PrimaryButton @@ -63,6 +68,8 @@ fun PersonalDataScreen(onBackClick: () -> Unit, profileVM: ProfileViewModel) { val focusManager = LocalFocusManager.current val coroutineScope = rememberCoroutineScope() + var imageChanged by remember { mutableStateOf(false) } + val personalData = profileVM.profileDataResource.collectAsState().value val pfpFile = profileVM.pfpFile.collectAsState().value val fullName = profileVM.fullName.collectAsState().value @@ -94,14 +101,21 @@ fun PersonalDataScreen(onBackClick: () -> Unit, profileVM: ProfileViewModel) { Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { - AsyncImage( - modifier = Modifier + val pfpModifier = + Modifier .size(100.dp) - .clip(CircleShape), - model = pfpFile, - contentScale = ContentScale.Crop, - contentDescription = null, - ) + .clip(CircleShape) + if (!imageChanged) + LoadImg(modifier = pfpModifier, url = data.pfpUrl) + else + AsyncImage( + modifier = Modifier + .size(100.dp) + .clip(CircleShape), + model = pfpFile, + contentScale = ContentScale.Crop, + contentDescription = null, + ) HorizontalSpace(width = 20.dp) ImagePicker( showPreview = false, @@ -110,6 +124,7 @@ fun PersonalDataScreen(onBackClick: () -> Unit, profileVM: ProfileViewModel) { profileVM.setPfpFile( File(FileUtils(context).getPath(uri)) ) + imageChanged = true } } ) { @@ -158,6 +173,7 @@ fun PersonalDataScreen(onBackClick: () -> Unit, profileVM: ProfileViewModel) { text = stringResource(id = R.string.country), fontSize = 12.sp ) + val backgroundColor = MaterialTheme.colorScheme.background.toArgb() val lContentColor = MaterialTheme.colorScheme.onBackground.toArgb() AndroidView( factory = { context -> @@ -172,6 +188,7 @@ fun PersonalDataScreen(onBackClick: () -> Unit, profileVM: ProfileViewModel) { setArrowColor(lContentColor) setCountryForNameCode(countryCodeName) + setDialogBackgroundColor(backgroundColor) setOnCountryChangeListener { profileVM.setCountryCodeName(ccp.selectedCountryNameCode) } 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 e889835937..8571e9de3e 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 @@ -68,6 +68,7 @@ fun ProfileScreen( ) { val context = LocalContext.current val personalData = profileVM.profileDataResource.collectAsState().value + val currencyRates = profileVM.currencyRates.collectAsState().value val signOutResponse = profileVM.signOutResponse.collectAsState().value ObserveAsEvents(flow = profileVM.uiEventsChannelFlow) { event -> @@ -97,9 +98,10 @@ fun ProfileScreen( VerticalSpace(height = 32.dp) } } - // todo currency rates. Couldn't find free api or library :( - CurrencyRates(currencyRates = CurrencyRates(10.88, 10.88, 10.88)) - VerticalSpace(height = 20.dp) + if (currencyRates != null) { + CurrencyRates(currencyRates = currencyRates) + VerticalSpace(height = 20.dp) + } GenericProfileItem( label = stringResource(R.string.personal_data), icon = R.drawable.profile, @@ -181,15 +183,15 @@ fun CurrencyRates(modifier: Modifier = Modifier, currencyRates: CurrencyRates) { ) { CurrencyRatesItem( currency = stringResource(id = R.string.usd), - value = currencyRates.usd.toString(), + value = "%.2f".format(currencyRates.usd), ) CurrencyRatesItem( currency = stringResource(id = R.string.eur), - value = currencyRates.eur.toString(), + value = "%.2f".format(currencyRates.eur), ) CurrencyRatesItem( currency = stringResource(id = R.string.rub), - value = currencyRates.rub.toString(), + value = "%.2f".format(currencyRates.rub), ) } } diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt index 4506732dce..a96250368f 100644 --- a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt +++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt @@ -1,14 +1,19 @@ package app.tourism.ui.screens.main.profile.profile +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.organicmaps.R import app.tourism.data.prefs.UserPreferences import app.tourism.data.repositories.AuthRepository +import app.tourism.data.repositories.CurrencyRepository import app.tourism.data.repositories.ProfileRepository import app.tourism.domain.models.SimpleResponse +import app.tourism.domain.models.profile.CurrencyRates import app.tourism.domain.models.profile.PersonalData import app.tourism.domain.models.resource.Resource import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -20,6 +25,8 @@ import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val currencyRepository: CurrencyRepository, private val profileRepository: ProfileRepository, private val authRepository: AuthRepository, private val userPreferences: UserPreferences @@ -34,18 +41,20 @@ class ProfileViewModel @Inject constructor( fun setPfpFile(pfpFile: File) { _pfpFile.value = pfpFile } - + private val _fullName = MutableStateFlow("") val fullName = _fullName.asStateFlow() fun setFullName(value: String) { _fullName.value = value } - + private val _email = MutableStateFlow("") val email = _email.asStateFlow() + private var currentEmail = "" + fun setEmail(value: String) { _email.value = value } @@ -81,15 +90,34 @@ class ProfileViewModel @Inject constructor( fun save() { viewModelScope.launch { - // todo + if (_personalDataResource.value is Resource.Success) { + profileRepository.updateProfile( + fullName = fullName.value, + country = countryCodeName.value ?: "", + email = if (currentEmail == email.value) null else email.value, + pfpFile.value + ).collectLatest { resource -> + if (resource is Resource.Success) { + resource.data?.let { updatePersonalDataInMemory(it) } + uiChannel.send(UiEvent.ShowToast(context.getString(R.string.saved))) + } + if (resource is Resource.Error) { + uiChannel.send(UiEvent.ShowToast(resource.message ?: "")) + } + } + } } } private fun updatePersonalDataInMemory(personalData: PersonalData) { personalData.let { + _personalDataResource.value = Resource.Success(it) setFullName(it.fullName) - setEmail(it.email) + it.email.let { email -> + setEmail(email) + currentEmail = email + } setCountryCodeName(it.country) } } @@ -116,8 +144,24 @@ class ProfileViewModel @Inject constructor( } // endregion requests + // region currency + private val _currencyRates = MutableStateFlow(null) + val currencyRates = _currencyRates.asStateFlow() + + fun getCurrency() { + viewModelScope.launch { + currencyRepository.getCurrency().collectLatest { + if (it is Resource.Success) { + _currencyRates.value = it.data + } + } + } + } + // endregion currency + init { getPersonalData() + getCurrency() } } diff --git a/android/app/src/main/java/app/tourism/utils/DateUtils.kt b/android/app/src/main/java/app/tourism/utils/DateUtils.kt index e9b32e6982..07908e5621 100644 --- a/android/app/src/main/java/app/tourism/utils/DateUtils.kt +++ b/android/app/src/main/java/app/tourism/utils/DateUtils.kt @@ -33,3 +33,8 @@ fun String.toUserFriendlyDate(dateFormat: String = "yyyy-MM-dd"): String { } return userFriendlyDate } + +fun getCurrentDate(dateFormat: String = "yyyy-MM-dd"): String { + val sdf = SimpleDateFormat(dateFormat) + return sdf.format(Date()) +} \ No newline at end of file diff --git a/android/app/src/main/java/app/tourism/utils/getCurrentLocale.kt b/android/app/src/main/java/app/tourism/utils/getCurrentLocale.kt new file mode 100644 index 0000000000..3ca5daa2ce --- /dev/null +++ b/android/app/src/main/java/app/tourism/utils/getCurrentLocale.kt @@ -0,0 +1,9 @@ +package app.tourism.utils + +import androidx.compose.ui.text.intl.Locale + +fun getCurrentLocale(): String { + var language = Locale.current.language + if (language == "tg") language = "tj" + return language +} \ No newline at end of file diff --git a/android/app/src/main/res/layout/ccp_profile.xml b/android/app/src/main/res/layout/ccp_profile.xml index c85d74e33a..63e94d073e 100644 --- a/android/app/src/main/res/layout/ccp_profile.xml +++ b/android/app/src/main/res/layout/ccp_profile.xml @@ -9,7 +9,6 @@ app:ccp_autoDetectLanguage="true" app:ccp_textGravity="LEFT" app:ccp_padding="0dp" - app:ccpDialog_background="@color/transparent" app:ccpDialog_cornerRadius="16dp" app:ccp_showFullName="true" app:ccp_showPhoneCode="false"> diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml index 5f59741dff..8fed6a4304 100644 --- a/android/app/src/main/res/values-ru/strings.xml +++ b/android/app/src/main/res/values-ru/strings.xml @@ -2217,4 +2217,7 @@ Отели Добавить в избранное Посмотреть маршрут + Пароли не схожи + Неправильный формат имейла + Сохранено diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 480b51ee84..72e604abc9 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2258,4 +2258,7 @@ Hotels Add to favorites Show route + Passwords are not the same + Wrong email format + Saved