forked from organicmaps/organicmaps
set up more libs, utils, and res; finish auth; ongoing: profile
This commit is contained in:
parent
c3c3736f07
commit
15b3613363
79 changed files with 2339 additions and 259 deletions
|
@ -396,7 +396,35 @@ dependencies {
|
|||
implementation "com.github.skydoves:cloudy:0.1.2"
|
||||
// countries
|
||||
implementation 'com.hbb20:ccp:2.7.3'
|
||||
// Google Play Location Services
|
||||
|
||||
//Background processing
|
||||
def coroutines = '1.8.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||
// Coroutine Lifecycle Scopes
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.2'
|
||||
|
||||
// region Network
|
||||
// Retrofit
|
||||
def retrofit = '2.11.0'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofit"
|
||||
def okhttp = '5.0.0-alpha.14'
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttp"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp"
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
def coil_version = '2.6.0'
|
||||
implementation("io.coil-kt:coil-compose:$coil_version")
|
||||
implementation("io.coil-kt:coil-svg:$coil_version")
|
||||
// endregion
|
||||
|
||||
// Room
|
||||
def room = '2.6.1'
|
||||
implementation "androidx.room:room-ktx:$room"
|
||||
implementation "androidx.room:room-runtime:$room"
|
||||
kapt "androidx.room:room-compiler:$room"
|
||||
|
||||
// Google Play Location Services
|
||||
//
|
||||
// Please add symlinks to google/java/app/organicmaps/location for each new gms-enabled flavor below:
|
||||
// ```
|
||||
|
|
|
@ -69,10 +69,12 @@
|
|||
android:resizeableActivity="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/MwmTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="33">
|
||||
<activity
|
||||
android:name="app.tourism.AuthActivity"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize|adjustPan"
|
||||
android:theme="@style/MwmTheme" />
|
||||
<!-- Allows for config and orientation change without killing/restarting main activity -->
|
||||
<activity
|
||||
|
|
|
@ -1,20 +1,40 @@
|
|||
package app.tourism
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.organicmaps.R
|
||||
|
||||
const val TAG = "GLOBAL_TAG"
|
||||
const val BASE_URL = "http://192.168.1.80:8888/api/"
|
||||
|
||||
object Constants {
|
||||
// UI
|
||||
val SCREEN_PADDING = 16.dp
|
||||
val USUAL_WINDOW_INSETS = WindowInsets(
|
||||
left = SCREEN_PADDING,
|
||||
right = SCREEN_PADDING,
|
||||
bottom = 0.dp,
|
||||
top = 0.dp
|
||||
)
|
||||
|
||||
// image loading<
|
||||
const val IMAGE_LOCATION = "https://newgo.livo.tj/storage/" //todo change newgo to delivery
|
||||
|
||||
// image loading
|
||||
const val IMAGE_URL_EXAMPLE =
|
||||
"https://img.freepik.com/free-photo/young-woman-hiker-taking-photo-with-smartphone-on-mountains-peak-in-winter_335224-427.jpg?w=2000"
|
||||
const val THUMBNAIL_URL_EXAMPLE =
|
||||
"https://cdn.pixabay.com/photo/2020/03/24/22/34/illustration-4965674_960_720.jpg"
|
||||
const val LOGO_URL_EXAMPLE = "https://brandeps.com/logo-download/O/OSCE-logo-vector-01.svg"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Modifier.applyAppBorder() = this.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.border),
|
||||
shape = RoundedCornerShape(20.dp)
|
||||
).clip(RoundedCornerShape(20.dp))
|
||||
|
|
|
@ -5,77 +5,74 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.organicmaps.DownloadResourcesLegacyActivity
|
||||
import app.organicmaps.downloader.CountryItem
|
||||
import app.tourism.data.dto.SiteLocation
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var userPreferences: UserPreferences
|
||||
|
||||
private val themeVM: ThemeViewModel by viewModels<ThemeViewModel>()
|
||||
private val profileVM: ProfileViewModel by viewModels<ProfileViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
navigateToMapToDownloadIfNotPresent()
|
||||
// navigateToAuthIfNotAuthed()
|
||||
navigateToAuthIfNotAuthed()
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
setContent {
|
||||
OrganicMapsTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
Greeting(
|
||||
name = "Android",
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
val isDark = themeVM.theme.collectAsState().value?.code == "dark"
|
||||
|
||||
OrganicMapsTheme(darkTheme = isDark) {
|
||||
MainSection(themeVM)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToMapToDownloadIfNotPresent() {
|
||||
val mCurrentCountry = CountryItem.fill("Tajikistan")
|
||||
if(!mCurrentCountry.present) {
|
||||
if (!mCurrentCountry.present) {
|
||||
val intent = Intent(this, DownloadResourcesLegacyActivity::class.java)
|
||||
startActivity(this, intent, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuthIfNotAuthed() {
|
||||
val token = userPreferences.getToken()
|
||||
if (token.isNullOrEmpty()) navigateToAuth()
|
||||
|
||||
profileVM.getPersonalData()
|
||||
lifecycleScope.launch {
|
||||
profileVM.profileDataResource.collectLatest {
|
||||
if (it is Resource.Error) {
|
||||
if (it.message?.contains("unauth", ignoreCase = true) == true)
|
||||
navigateToAuth()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuth() {
|
||||
val intent = Intent(this, AuthActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(this, intent, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
Column {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
val intent = Intent(context, DownloadResourcesLegacyActivity::class.java)
|
||||
intent.putExtra(
|
||||
"end_point",
|
||||
SiteLocation("Name", 38.573, 68.807)
|
||||
)
|
||||
startActivity(context, intent, null)
|
||||
},
|
||||
) {
|
||||
Text(text = "navigate to Map", modifier = modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package app.tourism.data.dto.auth
|
||||
|
||||
import app.tourism.domain.models.auth.AuthResponse
|
||||
|
||||
data class AuthResponseDto(
|
||||
val token: String,
|
||||
) {
|
||||
fun toAuthResponse() = AuthResponse(
|
||||
token = token
|
||||
)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package app.tourism.data.dto.profile
|
||||
|
||||
data class User(
|
||||
val id: Int,
|
||||
val avatar: String?,
|
||||
val country_id: Any?,
|
||||
val created_at: String,
|
||||
val full_name: String,
|
||||
val language: Int,
|
||||
val phone: String,
|
||||
val theme: Int,
|
||||
val updated_at: String,
|
||||
val username: String
|
||||
)
|
|
@ -0,0 +1,50 @@
|
|||
package app.tourism.data.remote
|
||||
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import org.json.JSONException
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
suspend inline fun <T, reified R> FlowCollector<Resource<R>>.handleCall(
|
||||
call: () -> Response<T>,
|
||||
mapper: (T) -> R,
|
||||
emitLoadingStatusBeforeCall: Boolean = true
|
||||
) {
|
||||
if(emitLoadingStatusBeforeCall) emit(Resource.Loading())
|
||||
try {
|
||||
val response = call()
|
||||
val body = response.body()?.let { mapper(it) }
|
||||
if (response.isSuccessful) emit(Resource.Success(body))
|
||||
|
||||
else emit(response.parseError())
|
||||
} catch(e: HttpException) {
|
||||
emit(
|
||||
Resource.Error(
|
||||
message = "Упс! Что-то пошло не так."
|
||||
))
|
||||
} catch(e: IOException) {
|
||||
emit(
|
||||
Resource.Error(
|
||||
message = "Не удается соединиться с сервером, проверьте интернет подключение"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, reified R> Response<T>.parseError(): Resource<R> {
|
||||
return try {
|
||||
val response = Gson()
|
||||
.fromJson(
|
||||
errorBody()?.string().toString(),
|
||||
SimpleResponse::class.java
|
||||
)
|
||||
|
||||
Resource.Error(message = response?.message ?: "")
|
||||
} catch (e: JSONException) {
|
||||
println(e.message)
|
||||
Resource.Error(e.toString())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package app.tourism.data.remote
|
||||
|
||||
import app.tourism.data.dto.auth.AuthResponseDto
|
||||
import app.tourism.data.dto.profile.User
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface TourismApi {
|
||||
// region auth
|
||||
@FormUrlEncoded
|
||||
@POST("login")
|
||||
suspend fun signIn(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
): Response<AuthResponseDto>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("register")
|
||||
suspend fun signUp(
|
||||
@Field("full_name") fullName: String,
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
@Field("password_confirmation") passwordConfirmation: String,
|
||||
@Field("country") country: String,
|
||||
): Response<AuthResponseDto>
|
||||
|
||||
@POST("logout")
|
||||
suspend fun signOut(): Response<SimpleResponse>
|
||||
// endregion auth
|
||||
|
||||
// region profile
|
||||
// todo api request not finished yet
|
||||
@GET("user")
|
||||
suspend fun getUser(): Response<User>
|
||||
// endregion profile
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleCall
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.auth.AuthResponse
|
||||
import app.tourism.domain.models.auth.RegistrationData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
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 {
|
||||
handleCall(
|
||||
call = { api.signIn(username, password) },
|
||||
mapper = { it.toAuthResponse() }
|
||||
)
|
||||
}
|
||||
|
||||
fun signUp(registrationData: RegistrationData) = flow {
|
||||
handleCall(
|
||||
call = {
|
||||
api.signUp(
|
||||
registrationData.fullName,
|
||||
registrationData.username,
|
||||
registrationData.password,
|
||||
registrationData.passwordConfirmation,
|
||||
registrationData.country
|
||||
)
|
||||
},
|
||||
mapper = { it.toAuthResponse() }
|
||||
)
|
||||
}
|
||||
|
||||
fun signOut(): Flow<Resource<SimpleResponse>> = flow {
|
||||
handleCall(
|
||||
call = { api.signOut() },
|
||||
mapper = { it }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import app.tourism.Constants
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleCall
|
||||
import app.tourism.domain.models.profile.PersonalData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class ProfileRepository(private val api: TourismApi) {
|
||||
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",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
11
android/app/src/main/java/app/tourism/di/DatabaseModule.kt
Normal file
11
android/app/src/main/java/app/tourism/di/DatabaseModule.kt
Normal file
|
@ -0,0 +1,11 @@
|
|||
package app.tourism.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
}
|
58
android/app/src/main/java/app/tourism/di/NetworkModule.kt
Normal file
58
android/app/src/main/java/app/tourism/di/NetworkModule.kt
Normal file
|
@ -0,0 +1,58 @@
|
|||
package app.tourism.di
|
||||
|
||||
import android.content.Context
|
||||
import app.tourism.BASE_URL
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApi(okHttpClient: OkHttpClient): TourismApi {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.client(okHttpClient)
|
||||
.build()
|
||||
.create(TourismApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideHttpClient(@ApplicationContext context: Context, userPreferences: UserPreferences): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor()
|
||||
.setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
)
|
||||
.addInterceptor { chain ->
|
||||
val original = chain.request()
|
||||
original.body.toString()
|
||||
|
||||
val requestBuilder = original.newBuilder()
|
||||
|
||||
userPreferences.getToken()?.let {
|
||||
requestBuilder.addHeader("Authorization", "Bearer $it")
|
||||
}
|
||||
|
||||
val request = requestBuilder
|
||||
.addHeader("Accept", "application/json")
|
||||
.method(original.method, original.body).build()
|
||||
|
||||
chain.proceed(request)
|
||||
|
||||
}.build()
|
||||
}
|
||||
}
|
|
@ -11,7 +11,8 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DataModule {
|
||||
object PreferencesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserPreferences(
|
|
@ -0,0 +1,26 @@
|
|||
package app.tourism.di
|
||||
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.repositories.AuthRepository
|
||||
import app.tourism.data.repositories.ProfileRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object RepositoriesModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthRepository(api: TourismApi): AuthRepository {
|
||||
return AuthRepository(api)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideProfileRepository(api: TourismApi): ProfileRepository {
|
||||
return ProfileRepository(api)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.domain.models
|
||||
|
||||
data class SimpleResponse(val message: String)
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.domain.models.auth
|
||||
|
||||
data class AuthResponse(val token: String)
|
|
@ -0,0 +1,9 @@
|
|||
package app.tourism.domain.models.auth
|
||||
|
||||
data class RegistrationData(
|
||||
val fullName: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val passwordConfirmation: String,
|
||||
val country: String,
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package app.tourism.domain.models.profile
|
||||
|
||||
data class CurrencyRates(val usd: Double, val eur: Double, val rub: Double)
|
|
@ -0,0 +1,9 @@
|
|||
package app.tourism.domain.models.profile
|
||||
|
||||
data class PersonalData(
|
||||
val fullName: String,
|
||||
val country: String,
|
||||
val pfpUrl: String,
|
||||
val phone: String,
|
||||
val email: String
|
||||
)
|
|
@ -0,0 +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 Success<T>(data: T?, message: String? = null): Resource<T>(data)
|
||||
class Error<T>(message: String, data: T? = null): Resource<T>(data, message)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package app.tourism.domain.models.resource
|
||||
|
||||
sealed class ResponseResource<T>(val data: T? = null, val message: String? = null) {
|
||||
class Success<T>(data: T?): ResponseResource<T>(data)
|
||||
class Error<T>(message: String, data: T? = null): ResponseResource<T>(data, message)
|
||||
}
|
22
android/app/src/main/java/app/tourism/ui/ComposeUtils.kt
Normal file
22
android/app/src/main/java/app/tourism/ui/ComposeUtils.kt
Normal file
|
@ -0,0 +1,22 @@
|
|||
package app.tourism.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
fun <T> ObserveAsEvents(flow: Flow<T>, onEvent: (T) -> Unit) {
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
LaunchedEffect(flow, lifecycleOwner.lifecycle) {
|
||||
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
flow.collect(onEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package app.tourism.ui.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
@ -15,12 +13,9 @@ import androidx.compose.runtime.setValue
|
|||
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.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.organicmaps.util.log.Logger
|
||||
import com.skydoves.cloudy.Cloudy
|
||||
|
||||
@Composable
|
||||
|
@ -36,7 +31,6 @@ fun BlurryContainer(modifier: Modifier = Modifier, content: @Composable () -> Un
|
|||
.height(height)
|
||||
.align(Alignment.Center)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(color = Color.White.copy(alpha = 0.25f))
|
||||
) {}
|
||||
Column(
|
||||
Modifier
|
||||
|
|
76
android/app/src/main/java/app/tourism/ui/common/LoadImage.kt
Normal file
76
android/app/src/main/java/app/tourism/ui/common/LoadImage.kt
Normal file
|
@ -0,0 +1,76 @@
|
|||
package app.tourism.ui.common
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.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.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 app.organicmaps.R
|
||||
import coil.compose.AsyncImage
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.request.ImageRequest
|
||||
|
||||
@Composable
|
||||
fun LoadImg(
|
||||
url: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
backgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||
contentScale: ContentScale = ContentScale.Crop
|
||||
) {
|
||||
if (url != null)
|
||||
CoilImg(
|
||||
modifier = modifier,
|
||||
url = url,
|
||||
backgroundColor = backgroundColor,
|
||||
contentScale = contentScale
|
||||
)
|
||||
else
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Image(painter = painterResource(id = R.drawable.image), contentDescription = null)
|
||||
Text(text = stringResource(id = R.string.no_image))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CoilImg(
|
||||
modifier: Modifier = Modifier,
|
||||
url: String,
|
||||
backgroundColor: Color,
|
||||
contentScale: ContentScale
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = modifier.background(color = backgroundColor),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.decoderFactory(SvgDecoder.Factory())
|
||||
.crossfade(500)
|
||||
.error(R.drawable.error_centered)
|
||||
.build(),
|
||||
placeholder = painterResource(R.drawable.placeholder),
|
||||
contentDescription = null,
|
||||
contentScale = contentScale
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CoilImgAccompanist() {
|
||||
// CoilImage(
|
||||
// modifier = modifier,
|
||||
// imageModel = url,
|
||||
// contentScale = contentScale,
|
||||
// placeHolder = painterResource(R.drawable.placeholder),
|
||||
// error = painterResource(R.drawable.error_centered)
|
||||
// )
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package app.tourism.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.SpaceForNavBar() {
|
||||
Spacer(modifier = Modifier.height(120.dp))
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
package app.tourism.ui.common.buttons
|
||||
|
||||
import ButtonLoading
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
package app.tourism.ui.common.nav
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.tourism.Constants
|
||||
import app.tourism.ui.common.VerticalSpace
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
|
||||
@Composable
|
||||
|
@ -22,34 +27,56 @@ fun AppTopBar(
|
|||
onBackClick: (() -> Boolean)? = null,
|
||||
actions: List<TopBarActionData> = emptyList()
|
||||
) {
|
||||
Column(modifier = Modifier.then(modifier)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
onBackClick?.let { BackButton(onBackClick = onBackClick) }
|
||||
Row {
|
||||
actions.forEach {
|
||||
TopBarAction(iconDrawable = it.iconDrawable, onClick = it.onClick)
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.then(modifier)
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
onBackClick?.let {
|
||||
BackButton(
|
||||
modifier.align(Alignment.CenterStart),
|
||||
onBackClick = onBackClick
|
||||
)
|
||||
}
|
||||
Row(modifier.align(Alignment.CenterEnd)) {
|
||||
actions.forEach {
|
||||
TopBarAction(
|
||||
iconDrawable = it.iconDrawable,
|
||||
color = it.color,
|
||||
onClick = it.onClick
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
VerticalSpace(height = 12.dp)
|
||||
|
||||
Column(Modifier.padding(horizontal = Constants.SCREEN_PADDING)) {
|
||||
Text(text = title, style = TextStyles.h1, color = MaterialTheme.colorScheme.onBackground)
|
||||
}
|
||||
Text(
|
||||
text = title,
|
||||
style = TextStyles.h1,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class TopBarActionData(@DrawableRes val iconDrawable: Int, val onClick: () -> Unit)
|
||||
data class TopBarActionData(
|
||||
@DrawableRes val iconDrawable: Int,
|
||||
val color: Color? = null,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun TopBarAction(@DrawableRes iconDrawable: Int, onClick: () -> Unit) {
|
||||
fun TopBarAction(
|
||||
@DrawableRes iconDrawable: Int,
|
||||
color: Color? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
IconButton(onClick = onClick) {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
modifier = Modifier
|
||||
.size(30.dp),
|
||||
painter = painterResource(id = iconDrawable),
|
||||
tint = color ?: MaterialTheme.colorScheme.onBackground,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package app.tourism.ui.common.nav
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -18,15 +17,14 @@ fun BackButton(
|
|||
onBackClick: () -> Boolean,
|
||||
tint: Color = MaterialTheme.colorScheme.onBackground
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.padding(12.dp).then(modifier),
|
||||
onClick = { onBackClick() }
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(28.dp),
|
||||
painter = painterResource(id = R.drawable.back),
|
||||
tint = tint,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
modifier = modifier
|
||||
.size(24.dp)
|
||||
.clickable { onBackClick() }
|
||||
.then(modifier),
|
||||
painter = painterResource(id = R.drawable.back),
|
||||
tint = tint,
|
||||
contentDescription = null
|
||||
)
|
||||
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -15,7 +14,8 @@ import app.tourism.ui.theme.TextStyles
|
|||
|
||||
@Composable
|
||||
fun AppEditText(
|
||||
value: MutableState<String>,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
hint: String = "",
|
||||
isError: () -> Boolean = { false },
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
|
@ -23,6 +23,7 @@ fun AppEditText(
|
|||
) {
|
||||
EditText(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
hint = hint,
|
||||
hintColor = Color.Gray,
|
||||
isError = isError,
|
||||
|
|
|
@ -5,18 +5,19 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
|
||||
@Composable
|
||||
fun AuthEditText(
|
||||
value: MutableState<String>,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
hint: String = "",
|
||||
isError: () -> Boolean = { false },
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
|
@ -27,6 +28,7 @@ fun AuthEditText(
|
|||
) {
|
||||
EditText(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
hint = hint,
|
||||
hintColor = Color.White,
|
||||
isError = isError,
|
||||
|
@ -34,7 +36,8 @@ fun AuthEditText(
|
|||
textFieldPadding = PaddingValues(vertical = 8.dp),
|
||||
hintFontSizeInt = 16,
|
||||
textSize = 16.sp,
|
||||
textStyle = TextStyles.h3.copy(
|
||||
textStyle = TextStyle(
|
||||
fontWeight = FontWeight.W900,
|
||||
textAlign = TextAlign.Start,
|
||||
color = Color.White,
|
||||
),
|
||||
|
|
|
@ -4,7 +4,6 @@ import androidx.compose.animation.animateColorAsState
|
|||
import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.animation.core.animateOffsetAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
@ -21,7 +20,6 @@ import androidx.compose.material3.Divider
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
@ -47,7 +45,8 @@ enum class EtState { Focused, Unfocused, Error }
|
|||
|
||||
@Composable
|
||||
fun EditText(
|
||||
value: MutableState<String>,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
hint: String = "",
|
||||
hintColor: Color = Color.Gray,
|
||||
|
@ -73,7 +72,7 @@ fun EditText(
|
|||
) {
|
||||
var etState by remember { mutableStateOf(EtState.Unfocused) }
|
||||
|
||||
val hintCondition = etState == EtState.Unfocused && value.value.isEmpty()
|
||||
val hintCondition = etState == EtState.Unfocused && value.isEmpty()
|
||||
val hintOffset by animateOffsetAsState(
|
||||
targetValue = if (hintCondition) Offset(0f, 0f)
|
||||
else Offset(0f, -(hintFontSizeInt * 1.3f))
|
||||
|
@ -91,9 +90,9 @@ fun EditText(
|
|||
etState = if (it.hasFocus) EtState.Focused else EtState.Unfocused
|
||||
}
|
||||
.fillMaxWidth(),
|
||||
value = value.value,
|
||||
value = value,
|
||||
onValueChange = {
|
||||
value.value = it
|
||||
onValueChange(it)
|
||||
etState = if (isError()) EtState.Error else EtState.Focused
|
||||
},
|
||||
cursorBrush = cursorBrush,
|
||||
|
|
|
@ -3,14 +3,12 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import app.organicmaps.R
|
||||
|
@ -18,7 +16,8 @@ import app.tourism.ui.common.textfields.AuthEditText
|
|||
|
||||
@Composable
|
||||
fun PasswordEditText(
|
||||
value: MutableState<String>,
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
hint: String,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default
|
||||
|
@ -26,6 +25,7 @@ fun PasswordEditText(
|
|||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
AuthEditText(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
hint = hint,
|
||||
keyboardActions = keyboardActions,
|
||||
keyboardOptions = keyboardOptions,
|
||||
|
@ -33,7 +33,7 @@ fun PasswordEditText(
|
|||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
painter = painterResource(id = if (passwordVisible) R.drawable.baseline_visibility_24 else com.google.android.material.R.drawable.design_ic_visibility_off),
|
||||
painter = painterResource(id = if (passwordVisible) R.drawable.baseline_visibility_24 else R.drawable.baseline_visibility_off_24),
|
||||
tint = Color.White,
|
||||
contentDescription = null
|
||||
)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package app.tourism.ui.common.ui_state
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
package app.tourism.ui.common.ui_state
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package app.tourism.ui.screens.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -34,7 +37,16 @@ fun AuthNavigation() {
|
|||
|
||||
val navigateUp = { navController.navigateUp() }
|
||||
|
||||
NavHost(navController = navController, startDestination = Welcome) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Welcome,
|
||||
enterTransition = {
|
||||
EnterTransition.None
|
||||
},
|
||||
exitTransition = {
|
||||
ExitTransition.None
|
||||
}
|
||||
) {
|
||||
composable<Welcome>() {
|
||||
WelcomeScreen(
|
||||
onLanguageClicked = { navController.navigate(route = Language) },
|
||||
|
@ -44,22 +56,16 @@ fun AuthNavigation() {
|
|||
}
|
||||
composable<SignIn> {
|
||||
SignInScreen(
|
||||
onSignInClicked = {
|
||||
// todo
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
onSignInComplete = {
|
||||
navigateToMainActivity(context)
|
||||
},
|
||||
onBackClick = navigateUp
|
||||
)
|
||||
}
|
||||
composable<SignUp> {
|
||||
SignUpScreen(
|
||||
onSignUpClicked = {
|
||||
// todo
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
onSignUpComplete = {
|
||||
navigateToMainActivity(context)
|
||||
},
|
||||
onBackClick = navigateUp
|
||||
)
|
||||
|
@ -70,4 +76,10 @@ fun AuthNavigation() {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateToMainActivity(context: Context) {
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
|
@ -11,36 +11,51 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import app.organicmaps.R
|
||||
import app.tourism.Constants
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import app.tourism.ui.ObserveAsEvents
|
||||
import app.tourism.ui.common.BlurryContainer
|
||||
import app.tourism.ui.common.VerticalSpace
|
||||
import app.tourism.ui.common.buttons.PrimaryButton
|
||||
import app.tourism.ui.common.nav.BackButton
|
||||
import app.tourism.ui.common.textfields.AuthEditText
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
import app.tourism.ui.utils.showToast
|
||||
|
||||
@Composable
|
||||
fun SignInScreen(
|
||||
onSignInClicked: () -> Unit,
|
||||
onSignInComplete: () -> Unit,
|
||||
onBackClick: () -> Boolean,
|
||||
vm: SignInViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val userName = remember { mutableStateOf("") }
|
||||
val password = remember { mutableStateOf("") }
|
||||
val userName = vm.username.collectAsState().value
|
||||
val password = vm.password.collectAsState().value
|
||||
|
||||
val signInResponse = vm.signInResponse.collectAsState().value
|
||||
|
||||
ObserveAsEvents(flow = vm.uiEventsChannelFlow) { event ->
|
||||
when (event) {
|
||||
is UiEvent.NavigateToMainActivity -> onSignInComplete()
|
||||
is UiEvent.ShowToast -> context.showToast(event.message)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Image(
|
||||
|
@ -50,49 +65,59 @@ fun SignInScreen(
|
|||
contentDescription = null
|
||||
)
|
||||
|
||||
BackButton(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
onBackClick = onBackClick,
|
||||
tint = Color.White
|
||||
)
|
||||
Box(Modifier.padding(Constants.SCREEN_PADDING)) {
|
||||
BackButton(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
onBackClick = onBackClick,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
BlurryContainer(
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth()
|
||||
.padding(Constants.SCREEN_PADDING),
|
||||
.align(Alignment.TopCenter)
|
||||
) {
|
||||
Column(Modifier.padding(36.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = R.string.sign_in_title),
|
||||
style = TextStyles.h2,
|
||||
color = Color.White
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
AuthEditText(
|
||||
value = userName,
|
||||
hint = stringResource(id = R.string.username),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
PasswordEditText(
|
||||
value = password,
|
||||
hint = stringResource(id = R.string.password),
|
||||
keyboardActions = KeyboardActions(onDone = { onSignInClicked() }),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
)
|
||||
VerticalSpace(height = 48.dp)
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(id = R.string.sign_in),
|
||||
onClick = { onSignInClicked() },
|
||||
)
|
||||
VerticalSpace(height = 48.dp)
|
||||
BlurryContainer(
|
||||
Modifier
|
||||
.padding(Constants.SCREEN_PADDING),
|
||||
) {
|
||||
Column(Modifier.padding(36.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = R.string.sign_in_title),
|
||||
style = TextStyles.h2,
|
||||
color = Color.White
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
AuthEditText(
|
||||
value = userName,
|
||||
onValueChange = { vm.setUsername(it) },
|
||||
hint = stringResource(id = R.string.username),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
PasswordEditText(
|
||||
value = password,
|
||||
onValueChange = { vm.setPassword(it) },
|
||||
hint = stringResource(id = R.string.password),
|
||||
keyboardActions = KeyboardActions(onDone = { onSignInComplete() }),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
)
|
||||
VerticalSpace(height = 48.dp)
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(id = R.string.sign_in),
|
||||
isLoading = signInResponse is Resource.Loading,
|
||||
onClick = { vm.signIn() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package app.tourism.ui.screens.auth.sign_in
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.repositories.AuthRepository
|
||||
import app.tourism.domain.models.auth.AuthResponse
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SignInViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val userPreferences: UserPreferences
|
||||
) : ViewModel() {
|
||||
private val uiChannel = Channel<UiEvent>()
|
||||
val uiEventsChannelFlow = uiChannel.receiveAsFlow()
|
||||
|
||||
private val _username = MutableStateFlow("")
|
||||
val username = _username.asStateFlow()
|
||||
|
||||
fun setUsername(value: String) {
|
||||
_username.value = value
|
||||
}
|
||||
|
||||
private val _password = MutableStateFlow("")
|
||||
val password = _password.asStateFlow()
|
||||
|
||||
fun setPassword(value: String) {
|
||||
_password.value = value
|
||||
}
|
||||
|
||||
|
||||
private val _signInResponse = MutableStateFlow<Resource<AuthResponse>>(Resource.Idle())
|
||||
val signInResponse = _signInResponse.asStateFlow()
|
||||
|
||||
fun signIn() {
|
||||
viewModelScope.launch {
|
||||
authRepository.signIn(username.value, password.value)
|
||||
.collectLatest { resource ->
|
||||
_signInResponse.value = resource
|
||||
if (resource is Resource.Success) {
|
||||
userPreferences.setToken(resource.data?.token)
|
||||
uiChannel.send(UiEvent.NavigateToMainActivity)
|
||||
} else if (resource is Resource.Error) {
|
||||
uiChannel.send(UiEvent.ShowToast(resource.message ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface UiEvent {
|
||||
data object NavigateToMainActivity : UiEvent
|
||||
data class ShowToast(val message: String) : UiEvent
|
||||
}
|
|
@ -13,45 +13,59 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusDirection
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import app.organicmaps.R
|
||||
import app.tourism.Constants
|
||||
import app.tourism.ui.ObserveAsEvents
|
||||
import app.tourism.ui.common.BlurryContainer
|
||||
import app.tourism.ui.common.VerticalSpace
|
||||
import app.tourism.ui.common.buttons.PrimaryButton
|
||||
import app.tourism.ui.common.nav.BackButton
|
||||
import app.tourism.ui.common.textfields.AuthEditText
|
||||
import app.tourism.ui.screens.auth.navigateToMainActivity
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
import app.tourism.ui.utils.showToast
|
||||
import com.hbb20.CountryCodePicker
|
||||
|
||||
@Composable
|
||||
fun SignUpScreen(
|
||||
onSignUpClicked: () -> Unit,
|
||||
onSignUpComplete: () -> Unit,
|
||||
onBackClick: () -> Boolean,
|
||||
vm: SignUpViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
val fullName = remember { mutableStateOf("") }
|
||||
var countryNameCode by remember { mutableStateOf("") }
|
||||
val username = remember { mutableStateOf("") }
|
||||
val password = remember { mutableStateOf("") }
|
||||
val confirmPassword = remember { mutableStateOf("") }
|
||||
val registrationData = vm.registrationData.collectAsState().value
|
||||
val fullName = registrationData?.fullName
|
||||
var countryNameCode = registrationData?.country
|
||||
val username = registrationData?.username
|
||||
val password = registrationData?.password
|
||||
val confirmPassword = registrationData?.passwordConfirmation
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
ObserveAsEvents(flow = vm.uiEventsChannelFlow) { event ->
|
||||
when (event) {
|
||||
is UiEvent.NavigateToMainActivity -> navigateToMainActivity(context)
|
||||
is UiEvent.ShowToast -> context.showToast(event.message)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
painter = painterResource(id = R.drawable.splash_background),
|
||||
|
@ -59,90 +73,100 @@ fun SignUpScreen(
|
|||
contentDescription = null
|
||||
)
|
||||
|
||||
BackButton(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
onBackClick = onBackClick,
|
||||
tint = Color.White
|
||||
)
|
||||
Box(Modifier.padding(Constants.SCREEN_PADDING)) {
|
||||
BackButton(
|
||||
modifier = Modifier.align(Alignment.TopStart),
|
||||
onBackClick = onBackClick,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
BlurryContainer(
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth()
|
||||
.padding(Constants.SCREEN_PADDING),
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(36.dp)
|
||||
.align(alignment = Alignment.TopCenter)) {
|
||||
VerticalSpace(height = 48.dp)
|
||||
BlurryContainer(
|
||||
Modifier
|
||||
.padding(Constants.SCREEN_PADDING),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = R.string.sign_up_title),
|
||||
style = TextStyles.h2,
|
||||
color = Color.White
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
AuthEditText(
|
||||
value = fullName,
|
||||
hint = stringResource(id = R.string.full_name),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.country_code_picker, null, false)
|
||||
val ccp = view.findViewById<CountryCodePicker>(R.id.ccp)
|
||||
ccp.setCountryForNameCode("TJ")
|
||||
ccp.setOnCountryChangeListener {
|
||||
countryNameCode = ccp.selectedCountryNameCode
|
||||
}
|
||||
view
|
||||
})
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
thickness = 1.dp
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
AuthEditText(
|
||||
value = username,
|
||||
hint = stringResource(id = R.string.username),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
PasswordEditText(
|
||||
value = password,
|
||||
hint = stringResource(id = R.string.password),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 32.dp)
|
||||
PasswordEditText(
|
||||
value = confirmPassword,
|
||||
hint = stringResource(id = R.string.confirm_password),
|
||||
keyboardActions = KeyboardActions(onDone = { onSignUpClicked() }),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
)
|
||||
VerticalSpace(height = 48.dp)
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(id = R.string.sign_up),
|
||||
onClick = { onSignUpClicked() },
|
||||
)
|
||||
Column(
|
||||
Modifier.padding(36.dp)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
text = stringResource(id = R.string.sign_up_title),
|
||||
style = TextStyles.h2,
|
||||
color = Color.White
|
||||
)
|
||||
VerticalSpace(height = 16.dp)
|
||||
AuthEditText(
|
||||
value = fullName ?: "",
|
||||
onValueChange = { vm.setFullName(it) },
|
||||
hint = stringResource(id = R.string.full_name),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 16.dp)
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.ccp_auth, null, false)
|
||||
val ccp = view.findViewById<CountryCodePicker>(R.id.ccp)
|
||||
ccp.setCountryForNameCode("TJ")
|
||||
ccp.setOnCountryChangeListener {
|
||||
vm.setCountryNameCode(ccp.selectedCountryNameCode)
|
||||
}
|
||||
view
|
||||
})
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.White,
|
||||
thickness = 1.dp
|
||||
)
|
||||
VerticalSpace(height = 16.dp)
|
||||
AuthEditText(
|
||||
value = username ?: "",
|
||||
onValueChange = { vm.setUsername(it) },
|
||||
hint = stringResource(id = R.string.username),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 16.dp)
|
||||
PasswordEditText(
|
||||
value = password ?: "",
|
||||
onValueChange = { vm.setPassword(it) },
|
||||
hint = stringResource(id = R.string.password),
|
||||
keyboardActions = KeyboardActions(
|
||||
onNext = {
|
||||
focusManager.moveFocus(FocusDirection.Next)
|
||||
},
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
)
|
||||
VerticalSpace(height = 16.dp)
|
||||
PasswordEditText(
|
||||
value = confirmPassword ?: "",
|
||||
onValueChange = { vm.setConfirmPassword(it) },
|
||||
hint = stringResource(id = R.string.confirm_password),
|
||||
keyboardActions = KeyboardActions(onDone = { onSignUpComplete() }),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
)
|
||||
VerticalSpace(height = 48.dp)
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(id = R.string.sign_up),
|
||||
onClick = { vm.signUp() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package app.tourism.ui.screens.auth.sign_up
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SignUpViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
private val userPreferences: UserPreferences
|
||||
) : ViewModel() {
|
||||
private val uiChannel = Channel<UiEvent>()
|
||||
val uiEventsChannelFlow = uiChannel.receiveAsFlow()
|
||||
|
||||
private val _registrationData =
|
||||
MutableStateFlow<RegistrationData?>(
|
||||
RegistrationData(
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"TJ"
|
||||
),
|
||||
)
|
||||
val registrationData = _registrationData.asStateFlow()
|
||||
|
||||
fun setFullName(value: String) {
|
||||
_registrationData.value = _registrationData.value?.copy(fullName = value)
|
||||
}
|
||||
|
||||
fun setCountryNameCode(value: String) {
|
||||
_registrationData.value = _registrationData.value?.copy(country = value)
|
||||
}
|
||||
|
||||
fun setUsername(value: String) {
|
||||
_registrationData.value = _registrationData.value?.copy(username = value)
|
||||
}
|
||||
|
||||
fun setPassword(value: String) {
|
||||
_registrationData.value = _registrationData.value?.copy(password = value)
|
||||
}
|
||||
|
||||
fun setConfirmPassword(value: String) {
|
||||
_registrationData.value = _registrationData.value?.copy(passwordConfirmation = value)
|
||||
}
|
||||
|
||||
private val _signUpResponse = MutableStateFlow<Resource<AuthResponse>>(Resource.Idle())
|
||||
val signUpResponse = _signUpResponse.asStateFlow()
|
||||
|
||||
fun signUp() {
|
||||
viewModelScope.launch {
|
||||
registrationData.value?.let {
|
||||
if (validatePasswordIsTheSame()) {
|
||||
authRepository.signUp(it).collectLatest { resource ->
|
||||
_signUpResponse.value = resource
|
||||
if (resource is Resource.Success) {
|
||||
userPreferences.setToken(resource.data?.token)
|
||||
uiChannel.send(UiEvent.NavigateToMainActivity)
|
||||
} else if (resource is Resource.Error) {
|
||||
uiChannel.send(UiEvent.ShowToast(resource.message ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun validatePasswordIsTheSame(): Boolean {
|
||||
return registrationData.value?.password == registrationData.value?.passwordConfirmation
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface UiEvent {
|
||||
data object NavigateToMainActivity : UiEvent
|
||||
data class ShowToast(val message: String) : UiEvent
|
||||
}
|
|
@ -36,6 +36,7 @@ fun LanguageScreen(
|
|||
containerColor = MaterialTheme.colorScheme.background,
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
// todo
|
||||
VerticalSpace(height = 16.dp)
|
||||
SingleChoiceCheckBoxes(
|
||||
itemNames = languages.map { it.name },
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
package app.tourism.ui.screens.main
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import app.tourism.AuthActivity
|
||||
import app.tourism.ui.screens.auth.Language
|
||||
import app.tourism.ui.screens.language.LanguageScreen
|
||||
import app.tourism.ui.screens.main.categories.categories.CategoriesScreen
|
||||
import app.tourism.ui.screens.main.favorites.favorites.FavoritesScreen
|
||||
import app.tourism.ui.screens.main.home.home.HomeScreen
|
||||
import app.tourism.ui.screens.main.profile.personal_data.PersonalDataScreen
|
||||
import app.tourism.ui.screens.main.profile.profile.ProfileScreen
|
||||
import app.tourism.ui.screens.main.site_details.SiteDetailsScreen
|
||||
import app.tourism.utils.navigateToMap
|
||||
import app.tourism.utils.navigateToMapForRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
// tabs
|
||||
@Serializable
|
||||
object HomeTab
|
||||
|
||||
@Serializable
|
||||
object CategoriesTab
|
||||
|
||||
@Serializable
|
||||
object FavoritesTab
|
||||
|
||||
@Serializable
|
||||
object ProfileTab
|
||||
|
||||
// home
|
||||
@Serializable
|
||||
object Home
|
||||
|
||||
@Serializable
|
||||
data class Search(val query: String)
|
||||
|
||||
// categories
|
||||
@Serializable
|
||||
object Categories
|
||||
|
||||
// favorites
|
||||
@Serializable
|
||||
object Favorites
|
||||
|
||||
// profile
|
||||
@Serializable
|
||||
object Profile
|
||||
|
||||
@Serializable
|
||||
object PersonalData
|
||||
|
||||
// site details
|
||||
@Serializable
|
||||
data class SiteDetails(val id: Int)
|
||||
|
||||
@Composable
|
||||
fun MainNavigation(rootNavController: NavHostController, themeVM: ThemeViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val onSiteClick: (id: Int) -> Unit = { id ->
|
||||
rootNavController.navigate(SiteDetails(id = id))
|
||||
}
|
||||
val onMapClick = { navigateToMap(context) }
|
||||
|
||||
NavHost(rootNavController, startDestination = HomeTab) {
|
||||
composable<HomeTab> {
|
||||
HomeNavHost(onSiteClick, onMapClick)
|
||||
}
|
||||
composable<CategoriesTab> {
|
||||
CategoriesNavHost(onSiteClick, onMapClick)
|
||||
}
|
||||
composable<FavoritesTab> {
|
||||
FavoritesNavHost(onSiteClick)
|
||||
}
|
||||
composable<ProfileTab> {
|
||||
ProfileNavHost(themeVM = themeVM)
|
||||
}
|
||||
composable<SiteDetails> { backStackEntry ->
|
||||
val siteDetails = backStackEntry.toRoute<SiteDetails>()
|
||||
SiteDetailsScreen(
|
||||
id = siteDetails.id,
|
||||
onBackClick = { rootNavController.navigateUp() },
|
||||
onMapClick = onMapClick,
|
||||
onCreateRoute = { siteLocation ->
|
||||
navigateToMapForRoute(context, siteLocation)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeNavHost(onSiteClick: (id: Int) -> Unit, onMapClick: () -> Unit) {
|
||||
val homeNavController = rememberNavController()
|
||||
NavHost(homeNavController, startDestination = Home) {
|
||||
composable<Home> {
|
||||
HomeScreen(
|
||||
onSearchClick = { query ->
|
||||
homeNavController.navigate(Search(query = query))
|
||||
},
|
||||
onSiteClick = onSiteClick,
|
||||
onMapClick = onMapClick
|
||||
)
|
||||
}
|
||||
composable<Search> { backStackEntry ->
|
||||
val search = backStackEntry.toRoute<Search>()
|
||||
Search(query = search.query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoriesNavHost(onSiteClick: (id: Int) -> Unit, onMapClick: () -> Unit) {
|
||||
val categoriesNavController = rememberNavController()
|
||||
NavHost(categoriesNavController, startDestination = Categories) {
|
||||
composable<Categories> {
|
||||
CategoriesScreen(onSiteClick, onMapClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FavoritesNavHost(onSiteClick: (id: Int) -> Unit) {
|
||||
val favoritesNavController = rememberNavController()
|
||||
NavHost(favoritesNavController, startDestination = Favorites) {
|
||||
composable<Favorites> {
|
||||
FavoritesScreen(onSiteClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNavHost(themeVM: ThemeViewModel) {
|
||||
val context = LocalContext.current
|
||||
val profileNavController = rememberNavController()
|
||||
val onBackClick = { profileNavController.navigateUp() }
|
||||
|
||||
NavHost(profileNavController, startDestination = Profile) {
|
||||
composable<Profile> {
|
||||
ProfileScreen(
|
||||
onPersonalDataClick = {
|
||||
profileNavController.navigate(PersonalData)
|
||||
},
|
||||
onLanguageClick = {
|
||||
profileNavController.navigate(Language)
|
||||
},
|
||||
onSignOutComplete = {
|
||||
navigateToAuth(context)
|
||||
},
|
||||
themeVM = themeVM
|
||||
)
|
||||
}
|
||||
composable<PersonalData> {
|
||||
PersonalDataScreen(onBackClick)
|
||||
}
|
||||
composable<Language> {
|
||||
LanguageScreen(onBackClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToAuth(context: Context) {
|
||||
val intent = Intent(context, AuthActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package app.tourism.ui.screens.main
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemColors
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import app.organicmaps.R
|
||||
import app.tourism.ui.common.VerticalSpace
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
|
||||
@Composable
|
||||
fun MainSection(themeVM: ThemeViewModel) {
|
||||
val rootNavController = rememberNavController()
|
||||
val navBackStackEntry by rootNavController.currentBackStackEntryAsState()
|
||||
val items = getNavItems()
|
||||
Scaffold { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
MainNavigation(rootNavController = rootNavController, themeVM = themeVM)
|
||||
|
||||
Column(modifier = Modifier.align(alignment = Alignment.BottomCenter)) {
|
||||
NavigationBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.clip(shape = RoundedCornerShape(50.dp)),
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
windowInsets = WindowInsets(
|
||||
left = 24.dp,
|
||||
right = 24.dp,
|
||||
bottom = 0.dp,
|
||||
top = 0.dp
|
||||
)
|
||||
) {
|
||||
items.forEach { item ->
|
||||
val isSelected = item.route == navBackStackEntry?.destination?.route
|
||||
NavigationBarItem(
|
||||
colors = NavigationBarItemColors(
|
||||
disabledIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
disabledTextColor = MaterialTheme.colorScheme.onPrimary,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onPrimary,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onPrimary,
|
||||
selectedIndicatorColor = Color.Transparent,
|
||||
),
|
||||
selected = isSelected,
|
||||
label = {
|
||||
Text(text = item.title, style = TextStyles.b3)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
if (isSelected) item.selectedIcon else item.unselectedIcon
|
||||
),
|
||||
contentDescription = item.title
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
rootNavController.navigate(item.route) {
|
||||
popUpTo(rootNavController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
VerticalSpace(height = 0.dp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
data class BottomNavigationItem(
|
||||
val route: Any,
|
||||
val title: String,
|
||||
@DrawableRes val unselectedIcon: Int,
|
||||
@DrawableRes val selectedIcon: Int
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun getNavItems(): List<BottomNavigationItem> {
|
||||
return listOf(
|
||||
BottomNavigationItem(
|
||||
route = HomeTab,
|
||||
title = stringResource(id = R.string.home),
|
||||
selectedIcon = R.drawable.home_selected,
|
||||
unselectedIcon = R.drawable.home,
|
||||
),
|
||||
BottomNavigationItem(
|
||||
route = CategoriesTab,
|
||||
title = stringResource(id = R.string.categories),
|
||||
selectedIcon = R.drawable.categories_selected,
|
||||
unselectedIcon = R.drawable.categories,
|
||||
),
|
||||
BottomNavigationItem(
|
||||
route = FavoritesTab,
|
||||
title = stringResource(id = R.string.favorites),
|
||||
selectedIcon = R.drawable.heart_selected,
|
||||
unselectedIcon = R.drawable.heart,
|
||||
),
|
||||
BottomNavigationItem(
|
||||
route = ProfileTab,
|
||||
title = stringResource(id = R.string.profile_tourism),
|
||||
selectedIcon = R.drawable.profile_selected,
|
||||
unselectedIcon = R.drawable.profile,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package app.tourism.ui.screens.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ThemeViewModel @Inject constructor(
|
||||
private val userPreferences: UserPreferences,
|
||||
) : ViewModel() {
|
||||
private val _theme = MutableStateFlow(userPreferences.getTheme())
|
||||
val theme = _theme.asStateFlow()
|
||||
|
||||
fun setTheme(themeCode: String) {
|
||||
_theme.value = userPreferences.themes.first { it.code == themeCode }
|
||||
userPreferences.setTheme(themeCode)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package app.tourism.ui.screens.main.categories.categories
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.organicmaps.R
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
import app.tourism.ui.common.nav.TopBarActionData
|
||||
|
||||
@Composable
|
||||
fun CategoriesScreen(
|
||||
onSiteClick: (id: Int) -> Unit,
|
||||
onMapClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(id = R.string.categories),
|
||||
actions = listOf(
|
||||
TopBarActionData(
|
||||
iconDrawable = R.drawable.map,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
onClick = onMapClick
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
// todo
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package app.tourism.ui.screens.main.favorites.favorites
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.organicmaps.R
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
import app.tourism.ui.common.nav.TopBarActionData
|
||||
|
||||
@Composable
|
||||
fun FavoritesScreen(
|
||||
onSiteClick: (id: Int) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(id = R.string.favorites),
|
||||
actions = listOf(
|
||||
TopBarActionData(
|
||||
iconDrawable = R.drawable.search,
|
||||
onClick = {
|
||||
// todo
|
||||
}
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
// todo
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package app.tourism.ui.screens.main.home.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.organicmaps.R
|
||||
import app.tourism.Constants
|
||||
import app.tourism.ui.common.SpaceForNavBar
|
||||
import app.tourism.ui.common.buttons.PrimaryButton
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
import app.tourism.ui.common.nav.TopBarActionData
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
onSearchClick: (String) -> Unit,
|
||||
onSiteClick: (id: Int) -> Unit,
|
||||
onMapClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
// todo remove hardcoded value
|
||||
title = "Душанбе",
|
||||
actions = listOf(
|
||||
TopBarActionData(
|
||||
iconDrawable = R.drawable.map,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
onClick = onMapClick
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
contentWindowInsets = Constants.USUAL_WINDOW_INSETS
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// todo
|
||||
PrimaryButton(label = "navigate to Site details screen", onClick = { onSiteClick(1) })
|
||||
|
||||
repeat(50) {
|
||||
Text(text = "sldkjfsdlkf")
|
||||
}
|
||||
SpaceForNavBar()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package app.tourism.ui.screens.main.home.search
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import app.organicmaps.R
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
import app.tourism.ui.common.nav.TopBarActionData
|
||||
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
onSiteClick: (id: Int) -> Unit,
|
||||
onMapClick: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
// todo remove hardcoded value
|
||||
title = "Search",
|
||||
actions = listOf(
|
||||
TopBarActionData(
|
||||
iconDrawable = R.drawable.map,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
onClick = onMapClick
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
// todo
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package app.tourism.ui.screens.main.profile.personal_data
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.organicmaps.R
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
|
||||
@Composable
|
||||
fun PersonalDataScreen(onBackClick: () -> Boolean) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(id = R.string.personal_data),
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package app.tourism.ui.screens.main.profile.profile
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import app.organicmaps.R
|
||||
import app.tourism.Constants
|
||||
import app.tourism.applyAppBorder
|
||||
import app.tourism.domain.models.profile.CurrencyRates
|
||||
import app.tourism.domain.models.profile.PersonalData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import app.tourism.ui.ObserveAsEvents
|
||||
import app.tourism.ui.common.HorizontalSpace
|
||||
import app.tourism.ui.common.LoadImg
|
||||
import app.tourism.ui.common.SpaceForNavBar
|
||||
import app.tourism.ui.common.VerticalSpace
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
import app.tourism.ui.common.ui_state.Loading
|
||||
import app.tourism.ui.screens.main.ThemeViewModel
|
||||
import app.tourism.ui.theme.TextStyles
|
||||
import app.tourism.ui.utils.showToast
|
||||
import com.hbb20.CountryCodePicker
|
||||
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
onPersonalDataClick: () -> Unit,
|
||||
onLanguageClick: () -> Unit,
|
||||
onSignOutComplete: () -> Unit,
|
||||
vm: ProfileViewModel = hiltViewModel(),
|
||||
themeVM: ThemeViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val personalData = vm.profileDataResource.collectAsState().value
|
||||
val signOutResponse = vm.signOutResponse.collectAsState().value
|
||||
|
||||
ObserveAsEvents(flow = vm.uiEventsChannelFlow) { event ->
|
||||
when (event) {
|
||||
is UiEvent.NavigateToAuth -> onSignOutComplete()
|
||||
is UiEvent.ShowToast -> context.showToast(event.message)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(id = R.string.profile_tourism),
|
||||
)
|
||||
},
|
||||
contentWindowInsets = Constants.USUAL_WINDOW_INSETS
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
if (personalData is Resource.Success) {
|
||||
personalData.data?.let {
|
||||
ProfileBar(it, onPersonalDataClick)
|
||||
}
|
||||
}
|
||||
VerticalSpace(height = 48.dp)
|
||||
// todo currency rates. Couldn't find free api or library :(
|
||||
CurrencyRates(currencyRates = CurrencyRates(10.88, 10.88, 10.88))
|
||||
VerticalSpace(height = 24.dp)
|
||||
GenericProfileItem(
|
||||
label = stringResource(R.string.personal_data),
|
||||
icon = R.drawable.profile,
|
||||
onClick = onPersonalDataClick
|
||||
)
|
||||
VerticalSpace(height = 24.dp)
|
||||
GenericProfileItem(
|
||||
label = stringResource(R.string.language),
|
||||
icon = R.drawable.globe,
|
||||
onClick = onLanguageClick
|
||||
)
|
||||
VerticalSpace(height = 24.dp)
|
||||
ThemeSwitch(themeVM = themeVM)
|
||||
VerticalSpace(height = 24.dp)
|
||||
GenericProfileItem(
|
||||
label = stringResource(R.string.sign_out),
|
||||
icon = R.drawable.sign_out,
|
||||
isLoading = signOutResponse is Resource.Loading,
|
||||
onClick = { vm.signOut() }
|
||||
)
|
||||
SpaceForNavBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileBar(personalData: PersonalData, onPersonalDataClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onPersonalDataClick() },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LoadImg(url = personalData.pfpUrl)
|
||||
HorizontalSpace(width = 16.dp)
|
||||
Column {
|
||||
Text(text = personalData.fullName, style = TextStyles.h2)
|
||||
VerticalSpace(height = 16.dp)
|
||||
Country(Modifier.fillMaxWidth(), personalData.country)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Country(modifier: Modifier = Modifier, countryCodeName: String) {
|
||||
AndroidView(
|
||||
modifier = Modifier.then(modifier),
|
||||
factory = { context ->
|
||||
val view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.ccp_as_country_label, null, false)
|
||||
val ccp = view.findViewById<CountryCodePicker>(R.id.ccp)
|
||||
ccp.setCountryForNameCode(countryCodeName)
|
||||
ccp.showArrow(false)
|
||||
ccp.setCcpClickable(false)
|
||||
view
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrencyRates(modifier: Modifier = Modifier, currencyRates: CurrencyRates) {
|
||||
// todo
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.applyAppBorder()
|
||||
.padding(horizontal = 15.dp, vertical = 24.dp)
|
||||
.then(modifier),
|
||||
horizontalArrangement = Arrangement.SpaceAround,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
CurrencyRatesItem(
|
||||
currency = stringResource(id = R.string.usd),
|
||||
value = currencyRates.usd.toString(),
|
||||
)
|
||||
CurrencyRatesItem(
|
||||
currency = stringResource(id = R.string.eur),
|
||||
value = currencyRates.eur.toString(),
|
||||
)
|
||||
CurrencyRatesItem(
|
||||
currency = stringResource(id = R.string.rub),
|
||||
value = currencyRates.rub.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrencyRatesItem(currency: String, value: String) {
|
||||
Row {
|
||||
Text(text = currency, style = TextStyles.b1)
|
||||
HorizontalSpace(width = 4.dp)
|
||||
Text(text = value, style = TextStyles.b1.copy(fontWeight = FontWeight.Medium))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GenericProfileItem(
|
||||
modifier: Modifier = Modifier,
|
||||
label: String,
|
||||
@DrawableRes icon: Int,
|
||||
onClick: () -> Unit,
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.applyAppBorder()
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 15.dp, vertical = 20.dp)
|
||||
.then(modifier),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = label, style = TextStyles.h4)
|
||||
if (isLoading)
|
||||
Loading(Modifier.size(22.dp))
|
||||
else
|
||||
Icon(
|
||||
modifier = Modifier.size(22.dp),
|
||||
painter = painterResource(id = icon),
|
||||
tint = colorResource(id = R.color.border),
|
||||
contentDescription = label,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ThemeSwitch(modifier: Modifier = Modifier, themeVM: ThemeViewModel) {
|
||||
val isDark = themeVM.theme.collectAsState().value?.code == "dark"
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.applyAppBorder()
|
||||
.padding(horizontal = 15.dp, vertical = 6.dp)
|
||||
.then(modifier),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.dark_theme), style = TextStyles.h4)
|
||||
Switch(
|
||||
checked = isDark,
|
||||
onCheckedChange = { isDark ->
|
||||
val themeCode = if (isDark) "dark" else "light"
|
||||
themeVM.setTheme(themeCode)
|
||||
},
|
||||
colors = SwitchDefaults.colors(uncheckedTrackColor = MaterialTheme.colorScheme.background)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package app.tourism.ui.screens.main.profile.profile
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.repositories.AuthRepository
|
||||
import app.tourism.data.repositories.ProfileRepository
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.profile.PersonalData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ProfileViewModel @Inject constructor(
|
||||
private val profileRepository: ProfileRepository,
|
||||
private val authRepository: AuthRepository,
|
||||
private val userPreferences: UserPreferences
|
||||
) : ViewModel() {
|
||||
private val uiChannel = Channel<UiEvent>()
|
||||
val uiEventsChannelFlow = uiChannel.receiveAsFlow()
|
||||
|
||||
private val _personalDataResource = MutableStateFlow<Resource<PersonalData>>(Resource.Idle())
|
||||
val profileDataResource = _personalDataResource.asStateFlow()
|
||||
|
||||
fun getPersonalData() {
|
||||
viewModelScope.launch {
|
||||
profileRepository.getPersonalData()
|
||||
.collectLatest { resource ->
|
||||
_personalDataResource.value = resource
|
||||
if (resource is Resource.Error) {
|
||||
uiChannel.send(UiEvent.ShowToast(resource.message ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _signOutResponse = MutableStateFlow<Resource<SimpleResponse>>(Resource.Idle())
|
||||
val signOutResponse = _signOutResponse.asStateFlow()
|
||||
|
||||
fun signOut() {
|
||||
viewModelScope.launch {
|
||||
authRepository.signOut()
|
||||
.collectLatest { resource ->
|
||||
_signOutResponse.value = resource
|
||||
if (resource is Resource.Success) {
|
||||
userPreferences.setToken(null)
|
||||
uiChannel.send(UiEvent.NavigateToAuth)
|
||||
uiChannel.send(UiEvent.ShowToast(resource.data?.message ?: ""))
|
||||
}
|
||||
if (resource is Resource.Error) {
|
||||
uiChannel.send(UiEvent.ShowToast(resource.message ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
getPersonalData()
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface UiEvent {
|
||||
data object NavigateToAuth : UiEvent
|
||||
data class ShowToast(val message: String) : UiEvent
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package app.tourism.ui.screens.main.site_details
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.dto.SiteLocation
|
||||
import app.tourism.ui.common.nav.AppTopBar
|
||||
|
||||
@Composable
|
||||
fun SiteDetailsScreen(
|
||||
id: Int,
|
||||
onBackClick: () -> Boolean,
|
||||
onMapClick: () -> Unit,
|
||||
onCreateRoute: (SiteLocation) -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
AppTopBar(
|
||||
title = stringResource(id = R.string.profile_tourism),
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(Modifier.padding(paddingValues)) {
|
||||
// todo
|
||||
Text("id: $id")
|
||||
}
|
||||
}
|
||||
}
|
34
android/app/src/main/java/app/tourism/ui/utils/showToast.kt
Normal file
34
android/app/src/main/java/app/tourism/ui/utils/showToast.kt
Normal file
|
@ -0,0 +1,34 @@
|
|||
package app.tourism.ui.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.Html
|
||||
import android.text.Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
fun Fragment.showToast(text: String) {
|
||||
getAppToast(requireContext(), text).show()
|
||||
}
|
||||
|
||||
fun Context.showToast(text: String) {
|
||||
getAppToast(this, text).show()
|
||||
}
|
||||
|
||||
private fun getAppToast(context: Context, text: String): Toast {
|
||||
val htmlText = "<small>$text</small>"
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
Html.fromHtml(htmlText, TO_HTML_PARAGRAPH_LINES_CONSECUTIVE),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
Html.fromHtml(htmlText),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
20
android/app/src/main/java/app/tourism/utils/MapUtils.kt
Normal file
20
android/app/src/main/java/app/tourism/utils/MapUtils.kt
Normal file
|
@ -0,0 +1,20 @@
|
|||
package app.tourism.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import app.organicmaps.MwmActivity
|
||||
import app.tourism.data.dto.SiteLocation
|
||||
|
||||
fun navigateToMap(context: Context, clearBackStack: Boolean = false) {
|
||||
val intent = Intent(context, MwmActivity::class.java)
|
||||
if (clearBackStack)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
||||
|
||||
fun navigateToMapForRoute(context: Context, siteLocation: SiteLocation) {
|
||||
val intent = Intent(context, MwmActivity::class.java)
|
||||
intent.putExtra("end_point", siteLocation)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
13
android/app/src/main/res/drawable/add.xml
Normal file
13
android/app/src/main/res/drawable/add.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M9,9V5H11V9H15V11H11V15H9V11H5V9H9Z"
|
||||
android:fillColor="#738DB9"/>
|
||||
<path
|
||||
android:pathData="M0,10C0,4.477 4.477,0 10,0C15.523,0 20,4.477 20,10C20,15.523 15.523,20 10,20C4.477,20 0,15.523 0,10ZM10,2C5.582,2 2,5.582 2,10C2,14.418 5.582,18 10,18C14.418,18 18,14.418 18,10C18,5.582 14.418,2 10,2Z"
|
||||
android:fillColor="#738DB9"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
|
||||
|
||||
</vector>
|
48
android/app/src/main/res/drawable/categories.xml
Normal file
48
android/app/src/main/res/drawable/categories.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M9.5,3.5H4.5C3.948,3.5 3.5,3.948 3.5,4.5V9.5C3.5,10.052 3.948,10.5 4.5,10.5H9.5C10.052,10.5 10.5,10.052 10.5,9.5V4.5C10.5,3.948 10.052,3.5 9.5,3.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9.5,14.5H4.5C3.948,14.5 3.5,14.948 3.5,15.5V20.5C3.5,21.052 3.948,21.5 4.5,21.5H9.5C10.052,21.5 10.5,21.052 10.5,20.5V15.5C10.5,14.948 10.052,14.5 9.5,14.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.5,4.5H21.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.5,9.5H21.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.5,15.5H21.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.5,20.5H21.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
48
android/app/src/main/res/drawable/categories_selected.xml
Normal file
48
android/app/src/main/res/drawable/categories_selected.xml
Normal file
|
@ -0,0 +1,48 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M9.833,3.5H4.833C4.281,3.5 3.833,3.948 3.833,4.5V9.5C3.833,10.052 4.281,10.5 4.833,10.5H9.833C10.386,10.5 10.833,10.052 10.833,9.5V4.5C10.833,3.948 10.386,3.5 9.833,3.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9.833,14.5H4.833C4.281,14.5 3.833,14.948 3.833,15.5V20.5C3.833,21.052 4.281,21.5 4.833,21.5H9.833C10.386,21.5 10.833,21.052 10.833,20.5V15.5C10.833,14.948 10.386,14.5 9.833,14.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.833,4.5H21.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.833,9.5H21.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.833,15.5H21.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14.833,20.5H21.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
13
android/app/src/main/res/drawable/chevron_down.xml
Normal file
13
android/app/src/main/res/drawable/chevron_down.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6,9L12,15L18,9"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
10
android/app/src/main/res/drawable/close.xml
Normal file
10
android/app/src/main/res/drawable/close.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M10,0C4.477,0 0,4.477 0,10C0,15.523 4.477,20 10,20C15.523,20 20,15.523 20,10C20,4.477 15.523,0 10,0ZM12.793,14.207L10,11.414L7.207,14.207L5.793,12.793L8.586,10L5.793,7.207L7.207,5.793L10,8.586L12.793,5.793L14.207,7.207L11.414,10L14.207,12.793L12.793,14.207Z"
|
||||
android:fillColor="#C9D4E7"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
17
android/app/src/main/res/drawable/error_centered.xml
Normal file
17
android/app/src/main/res/drawable/error_centered.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/dark_gray"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<group
|
||||
android:pivotX="12"
|
||||
android:pivotY="12"
|
||||
android:scaleX="0.4"
|
||||
android:scaleY="0.4">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||
</group>
|
||||
</vector>
|
13
android/app/src/main/res/drawable/heart.xml
Normal file
13
android/app/src/main/res/drawable/heart.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M19.5,14.5C20.99,13.04 22.5,11.29 22.5,9C22.5,7.541 21.92,6.142 20.889,5.111C19.858,4.079 18.459,3.5 17,3.5C15.24,3.5 14,4 12.5,5.5C11,4 9.76,3.5 8,3.5C6.541,3.5 5.142,4.079 4.111,5.111C3.079,6.142 2.5,7.541 2.5,9C2.5,11.3 4,13.05 5.5,14.5L12.5,21.5L19.5,14.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
13
android/app/src/main/res/drawable/heart_selected.xml
Normal file
13
android/app/src/main/res/drawable/heart_selected.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M19.167,14.5C20.657,13.04 22.167,11.29 22.167,9C22.167,7.541 21.587,6.142 20.556,5.111C19.524,4.079 18.125,3.5 16.667,3.5C14.907,3.5 13.667,4 12.167,5.5C10.667,4 9.427,3.5 7.667,3.5C6.208,3.5 4.809,4.079 3.778,5.111C2.746,6.142 2.167,7.541 2.167,9C2.167,11.3 3.667,13.05 5.167,14.5L12.167,21.5L19.167,14.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
android/app/src/main/res/drawable/home.xml
Normal file
20
android/app/src/main/res/drawable/home.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M3,9.5L12,2.5L21,9.5V20.5C21,21.03 20.789,21.539 20.414,21.914C20.039,22.289 19.53,22.5 19,22.5H5C4.47,22.5 3.961,22.289 3.586,21.914C3.211,21.539 3,21.03 3,20.5V9.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9,22.5V12.5H15V22.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
13
android/app/src/main/res/drawable/home_selected.xml
Normal file
13
android/app/src/main/res/drawable/home_selected.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="23dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="23">
|
||||
<path
|
||||
android:pathData="M1,8.5L10,1.5L19,8.5V19.5C19,20.03 18.789,20.539 18.414,20.914C18.039,21.289 17.53,21.5 17,21.5H3C2.47,21.5 1.961,21.289 1.586,20.914C1.211,20.539 1,20.03 1,19.5V8.5Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
9
android/app/src/main/res/drawable/image.xml
Normal file
9
android/app/src/main/res/drawable/image.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:pathData="M216,40L40,40A16,16 0,0 0,24 56L24,200a16,16 0,0 0,16 16L216,216a16,16 0,0 0,16 -16L232,56A16,16 0,0 0,216 40ZM216,56L216,158.75l-26.07,-26.06a16,16 0,0 0,-22.63 0l-20,20 -44,-44a16,16 0,0 0,-22.62 0L40,149.37L40,56ZM40,172l52,-52 80,80L40,200ZM216,200L194.63,200l-36,-36 20,-20L216,181.38L216,200ZM144,100a12,12 0,1 1,12 12A12,12 0,0 1,144 100Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
34
android/app/src/main/res/drawable/image_down.xml
Normal file
34
android/app/src/main/res/drawable/image_down.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10.3,21H5C4.47,21 3.961,20.789 3.586,20.414C3.211,20.039 3,19.53 3,19V5C3,4.47 3.211,3.961 3.586,3.586C3.961,3.211 4.47,3 5,3H19C19.53,3 20.039,3.211 20.414,3.586C20.789,3.961 21,4.47 21,5V15L17.9,11.9C17.524,11.531 17.017,11.326 16.49,11.328C15.963,11.331 15.459,11.542 15.086,11.914L6,21"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M14,19L17,22V16.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M17,22L20,19"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9,11C10.105,11 11,10.105 11,9C11,7.895 10.105,7 9,7C7.895,7 7,7.895 7,9C7,10.105 7.895,11 9,11Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
27
android/app/src/main/res/drawable/map.xml
Normal file
27
android/app/src/main/res/drawable/map.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14.106,5.553C14.384,5.692 14.69,5.764 15,5.764C15.31,5.764 15.616,5.692 15.894,5.553L19.553,3.723C19.706,3.647 19.875,3.611 20.045,3.619C20.216,3.626 20.381,3.678 20.527,3.767C20.671,3.857 20.791,3.983 20.874,4.132C20.957,4.281 21,4.448 21,4.619V17.383C21,17.569 20.948,17.751 20.851,17.909C20.753,18.066 20.613,18.194 20.447,18.277L15.894,20.554C15.616,20.693 15.31,20.765 15,20.765C14.69,20.765 14.384,20.693 14.106,20.554L9.894,18.448C9.616,18.309 9.31,18.237 9,18.237C8.69,18.237 8.384,18.309 8.106,18.448L4.447,20.278C4.294,20.354 4.125,20.39 3.954,20.382C3.784,20.375 3.618,20.323 3.473,20.233C3.328,20.143 3.208,20.018 3.126,19.869C3.043,19.72 3,19.552 3,19.381V6.618C3,6.432 3.052,6.25 3.15,6.092C3.247,5.935 3.387,5.807 3.553,5.724L8.106,3.447C8.384,3.308 8.69,3.236 9,3.236C9.31,3.236 9.616,3.308 9.894,3.447L14.106,5.553Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15,5.764V20.764"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M9,3.236V18.236"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
12
android/app/src/main/res/drawable/placeholder.xml
Normal file
12
android/app/src/main/res/drawable/placeholder.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="5906dp"
|
||||
android:height="5906dp"
|
||||
android:viewportWidth="5606"
|
||||
android:viewportHeight="5606">
|
||||
<path
|
||||
android:pathData="M2135.2,2376.3l1427,0.9 -1.1,919.6c-33.5,-46.3 -254.1,-402.2 -273.2,-409.5 -56.3,0.3 -70.3,48.7 -96.4,89.3l-65.1,99.4c-9.8,10.5 -0.2,2.6 -12.9,11.2 -10.2,-24.5 -289.6,-464.7 -294.5,-468.7 -47.3,-38.5 -80.7,37.4 -100.7,70.1l-148.5,236.3c-15.9,24.8 -52,93.8 -74.7,110.6 -25.7,-24.5 -67.8,-104.5 -91.8,-115.2 -29.2,-13 -47.6,9.4 -60.2,27.4 -40.5,57.8 -178.4,266 -211.2,295l3.2,-866.4zM2104.7,2314.6c-54.2,24.4 -39.9,63.3 -39.8,129.6l-0.1,819.4c-0.9,140.4 34.2,114.9 223.6,114.9 145.9,0 1271.6,11.2 1309.6,-9.5 41,-22.2 33.8,-72.8 33.7,-126.8l0.1,-819.6c0.6,-139.3 -37.9,-113.6 -217.6,-113.6 -140.8,0 -1278.3,-8.5 -1309.5,5.5z"
|
||||
android:fillColor="#6E6E6E"/>
|
||||
<path
|
||||
android:pathData="M3188.9,2443.2c-125.5,44.3 -60.8,215.7 54.1,179 45.4,-14.5 76.8,-60.2 61.7,-118.2 -11,-42.2 -66.3,-78.3 -115.8,-60.8z"
|
||||
android:fillColor="#6E6E6E"/>
|
||||
</vector>
|
20
android/app/src/main/res/drawable/profile.xml
Normal file
20
android/app/src/main/res/drawable/profile.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,13C14.761,13 17,10.761 17,8C17,5.239 14.761,3 12,3C9.239,3 7,5.239 7,8C7,10.761 9.239,13 12,13Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M20,21C20,18.878 19.157,16.843 17.657,15.343C16.157,13.843 14.122,13 12,13C9.878,13 7.843,13.843 6.343,15.343C4.843,16.843 4,18.878 4,21"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
android/app/src/main/res/drawable/profile_selected.xml
Normal file
20
android/app/src/main/res/drawable/profile_selected.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,13C14.761,13 17,10.761 17,8C17,5.239 14.761,3 12,3C9.239,3 7,5.239 7,8C7,10.761 9.239,13 12,13Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M20,21C20,18.878 19.157,16.843 17.657,15.343C16.157,13.843 14.122,13 12,13C9.878,13 7.843,13.843 6.343,15.343C4.843,16.843 4,18.878 4,21"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
android/app/src/main/res/drawable/search.xml
Normal file
20
android/app/src/main/res/drawable/search.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M11,19C15.418,19 19,15.418 19,11C19,6.582 15.418,3 11,3C6.582,3 3,6.582 3,11C3,15.418 6.582,19 11,19Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M21,21L16.7,16.7"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
27
android/app/src/main/res/drawable/sign_out.xml
Normal file
27
android/app/src/main/res/drawable/sign_out.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M7.5,17.5H4.167C3.725,17.5 3.301,17.324 2.988,17.012C2.676,16.699 2.5,16.275 2.5,15.833V4.167C2.5,3.725 2.676,3.301 2.988,2.988C3.301,2.676 3.725,2.5 4.167,2.5H7.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M13.333,14.167L17.5,10L13.333,5.833"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M17.5,10H7.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#C9D4E7"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
17
android/app/src/main/res/layout/ccp_as_country_label.xml
Normal file
17
android/app/src/main/res/layout/ccp_as_country_label.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.hbb20.CountryCodePicker xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/ccp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="12dp"
|
||||
app:ccp_showArrow="false"
|
||||
app:ccp_contentColor="@color/onBackground"
|
||||
app:ccp_autoDetectLanguage="true"
|
||||
app:ccp_textGravity="LEFT"
|
||||
app:ccp_padding="0dp"
|
||||
app:ccp_showFullName="true"
|
||||
app:ccp_showNameCode="false"
|
||||
app:ccp_showPhoneCode="false">
|
||||
|
||||
</com.hbb20.CountryCodePicker>
|
20
android/app/src/main/res/layout/ccp_profile.xml
Normal file
20
android/app/src/main/res/layout/ccp_profile.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.hbb20.CountryCodePicker xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/ccp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="12dp"
|
||||
app:ccp_autoDetectLanguage="true"
|
||||
app:ccp_contentColor="@color/onBackground"
|
||||
app:ccpDialog_backgroundColor="@color/transparent"
|
||||
app:ccpDialog_textColor="@color/onBackground"
|
||||
app:ccp_arrowColor="@color/onBackground"
|
||||
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">
|
||||
|
||||
</com.hbb20.CountryCodePicker>
|
4
android/app/src/main/res/values-night/colors.xml
Normal file
4
android/app/src/main/res/values-night/colors.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="onBackground">#FFFFFF</color>
|
||||
</resources>
|
|
@ -2197,7 +2197,6 @@
|
|||
<string name="personal_data">Персональные данные</string>
|
||||
<string name="language">Язык</string>
|
||||
<string name="current_language">Русский</string>
|
||||
<string name="system_theme">Системная тема</string>
|
||||
<string name="dark_theme">Темная тема</string>
|
||||
<string name="light_theme">Светлая тема</string>
|
||||
<string name="sign_out">Выход</string>
|
||||
|
@ -2206,8 +2205,7 @@
|
|||
<string name="update_data">Изменить данные</string>
|
||||
<string name="phone">Номер телефона</string>
|
||||
<string name="chose_language">Выберите язык</string>
|
||||
<string name="russian">Русский</string>
|
||||
<string name="english">English</string>
|
||||
<string name="retry">Попробовать заново</string>
|
||||
<string name="no_network">Не удается соединиться с сервером, проверьте интернет подключение</string>
|
||||
<string name="no_image">Нет изображения</string>
|
||||
</resources>
|
||||
|
|
|
@ -136,4 +136,6 @@
|
|||
<color name="elevation_profile_dark">#4BB9E6</color>
|
||||
|
||||
<color name="material_calendar_surface_dark">#929292</color>
|
||||
<color name="onBackground">#2B2D33</color>
|
||||
<color name="border">#C9D4E7</color>
|
||||
</resources>
|
||||
|
|
|
@ -2248,4 +2248,5 @@
|
|||
<string name="chose_language">Select a language</string>
|
||||
<string name="retry">Try again</string>
|
||||
<string name="no_network">Couldn\'t reach the server, please check connection</string>
|
||||
<string name="no_image">No image</string>
|
||||
</resources>
|
||||
|
|
Loading…
Add table
Reference in a new issue