diff --git a/android/app/build.gradle b/android/app/build.gradle
index 9be57a5693..a406507a85 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -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:
// ```
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 54d95540f2..2e9a7653f4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -69,10 +69,12 @@
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/MwmTheme"
+ android:usesCleartextTraffic="true"
tools:targetApi="33">
()
+ private val profileVM: ProfileViewModel by viewModels()
+
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)
- }
- }
-}
diff --git a/android/app/src/main/java/app/tourism/data/dto/auth/AuthResponseDataDto.kt b/android/app/src/main/java/app/tourism/data/dto/auth/AuthResponseDataDto.kt
new file mode 100644
index 0000000000..025045b6a0
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/data/dto/auth/AuthResponseDataDto.kt
@@ -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
+ )
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/data/dto/profile/User.kt b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt
new file mode 100644
index 0000000000..7fed946a56
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/data/dto/profile/User.kt
@@ -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
+)
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt b/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt
new file mode 100644
index 0000000000..77b02299ff
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/data/remote/NetworkUtils.kt
@@ -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 FlowCollector>.handleCall(
+ call: () -> Response,
+ 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 Response.parseError(): Resource {
+ 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())
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt
new file mode 100644
index 0000000000..0976616e06
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/data/remote/TourismApi.kt
@@ -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
+
+ @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
+
+ @POST("logout")
+ suspend fun signOut(): Response
+ // endregion auth
+
+ // region profile
+ // todo api request not finished yet
+ @GET("user")
+ suspend fun getUser(): Response
+ // endregion profile
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt
new file mode 100644
index 0000000000..9436af71e7
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/data/repositories/AuthRepository.kt
@@ -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> = 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> = flow {
+ handleCall(
+ call = { api.signOut() },
+ mapper = { it }
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt b/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt
new file mode 100644
index 0000000000..4e8bc15572
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/data/repositories/ProfileRepository.kt
@@ -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> = 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",
+ )
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/di/DatabaseModule.kt b/android/app/src/main/java/app/tourism/di/DatabaseModule.kt
new file mode 100644
index 0000000000..f51e467781
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/di/DatabaseModule.kt
@@ -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 {
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/di/NetworkModule.kt b/android/app/src/main/java/app/tourism/di/NetworkModule.kt
new file mode 100644
index 0000000000..41fbaa000d
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/di/NetworkModule.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/di/DataModule.kt b/android/app/src/main/java/app/tourism/di/PreferencesModule.kt
similarity index 95%
rename from android/app/src/main/java/app/tourism/di/DataModule.kt
rename to android/app/src/main/java/app/tourism/di/PreferencesModule.kt
index 6e23cc5cc9..19c86077b1 100644
--- a/android/app/src/main/java/app/tourism/di/DataModule.kt
+++ b/android/app/src/main/java/app/tourism/di/PreferencesModule.kt
@@ -11,7 +11,8 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
-object DataModule {
+object PreferencesModule {
+
@Provides
@Singleton
fun provideUserPreferences(
diff --git a/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt
new file mode 100644
index 0000000000..e4dfca0f8e
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/di/RepositoriesModule.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/domain/models/SimpleResponse.kt b/android/app/src/main/java/app/tourism/domain/models/SimpleResponse.kt
new file mode 100644
index 0000000000..be85ec1df3
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/SimpleResponse.kt
@@ -0,0 +1,3 @@
+package app.tourism.domain.models
+
+data class SimpleResponse(val message: String)
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/domain/models/auth/AuthResponse.kt b/android/app/src/main/java/app/tourism/domain/models/auth/AuthResponse.kt
new file mode 100644
index 0000000000..7007d6c2db
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/auth/AuthResponse.kt
@@ -0,0 +1,3 @@
+package app.tourism.domain.models.auth
+
+data class AuthResponse(val token: String)
diff --git a/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt b/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt
new file mode 100644
index 0000000000..e9213cc993
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/auth/RegistrationData.kt
@@ -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,
+)
diff --git a/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt b/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt
new file mode 100644
index 0000000000..6a49e6aed0
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/profile/CurrencyRates.kt
@@ -0,0 +1,3 @@
+package app.tourism.domain.models.profile
+
+data class CurrencyRates(val usd: Double, val eur: Double, val rub: Double)
diff --git a/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt b/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt
new file mode 100644
index 0000000000..6994858d4f
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/profile/PersonalData.kt
@@ -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
+)
diff --git a/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt b/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt
new file mode 100644
index 0000000000..b78bc9b939
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/resource/Resource.kt
@@ -0,0 +1,8 @@
+package app.tourism.domain.models.resource
+
+sealed class Resource(val data: T? = null, val message: String? = null) {
+ class Loading(data: T? = null): Resource(data)
+ class Idle: Resource()
+ class Success(data: T?, message: String? = null): Resource(data)
+ class Error(message: String, data: T? = null): Resource(data, message)
+}
diff --git a/android/app/src/main/java/app/tourism/domain/models/resource/ResponseResource.kt b/android/app/src/main/java/app/tourism/domain/models/resource/ResponseResource.kt
new file mode 100644
index 0000000000..14ac84f030
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/domain/models/resource/ResponseResource.kt
@@ -0,0 +1,6 @@
+package app.tourism.domain.models.resource
+
+sealed class ResponseResource(val data: T? = null, val message: String? = null) {
+ class Success(data: T?): ResponseResource(data)
+ class Error(message: String, data: T? = null): ResponseResource(data, message)
+}
diff --git a/android/app/src/main/java/app/tourism/ui/ComposeUtils.kt b/android/app/src/main/java/app/tourism/ui/ComposeUtils.kt
new file mode 100644
index 0000000000..befb47c452
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/ComposeUtils.kt
@@ -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 ObserveAsEvents(flow: Flow, onEvent: (T) -> Unit) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ LaunchedEffect(flow, lifecycleOwner.lifecycle) {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ withContext(Dispatchers.Main.immediate) {
+ flow.collect(onEvent)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/common/BlurryContainer.kt b/android/app/src/main/java/app/tourism/ui/common/BlurryContainer.kt
index 5caa2d6560..d212a31dfd 100644
--- a/android/app/src/main/java/app/tourism/ui/common/BlurryContainer.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/BlurryContainer.kt
@@ -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
diff --git a/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt b/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt
new file mode 100644
index 0000000000..5ab77c597d
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/common/LoadImage.kt
@@ -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)
+// )
+}
diff --git a/android/app/src/main/java/app/tourism/ui/common/SpaceForNavBar.kt b/android/app/src/main/java/app/tourism/ui/common/SpaceForNavBar.kt
new file mode 100644
index 0000000000..976abdfa72
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/common/SpaceForNavBar.kt
@@ -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))
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt b/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt
index 5b7c1cc357..b3f9dd6d76 100644
--- a/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/buttons/PrimaryButton.kt
@@ -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
diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt b/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt
index 69a0d0d1dc..7a9953ae50 100644
--- a/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/nav/AppTopBar.kt
@@ -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 = 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,
)
}
diff --git a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt
index 8dc43867f2..44426b5ecd 100644
--- a/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/nav/BackButton.kt
@@ -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
+ )
+
}
diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt
index e5edf1aa08..85e7cf4f94 100644
--- a/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/textfields/AppEditText.kt
@@ -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,
+ 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,
diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/AuthEditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/AuthEditText.kt
index f7fa91c19d..4b3ac4d8a1 100644
--- a/android/app/src/main/java/app/tourism/ui/common/textfields/AuthEditText.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/textfields/AuthEditText.kt
@@ -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,
+ 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,
),
diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt
index 8a017cc890..8b5265667c 100644
--- a/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/textfields/EditText.kt
@@ -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,
+ 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,
diff --git a/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt b/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt
index b9a519eeae..1360521ec9 100644
--- a/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/textfields/PasswordEditText.kt
@@ -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,
+ 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
)
diff --git a/android/app/src/main/java/app/tourism/ui/common/ui_state/Loading.kt b/android/app/src/main/java/app/tourism/ui/common/ui_state/Loading.kt
index 7a2500aa44..8095d2bb77 100644
--- a/android/app/src/main/java/app/tourism/ui/common/ui_state/Loading.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/ui_state/Loading.kt
@@ -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
diff --git a/android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt b/android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt
index 26ec971512..3bdc05de2b 100644
--- a/android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt
+++ b/android/app/src/main/java/app/tourism/ui/common/ui_state/NetworkError.kt
@@ -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
diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt
index 02c25cb0ff..7269f2ff20 100644
--- a/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt
+++ b/android/app/src/main/java/app/tourism/ui/screens/auth/AuthNavigation.kt
@@ -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() {
WelcomeScreen(
onLanguageClicked = { navController.navigate(route = Language) },
@@ -44,22 +56,16 @@ fun AuthNavigation() {
}
composable {
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 {
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)
}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt
index 115fbec469..188c3c9828 100644
--- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt
+++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInScreen.kt
@@ -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() },
+ )
+ }
}
}
}
diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt
new file mode 100644
index 0000000000..c3548bd518
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_in/SignInViewModel.kt
@@ -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()
+ 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.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
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt
index a48708e9ea..b04895d7fd 100644
--- a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt
+++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpScreen.kt
@@ -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(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(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() },
+ )
+ }
}
}
}
diff --git a/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt
new file mode 100644
index 0000000000..f7e98209e7
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/auth/sign_up/SignUpViewModel.kt
@@ -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()
+ val uiEventsChannelFlow = uiChannel.receiveAsFlow()
+
+ private val _registrationData =
+ MutableStateFlow(
+ 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.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
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt
index dfb6223713..63ceca9899 100644
--- a/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt
+++ b/android/app/src/main/java/app/tourism/ui/screens/language/LanguageScreen.kt
@@ -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 },
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt
new file mode 100644
index 0000000000..5854dc0239
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/MainNavigation.kt
@@ -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 {
+ HomeNavHost(onSiteClick, onMapClick)
+ }
+ composable {
+ CategoriesNavHost(onSiteClick, onMapClick)
+ }
+ composable {
+ FavoritesNavHost(onSiteClick)
+ }
+ composable {
+ ProfileNavHost(themeVM = themeVM)
+ }
+ composable { backStackEntry ->
+ val siteDetails = backStackEntry.toRoute()
+ 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 {
+ HomeScreen(
+ onSearchClick = { query ->
+ homeNavController.navigate(Search(query = query))
+ },
+ onSiteClick = onSiteClick,
+ onMapClick = onMapClick
+ )
+ }
+ composable { backStackEntry ->
+ val search = backStackEntry.toRoute()
+ Search(query = search.query)
+ }
+ }
+}
+
+@Composable
+fun CategoriesNavHost(onSiteClick: (id: Int) -> Unit, onMapClick: () -> Unit) {
+ val categoriesNavController = rememberNavController()
+ NavHost(categoriesNavController, startDestination = Categories) {
+ composable {
+ CategoriesScreen(onSiteClick, onMapClick)
+ }
+ }
+}
+
+@Composable
+fun FavoritesNavHost(onSiteClick: (id: Int) -> Unit) {
+ val favoritesNavController = rememberNavController()
+ NavHost(favoritesNavController, startDestination = Favorites) {
+ composable {
+ FavoritesScreen(onSiteClick)
+ }
+ }
+}
+
+@Composable
+fun ProfileNavHost(themeVM: ThemeViewModel) {
+ val context = LocalContext.current
+ val profileNavController = rememberNavController()
+ val onBackClick = { profileNavController.navigateUp() }
+
+ NavHost(profileNavController, startDestination = Profile) {
+ composable {
+ ProfileScreen(
+ onPersonalDataClick = {
+ profileNavController.navigate(PersonalData)
+ },
+ onLanguageClick = {
+ profileNavController.navigate(Language)
+ },
+ onSignOutComplete = {
+ navigateToAuth(context)
+ },
+ themeVM = themeVM
+ )
+ }
+ composable {
+ PersonalDataScreen(onBackClick)
+ }
+ composable {
+ 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)
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt b/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt
new file mode 100644
index 0000000000..b917170fd9
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/MainSection.kt
@@ -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 {
+ 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,
+ ),
+ )
+}
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt
new file mode 100644
index 0000000000..ce32166bfe
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/ThemeViewModel.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt
new file mode 100644
index 0000000000..14f3aeed65
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/categories/categories/CategoriesScreen.kt
@@ -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
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt
new file mode 100644
index 0000000000..3edf3e2337
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/favorites/favorites/FavoritesScreen.kt
@@ -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
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt
new file mode 100644
index 0000000000..8c0d5b8e1f
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/home/HomeScreen.kt
@@ -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()
+ }
+ }
+}
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt
new file mode 100644
index 0000000000..0b63ab7670
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/home/search/SearchScreen.kt
@@ -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
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt
new file mode 100644
index 0000000000..89ff1ef689
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/personal_data/PersonalDataScreen.kt
@@ -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
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt
new file mode 100644
index 0000000000..ee6a1943f3
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileScreen.kt
@@ -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(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)
+ )
+ }
+}
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt
new file mode 100644
index 0000000000..92e9f9840a
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/profile/profile/ProfileViewModel.kt
@@ -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()
+ val uiEventsChannelFlow = uiChannel.receiveAsFlow()
+
+ private val _personalDataResource = MutableStateFlow>(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.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
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/screens/main/site_details/ProfileScreen.kt b/android/app/src/main/java/app/tourism/ui/screens/main/site_details/ProfileScreen.kt
new file mode 100644
index 0000000000..6acc37dc5f
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/screens/main/site_details/ProfileScreen.kt
@@ -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")
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/ui/utils/showToast.kt b/android/app/src/main/java/app/tourism/ui/utils/showToast.kt
new file mode 100644
index 0000000000..f647c0a4ca
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/ui/utils/showToast.kt
@@ -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 = "$text"
+
+ 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
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/app/tourism/utils/MapUtils.kt b/android/app/src/main/java/app/tourism/utils/MapUtils.kt
new file mode 100644
index 0000000000..c302163ff2
--- /dev/null
+++ b/android/app/src/main/java/app/tourism/utils/MapUtils.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/android/app/src/main/res/drawable/add.xml b/android/app/src/main/res/drawable/add.xml
new file mode 100644
index 0000000000..b2e81771a6
--- /dev/null
+++ b/android/app/src/main/res/drawable/add.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/baseline_visibility_off_24.xml b/android/app/src/main/res/drawable/baseline_visibility_off_24.xml
new file mode 100644
index 0000000000..5993ca393d
--- /dev/null
+++ b/android/app/src/main/res/drawable/baseline_visibility_off_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/categories.xml b/android/app/src/main/res/drawable/categories.xml
new file mode 100644
index 0000000000..3087b14779
--- /dev/null
+++ b/android/app/src/main/res/drawable/categories.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/categories_selected.xml b/android/app/src/main/res/drawable/categories_selected.xml
new file mode 100644
index 0000000000..db29817e1b
--- /dev/null
+++ b/android/app/src/main/res/drawable/categories_selected.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/chevron_down.xml b/android/app/src/main/res/drawable/chevron_down.xml
new file mode 100644
index 0000000000..043c41fadf
--- /dev/null
+++ b/android/app/src/main/res/drawable/chevron_down.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/close.xml b/android/app/src/main/res/drawable/close.xml
new file mode 100644
index 0000000000..7c27776a72
--- /dev/null
+++ b/android/app/src/main/res/drawable/close.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/error_centered.xml b/android/app/src/main/res/drawable/error_centered.xml
new file mode 100644
index 0000000000..b1f2e893ec
--- /dev/null
+++ b/android/app/src/main/res/drawable/error_centered.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/heart.xml b/android/app/src/main/res/drawable/heart.xml
new file mode 100644
index 0000000000..a02a6f43c6
--- /dev/null
+++ b/android/app/src/main/res/drawable/heart.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/heart_selected.xml b/android/app/src/main/res/drawable/heart_selected.xml
new file mode 100644
index 0000000000..66c82b992d
--- /dev/null
+++ b/android/app/src/main/res/drawable/heart_selected.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/home.xml b/android/app/src/main/res/drawable/home.xml
new file mode 100644
index 0000000000..d66fbb4956
--- /dev/null
+++ b/android/app/src/main/res/drawable/home.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/home_selected.xml b/android/app/src/main/res/drawable/home_selected.xml
new file mode 100644
index 0000000000..e1409dffb6
--- /dev/null
+++ b/android/app/src/main/res/drawable/home_selected.xml
@@ -0,0 +1,13 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/image.xml b/android/app/src/main/res/drawable/image.xml
new file mode 100644
index 0000000000..0a863e3c50
--- /dev/null
+++ b/android/app/src/main/res/drawable/image.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/image_down.xml b/android/app/src/main/res/drawable/image_down.xml
new file mode 100644
index 0000000000..52b1127aeb
--- /dev/null
+++ b/android/app/src/main/res/drawable/image_down.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/map.xml b/android/app/src/main/res/drawable/map.xml
new file mode 100644
index 0000000000..25c2aec579
--- /dev/null
+++ b/android/app/src/main/res/drawable/map.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/placeholder.xml b/android/app/src/main/res/drawable/placeholder.xml
new file mode 100644
index 0000000000..e117107b5e
--- /dev/null
+++ b/android/app/src/main/res/drawable/placeholder.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/profile.xml b/android/app/src/main/res/drawable/profile.xml
new file mode 100644
index 0000000000..9fed2c1159
--- /dev/null
+++ b/android/app/src/main/res/drawable/profile.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/profile_selected.xml b/android/app/src/main/res/drawable/profile_selected.xml
new file mode 100644
index 0000000000..29d8a744b4
--- /dev/null
+++ b/android/app/src/main/res/drawable/profile_selected.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/search.xml b/android/app/src/main/res/drawable/search.xml
new file mode 100644
index 0000000000..e7988d437b
--- /dev/null
+++ b/android/app/src/main/res/drawable/search.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/android/app/src/main/res/drawable/sign_out.xml b/android/app/src/main/res/drawable/sign_out.xml
new file mode 100644
index 0000000000..33148293b4
--- /dev/null
+++ b/android/app/src/main/res/drawable/sign_out.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/ccp_as_country_label.xml b/android/app/src/main/res/layout/ccp_as_country_label.xml
new file mode 100644
index 0000000000..a5158697e2
--- /dev/null
+++ b/android/app/src/main/res/layout/ccp_as_country_label.xml
@@ -0,0 +1,17 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/layout/country_code_picker.xml b/android/app/src/main/res/layout/ccp_auth.xml
similarity index 100%
rename from android/app/src/main/res/layout/country_code_picker.xml
rename to android/app/src/main/res/layout/ccp_auth.xml
diff --git a/android/app/src/main/res/layout/ccp_profile.xml b/android/app/src/main/res/layout/ccp_profile.xml
new file mode 100644
index 0000000000..4adbe2e667
--- /dev/null
+++ b/android/app/src/main/res/layout/ccp_profile.xml
@@ -0,0 +1,20 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000000..3ad7d62ec5
--- /dev/null
+++ b/android/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index 3cc97870e8..528c58eea1 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -2197,7 +2197,6 @@
Персональные данные
Язык
Русский
- Системная тема
Темная тема
Светлая тема
Выход
@@ -2206,8 +2205,7 @@
Изменить данные
Номер телефона
Выберите язык
- Русский
- English
Попробовать заново
Не удается соединиться с сервером, проверьте интернет подключение
+ Нет изображения
diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml
index 0c9578f5aa..d76f485ca9 100644
--- a/android/app/src/main/res/values/colors.xml
+++ b/android/app/src/main/res/values/colors.xml
@@ -136,4 +136,6 @@
#4BB9E6
#929292
+ #2B2D33
+ #C9D4E7
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index fee2a54f9c..7b7c4e24c5 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -2248,4 +2248,5 @@
Select a language
Try again
Couldn\'t reach the server, please check connection
+ No image