forked from organicmaps/organicmaps
adjust auth, profile, do currency, ongoing: places API/DB
This commit is contained in:
parent
1d6e96e1fe
commit
b7eeeb2ed7
46 changed files with 668 additions and 79 deletions
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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<Currency> currencies;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
) {
|
||||
fun toPersonalData() = PersonalData(
|
||||
fullName = full_name,
|
||||
country = country,
|
||||
pfpUrl = avatar,
|
||||
email = email,
|
||||
language = language,
|
||||
theme = theme,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.data.dto.profile
|
||||
|
||||
data class UserData(val data: User)
|
|
@ -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<CurrenciesList>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "http://nbt.tj/"
|
||||
}
|
||||
}
|
|
@ -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 <T, reified R> Response<T>.parseError(): Resource<R> {
|
|||
println(e.message)
|
||||
Resource.Error(e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.toFormDataRequestBody() = this.toRequestBody("multipart/form-data".toMediaTypeOrNull())
|
||||
|
||||
|
|
|
@ -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<AuthResponseDto>
|
||||
|
||||
|
@ -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<User>
|
||||
suspend fun getUser(): Response<UserData>
|
||||
|
||||
@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<UserData>
|
||||
// endregion profile
|
||||
|
||||
}
|
|
@ -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<Resource<AuthResponse>> = flow {
|
||||
fun signIn(email: String, password: String): Flow<Resource<AuthResponse>> = 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
|
||||
|
|
|
@ -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<Resource<CurrencyRates>> = 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)
|
||||
}
|
||||
}
|
|
@ -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<Resource<PersonalData>> = 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<Resource<PersonalData>> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
16
android/app/src/main/java/app/tourism/db/Database.kt
Normal file
16
android/app/src/main/java/app/tourism/db/Database.kt
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
21
android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt
Normal file
21
android/app/src/main/java/app/tourism/db/dao/FeedbackDao.kt
Normal file
|
@ -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<Feedback>
|
||||
}
|
31
android/app/src/main/java/app/tourism/db/dao/MarkDao.kt
Normal file
31
android/app/src/main/java/app/tourism/db/dao/MarkDao.kt
Normal file
|
@ -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<Place>)
|
||||
|
||||
@Query("DELETE FROM Places")
|
||||
suspend fun deleteAllPlaces()
|
||||
|
||||
@Query("SELECT * FROM Places")
|
||||
suspend fun getAllPlaces(): Flow<List<Place>>
|
||||
|
||||
@Query("SELECT * FROM Places WHERE id = :placeId")
|
||||
suspend fun getPlaceById(placeId: Long): Flow<Place>
|
||||
|
||||
@Query("SELECT * FROM Places WHERE isFavorite == 1")
|
||||
suspend fun getFavoritePlaces(): Flow<List<Place>>
|
||||
|
||||
@Update
|
||||
suspend fun setFavorite(placeId: Long, isFavorite: Boolean)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<String>
|
||||
)
|
26
android/app/src/main/java/app/tourism/db/entities/Mark.kt
Normal file
26
android/app/src/main/java/app/tourism/db/entities/Mark.kt
Normal file
|
@ -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<String>,
|
||||
@Relation(parentColumn = "id", entityColumn = "placeId", entity = Feedback::class)
|
||||
val feedbacks: List<Feedback>,
|
||||
val coordinates: Coordinates,
|
||||
val rating: Double,
|
||||
val isFavorite: Boolean
|
||||
)
|
||||
|
||||
data class Coordinates(
|
||||
val latitude: String,
|
||||
val longitude: String
|
||||
)
|
12
android/app/src/main/java/app/tourism/db/entities/User.kt
Normal file
12
android/app/src/main/java/app/tourism/db/entities/User.kt
Normal file
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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"
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?,
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package app.tourism.domain.models.resource
|
||||
|
||||
sealed class Resource<T>(val data: T? = null, val message: String? = null) {
|
||||
class Loading<T>(data: T? = null): Resource<T>(data)
|
||||
class Idle<T>: Resource<T>()
|
||||
class Loading<T>(data: T? = null): Resource<T>(data)
|
||||
class Success<T>(data: T?, message: String? = null): Resource<T>(data)
|
||||
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -23,11 +23,11 @@ class SignInViewModel @Inject constructor(
|
|||
private val uiChannel = Channel<UiEvent>()
|
||||
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) {
|
||||
|
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CurrencyRates?>(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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -2217,4 +2217,7 @@
|
|||
<string name="hotels_tourism">Отели</string>
|
||||
<string name="add_to_favorites">Добавить в избранное</string>
|
||||
<string name="show_route">Посмотреть маршрут</string>
|
||||
<string name="passwords_not_same">Пароли не схожи</string>
|
||||
<string name="wrong_email_format">Неправильный формат имейла</string>
|
||||
<string name="saved">Сохранено</string>
|
||||
</resources>
|
||||
|
|
|
@ -2258,4 +2258,7 @@
|
|||
<string name="hotels_tourism">Hotels</string>
|
||||
<string name="add_to_favorites">Add to favorites</string>
|
||||
<string name="show_route">Show route</string>
|
||||
<string name="passwords_not_same">Passwords are not the same</string>
|
||||
<string name="wrong_email_format">Wrong email format</string>
|
||||
<string name="saved">Saved</string>
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Reference in a new issue