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