From 5beb542341645428b09ccb407aedd8e9a726ac97 Mon Sep 17 00:00:00 2001 From: EgorVorobev Date: Tue, 18 Feb 2025 18:44:09 +0300 Subject: [PATCH] aa --- .gitignore | 10 + .gitmodules | 6 + app/.gitignore | 1 + app/build.gradle.kts | 78 ++++++ app/proguard-rules.pro | 21 ++ app/src/main/AndroidManifest.xml | 33 +++ app/src/main/java/ru/myitschool/work/App.kt | 7 + .../java/ru/myitschool/work/SessionManager.kt | 5 + .../java/ru/myitschool/work/api/ApiModule.kt | 31 +++ .../java/ru/myitschool/work/api/ApiService.kt | 20 ++ .../java/ru/myitschool/work/core/Constants.kt | 5 + .../myitschool/work/ui/Main/MainFragment.kt | 168 +++++++++++++ .../myitschool/work/ui/Main/MainViewModel.kt | 35 +++ .../ru/myitschool/work/ui/RootActivity.kt | 47 ++++ .../work/ui/login/LoginDestination.kt | 6 + .../myitschool/work/ui/login/LoginFragment.kt | 114 +++++++++ .../work/ui/login/LoginViewModel.kt | 54 ++++ .../myitschool/work/ui/qr/result/QrResult.kt | 69 ++++++ .../work/ui/qr/scan/QrScanDestination.kt | 30 +++ .../work/ui/qr/scan/QrScanFragment.kt | 139 +++++++++++ .../work/ui/qr/scan/QrScanViewModel.kt | 93 +++++++ .../myitschool/work/utils/FlowExtensions.kt | 10 + .../work/utils/FragmentExtesions.kt | 18 ++ .../work/utils/TextChangedListener.kt | 12 + .../myitschool/work/utils/ViewExtensions.kt | 7 + app/src/main/java/utils/AuthPreferences.kt | 33 +++ app/src/main/res/drawable/ic_close.xml | 5 + .../res/drawable/ic_launcher_background.xml | 170 +++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 30 +++ app/src/main/res/drawable/ic_logout.xml | 5 + app/src/main/res/drawable/ic_no_img.xml | 5 + app/src/main/res/drawable/ic_qr_code.xml | 25 ++ app/src/main/res/drawable/ic_refresh.xml | 5 + app/src/main/res/layout/activity_root.xml | 21 ++ app/src/main/res/layout/fragment_login.xml | 35 +++ app/src/main/res/layout/fragment_main.xml | 72 ++++++ app/src/main/res/layout/fragment_qr_scan.xml | 35 +++ .../res/layout/fragment_qr_scan_result.xml | 27 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/navigation/nav_graph.xml | 34 +++ app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/ids.xml | 4 + app/src/main/res/values/strings.xml | 35 +++ app/src/main/res/values/strings_qr.xml | 4 + app/src/main/res/values/themes.xml | 16 ++ app/src/main/res/xml/backup_rules.xml | 13 + .../main/res/xml/data_extraction_rules.xml | 19 ++ .../main/res/xml/network_security_config.xml | 4 + build.gradle.kts | 8 + buildSrc/.gitignore | 2 + buildSrc/build.gradle.kts | 8 + buildSrc/src/main/java/Dependencies.kt | 220 ++++++++++++++++ .../main/java/DependencyHandlerExtensions.kt | 40 +++ buildSrc/src/main/java/Plugin.kt | 67 +++++ buildSrc/src/main/java/Version.kt | 42 ++++ gradle.properties | 21 ++ gradle/README.md | 11 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 234 ++++++++++++++++++ gradlew.bat | 89 +++++++ settings.gradle.kts | 17 ++ 73 files changed, 2403 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 app/.gitignore create mode 100644 app/build.gradle.kts create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/ru/myitschool/work/App.kt create mode 100644 app/src/main/java/ru/myitschool/work/SessionManager.kt create mode 100644 app/src/main/java/ru/myitschool/work/api/ApiModule.kt create mode 100644 app/src/main/java/ru/myitschool/work/api/ApiService.kt create mode 100644 app/src/main/java/ru/myitschool/work/core/Constants.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/Main/MainFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/Main/MainViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/RootActivity.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/result/QrResult.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt create mode 100644 app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt create mode 100644 app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt create mode 100644 app/src/main/java/utils/AuthPreferences.kt create mode 100644 app/src/main/res/drawable/ic_close.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_logout.xml create mode 100644 app/src/main/res/drawable/ic_no_img.xml create mode 100644 app/src/main/res/drawable/ic_qr_code.xml create mode 100644 app/src/main/res/drawable/ic_refresh.xml create mode 100644 app/src/main/res/layout/activity_root.xml create mode 100644 app/src/main/res/layout/fragment_login.xml create mode 100644 app/src/main/res/layout/fragment_main.xml create mode 100644 app/src/main/res/layout/fragment_qr_scan.xml create mode 100644 app/src/main/res/layout/fragment_qr_scan_result.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/strings_qr.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 build.gradle.kts create mode 100644 buildSrc/.gitignore create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/java/Dependencies.kt create mode 100644 buildSrc/src/main/java/DependencyHandlerExtensions.kt create mode 100644 buildSrc/src/main/java/Plugin.kt create mode 100644 buildSrc/src/main/java/Version.kt create mode 100644 gradle.properties create mode 100644 gradle/README.md create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b3f5536 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "gradle"] + path = gradle + url = https://git.sicampus.ru/core/gradle.git +[submodule "buildSrc"] + path = buildSrc + url = https://git.sicampus.ru/core/dependecies.git diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..049c0f4 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + kotlinAndroid + androidApplication + jetbrainsKotlinSerialization version Version.Kotlin.language + kotlinAnnotationProcessor + id("com.google.dagger.hilt.android").version("2.51.1") +} + +val packageName = "ru.myitschool.work" + +android { + namespace = packageName + compileSdk = Version.Android.Sdk.compile + + defaultConfig { + applicationId = packageName + minSdk = Version.Android.Sdk.min + targetSdk = Version.Android.Sdk.target + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures.viewBinding = true + + + + compileOptions { + sourceCompatibility = Version.Kotlin.javaSource + targetCompatibility = Version.Kotlin.javaSource + } + + kotlinOptions { + jvmTarget = Version.Kotlin.jvmTarget + } +} + +dependencies { + implementation ("com.squareup.retrofit2:retrofit:2.9.0") + implementation ("com.squareup.retrofit2:converter-gson:2.9.0") + implementation ("com.squareup.okhttp3:okhttp:4.9.0") + implementation ("com.github.bumptech.glide:glide:4.15.1") + kapt ("com.github.bumptech.glide:compiler:4.15.1") + + defaultLibrary() + + implementation(Dependencies.AndroidX.activity) + implementation(Dependencies.AndroidX.fragment) + implementation(Dependencies.AndroidX.constraintLayout) + + implementation(Dependencies.AndroidX.Navigation.fragment) + implementation(Dependencies.AndroidX.Navigation.navigationUi) + + implementation(Dependencies.Retrofit.library) + implementation(Dependencies.Retrofit.gsonConverter) + + + implementation("com.squareup.picasso:picasso:2.8") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("androidx.datastore:datastore-preferences:1.1.1") + implementation("com.google.mlkit:barcode-scanning:17.3.0") + + val cameraX = "1.3.4" + implementation("androidx.camera:camera-core:$cameraX") + implementation("androidx.camera:camera-camera2:$cameraX") + implementation("androidx.camera:camera-lifecycle:$cameraX") + implementation("androidx.camera:camera-view:$cameraX") + implementation("androidx.camera:camera-mlkit-vision:1.4.0-rc04") + + val hilt = "2.51.1" + implementation("com.google.dagger:hilt-android:$hilt") + kapt("com.google.dagger:hilt-android-compiler:$hilt") +} + +kapt { + correctErrorTypes = true +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a986978 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/App.kt b/app/src/main/java/ru/myitschool/work/App.kt new file mode 100644 index 0000000..3085135 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/App.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class App : Application() \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/SessionManager.kt b/app/src/main/java/ru/myitschool/work/SessionManager.kt new file mode 100644 index 0000000..699b478 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/SessionManager.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work + +object SessionManager { + var userLogin: String = "" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/ApiModule.kt b/app/src/main/java/ru/myitschool/work/api/ApiModule.kt new file mode 100644 index 0000000..a13ebd5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/ApiModule.kt @@ -0,0 +1,31 @@ +package ru.myitschool.work.api + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.myitschool.work.api.ApiService +import ru.myitschool.work.core.Constants +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object ApiModule { + + @Provides + @Singleton + fun provideRetrofit(): Retrofit { + return Retrofit.Builder() + .baseUrl(Constants.SERVER_ADDRESS) + .addConverterFactory(GsonConverterFactory.create()) + .build() + } + + @Provides + @Singleton + fun provideApiService(retrofit: Retrofit): ApiService { + return retrofit.create(ApiService::class.java) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/api/ApiService.kt b/app/src/main/java/ru/myitschool/work/api/ApiService.kt new file mode 100644 index 0000000..c199218 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/api/ApiService.kt @@ -0,0 +1,20 @@ +package ru.myitschool.work.api + +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.PATCH +import retrofit2.http.Path + +interface ApiService { + @GET("api/{login}/auth") + suspend fun authenticate(@Path("login") login: String): Response + + @GET("api/{login}/info") + suspend fun getUserInfo(@Path("login") login: String): Response> // Возвращаем Map вместо UserInfo + + @PATCH("api/{login}/open") + suspend fun openDoor(@Path("login") login: String, @Body body: OpenDoorRequest): Response +} + +data class OpenDoorRequest(val value: String) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/core/Constants.kt b/app/src/main/java/ru/myitschool/work/core/Constants.kt new file mode 100644 index 0000000..f24b5bb --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/core/Constants.kt @@ -0,0 +1,5 @@ +package ru.myitschool.work.core +// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ +object Constants { + const val SERVER_ADDRESS = "const val SERVER_ADDRESS = \"http://localhost:8090\"\n" +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/Main/MainFragment.kt b/app/src/main/java/ru/myitschool/work/ui/Main/MainFragment.kt new file mode 100644 index 0000000..6435e05 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/Main/MainFragment.kt @@ -0,0 +1,168 @@ +package ru.myitschool.work.ui.main + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.myitschool.work.R +import ru.myitschool.work.SessionManager +import ru.myitschool.work.api.ApiService +import ru.myitschool.work.core.Constants +import ru.myitschool.work.databinding.FragmentMainBinding +import ru.myitschool.work.ui.qr.scan.QrScanDestination +import java.net.HttpURLConnection +import java.net.URL + +class MainFragment : Fragment(R.layout.fragment_main) { + private var _binding: FragmentMainBinding? = null + private val binding get() = _binding + + private val apiService: ApiService by lazy { + Retrofit.Builder() + .baseUrl(Constants.SERVER_ADDRESS) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ApiService::class.java) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentMainBinding.bind(view) // Подключаем binding + + setupUI() + fetchUserData() + + // Проверяем, есть ли результат QR + checkQrResult() + } + + private fun checkQrResult() { + // Слушаем результат QR сканирования + setFragmentResultListener(QrScanDestination.REQUEST_KEY) { _, bundle -> + val qrData = QrScanDestination.getDataIfExist(bundle) + if (qrData != null) { + // Если данные QR есть, переходим на экран с результатом + findNavController().navigate(R.id.qrResultFragment) + } + } + } + + private fun setupUI() { + // Проверяем, что binding не null, прежде чем устанавливать слушателей + binding?.apply { + refresh.setOnClickListener { fetchUserData() } + logout.setOnClickListener { logout() } + scan.setOnClickListener { navigateToQrScan() } + } + } + + private fun fetchUserData() { + lifecycleScope.launch { + showError(null) // Скрыть ошибку, если она была + try { + val response = apiService.getUserInfo(SessionManager.userLogin) // Получаем данные пользователя + if (response.isSuccessful) { + response.body()?.let { data -> + // Извлекаем значения из Map + val fullName = data["name"] as? String ?: "Неизвестно" + val position = data["position"] as? String ?: "Неизвестно" + val lastVisit = data["lastVisit"] as? String ?: "Неизвестно" + val photoUrl = data["photo"] as? String ?: "" + + // Обновляем UI + updateUI(fullName, position, lastVisit, photoUrl) + } + } else { + showError(getString(R.string.error_loading_data)) // Показываем ошибку, если данные не загрузились + } + } catch (e: Exception) { + showError(e.localizedMessage) // Показываем ошибку при исключении + } + } + } + + private fun updateUI(fullName: String, position1: String, lastVisit: String, photoUrl: String) { + // Проверяем, что binding не null, прежде чем обновлять UI + binding?.apply { + fullname.text = fullName + position.text = position1 + lastEntry.text = lastVisit + if (photoUrl.isNotEmpty()) { + // Загружаем изображение + lifecycleScope.launch { + val bitmap = loadImageFromUrl(photoUrl) + bitmap?.let { photo.setImageBitmap(it) } + } + } + + // Показываем элементы + fullname.visibility = View.VISIBLE + position.visibility = View.VISIBLE + lastEntry.visibility = View.VISIBLE + photo.visibility = View.VISIBLE + logout.visibility = View.VISIBLE + scan.visibility = View.VISIBLE + } + } + + private suspend fun loadImageFromUrl(urlString: String): Bitmap? { + return withContext(Dispatchers.IO) { + try { + val url = URL(urlString) + val connection = url.openConnection() as HttpURLConnection + connection.doInput = true + connection.connect() + val inputStream = connection.inputStream + BitmapFactory.decodeStream(inputStream) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + + private fun showError(message: String?) { + // Проверяем, что binding не null, прежде чем обновлять ошибку + binding?.apply { + if (message != null) { + error.text = message + error.visibility = View.VISIBLE + + // Скрываем остальные элементы, когда возникает ошибка + fullname.visibility = View.GONE + position.visibility = View.GONE + lastEntry.visibility = View.GONE + photo.visibility = View.GONE + logout.visibility = View.GONE + scan.visibility = View.GONE + } else { + error.visibility = View.GONE + } + } + } + + private fun logout() { + // Очистите данные пользователя + Toast.makeText(requireContext(), getString(R.string.logged_out), Toast.LENGTH_SHORT).show() + findNavController().navigate(R.id.loginFragment) // Переход на экран входа + } + + private fun navigateToQrScan() { + findNavController().navigate(R.id.qrScanFragment) // Переход на экран сканирования QR + } + + override fun onDestroyView() { + _binding = null // Освобождаем binding, когда представление уничтожается + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/Main/MainViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/Main/MainViewModel.kt new file mode 100644 index 0000000..41edf8c --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/Main/MainViewModel.kt @@ -0,0 +1,35 @@ +package ru.myitschool.work.ui.main + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import ru.myitschool.work.SessionManager +import ru.myitschool.work.api.ApiService +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor( + private val apiService: ApiService +) : ViewModel() { + private val _userInfoState = MutableStateFlow?>(null) + val userInfoState: StateFlow?> = _userInfoState + + init { + loadUserData() + } + + private fun loadUserData() { + viewModelScope.launch { + val login = SessionManager.userLogin + if (login != null) { + val response = apiService.getUserInfo(login) + if (response.isSuccessful) { + _userInfoState.value = response.body() + } + } + } + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt new file mode 100644 index 0000000..e16a644 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/RootActivity.kt @@ -0,0 +1,47 @@ +package ru.myitschool.work.ui + +import android.os.Bundle +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.fragment.NavHostFragment +import dagger.hilt.android.AndroidEntryPoint +import ru.myitschool.work.R + +// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! +@AndroidEntryPoint +class RootActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_root) + + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? + + if (navHostFragment != null) { + val navController = navHostFragment.navController // Получаем NavController из NavHostFragment + navController.setGraph(R.navigation.nav_graph) // Устанавливаем граф навигации + } + + // Настраиваем кнопку "Назад" + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + onSupportNavigateUp() + } + } + ) + } + + override fun onSupportNavigateUp(): Boolean { + val navHostFragment = supportFragmentManager + .findFragmentById(R.id.nav_host_fragment) as NavHostFragment? + val navController = navHostFragment?.navController // Получаем NavController из NavHostFragment + val popBackResult = if (navController?.previousBackStackEntry != null) { + navController.popBackStack() + } else { + false + } + return popBackResult || super.onSupportNavigateUp() + } +} diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt new file mode 100644 index 0000000..50acfb0 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginDestination.kt @@ -0,0 +1,6 @@ +package ru.myitschool.work.ui.login + +import kotlinx.serialization.Serializable + +@Serializable +data object LoginDestination \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt new file mode 100644 index 0000000..b5478a5 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginFragment.kt @@ -0,0 +1,114 @@ +package ru.myitschool.work.ui.login + +import android.os.Bundle +import android.text.Editable +import android.text.InputType +import android.text.TextWatcher +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentLoginBinding +import ru.myitschool.work.utils.collectWhenStarted +import ru.myitschool.work.utils.visibleOrGone +import ru.myitschool.work.utils.AuthPreferences +import kotlinx.coroutines.launch +import androidx.lifecycle.lifecycleScope + +@AndroidEntryPoint +class LoginFragment : Fragment(R.layout.fragment_login) { + private var _binding: FragmentLoginBinding? = null + private val binding get() = _binding!! + private val viewModel: LoginViewModel by viewModels() + private lateinit var authPreferences: AuthPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + authPreferences = AuthPreferences(requireContext()) + + // Проверка, авторизован ли пользователь + if (authPreferences.isLoggedIn()) { + navigateToMainScreen() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentLoginBinding.bind(view) + + setupUI() + subscribe() + setupLoginTextWatcher() + } + + private fun setupUI() { + binding.username.apply { + inputType = InputType.TYPE_CLASS_TEXT + } + + binding.login.setOnClickListener { + val username = binding.username.text.toString() + performLogin(username) // Вызываем метод performLogin + } + + binding.loading.visibleOrGone(false) + binding.error.visibleOrGone(false) + } + + private fun setupLoginTextWatcher() { + binding.username.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + val username = s.toString() + binding.login.isEnabled = username.isNotEmpty() // Кнопка активна, если поле не пустое + } + + override fun afterTextChanged(s: Editable?) {} + }) + } + + private fun performLogin(username: String) { + lifecycleScope.launch { + viewModel.authenticate(username) // Вызываем метод authenticate из ViewModel + } + } + + private fun subscribe() { + viewModel.state.collectWhenStarted(this) { state -> + binding.loading.visibleOrGone(false) + + if (state.error != null) { + binding.error.text = state.error + binding.error.visibility = View.VISIBLE + } else if (state.success) { + binding.error.visibility = View.GONE + authPreferences.saveLoginState(true) + authPreferences.saveLogin(binding.username.text.toString()) // Сохраняем логин + Toast.makeText(context, "Авторизация прошла успешно", Toast.LENGTH_SHORT).show() + navigateToMainScreen() + } + } + } + + private fun navigateToMainScreen() { + try { + findNavController().apply { + popBackStack(R.id.mainFragment, false) + navigate(R.id.mainFragment) + } + } catch (e: Exception) { + Log.e("LF", "Nav_err", e) + Toast.makeText(context, "Ошибка перехода", Toast.LENGTH_SHORT).show() + } + } + + override fun onDestroyView() { + _binding = null + super.onDestroyView() + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt new file mode 100644 index 0000000..faa5fd7 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/login/LoginViewModel.kt @@ -0,0 +1,54 @@ +package ru.myitschool.work.ui.login + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.myitschool.work.SessionManager +import ru.myitschool.work.api.ApiService +import ru.myitschool.work.core.Constants +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + @ApplicationContext private val context: Context, +) : ViewModel() { + private val _state = MutableStateFlow(LoginState()) + val state = _state.asStateFlow() + + private val apiService: ApiService by lazy { + Retrofit.Builder() + .baseUrl(Constants.SERVER_ADDRESS) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ApiService::class.java) + } + + fun authenticate(username: String) { + if (isValidUsername(username)) { + viewModelScope.launch { + val response = apiService.authenticate(username) + if (response.isSuccessful) { + SessionManager.userLogin = username + _state.value = LoginState(success = true) + } else { + _state.value = LoginState(error = "Ошибка авторизации") + } + } + } else { + _state.value = LoginState(error = "Неправильный логин") + } + } + + private fun isValidUsername(username: String): Boolean { + return username.length >= 3 && !username.first().isDigit() && username.all { it.isLetterOrDigit() } + } + + data class LoginState(val success: Boolean = false, val error: String? = null) +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResult.kt b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResult.kt new file mode 100644 index 0000000..f607c58 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/result/QrResult.kt @@ -0,0 +1,69 @@ +package ru.myitschool.work.ui.qr.result + +import ru.myitschool.work.ui.qr.scan.QrScanDestination +import androidx.navigation.fragment.findNavController +import ru.myitschool.work.SessionManager +import ru.myitschool.work.api.OpenDoorRequest +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import ru.myitschool.work.R +import ru.myitschool.work.api.ApiService +import ru.myitschool.work.core.Constants +import ru.myitschool.work.databinding.FragmentQrScanResultBinding + +class QrResult : Fragment(R.layout.fragment_qr_scan_result) { + private lateinit var binding: FragmentQrScanResultBinding + private lateinit var apiService: ApiService + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentQrScanResultBinding.inflate(inflater, container, false) + apiService = Retrofit.Builder() + .baseUrl(Constants.SERVER_ADDRESS) + .addConverterFactory(GsonConverterFactory.create()) + .build() + .create(ApiService::class.java) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val qrData = QrScanDestination.getDataIfExist(requireArguments()) + if (qrData != null) { + sendRequestToServer(qrData) + } else { + binding.result.text = "Вход был отменён/Operation was cancelled" + } + + binding.close.setOnClickListener { + findNavController().popBackStack() + } + } + + private fun sendRequestToServer(qrData: String) { + lifecycleScope.launch { + try { + val response = apiService.openDoor(SessionManager.userLogin, OpenDoorRequest(qrData)) + if (response.isSuccessful) { + binding.result.text = "Успешно/Success" + } else { + binding.result.text = "Что-то пошло не так/Something wrong" + } + } catch (e: Exception) { + binding.result.text = "Что-то пошло не так/Something wrong" + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt new file mode 100644 index 0000000..7e34b28 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanDestination.kt @@ -0,0 +1,30 @@ +package ru.myitschool.work.ui.qr.scan + +import android.os.Bundle +import androidx.core.os.bundleOf +import kotlinx.serialization.Serializable + +// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ +@Serializable +data object QrScanDestination { + const val REQUEST_KEY = "qr_result" + private const val KEY_QR_DATA = "key_qr" + + fun newInstance(): QrScanFragment { + return QrScanFragment() + } + + fun getDataIfExist(bundle: Bundle): String? { + return if (bundle.containsKey(KEY_QR_DATA)) { + bundle.getString(KEY_QR_DATA) + } else { + null + } + } + + internal fun packToBundle(data: String): Bundle { + return bundleOf( + KEY_QR_DATA to data + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt new file mode 100644 index 0000000..a9ddaab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanFragment.kt @@ -0,0 +1,139 @@ +package ru.myitschool.work.ui.qr.scan + +import android.os.Bundle +import android.view.View +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.ImageAnalysis +import androidx.camera.mlkit.vision.MlKitAnalyzer +import androidx.camera.view.LifecycleCameraController +import androidx.camera.view.PreviewView +import androidx.core.content.ContextCompat +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import com.google.mlkit.vision.barcode.BarcodeScanner +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import ru.myitschool.work.R +import ru.myitschool.work.databinding.FragmentQrScanBinding +import ru.myitschool.work.utils.collectWhenStarted +import ru.myitschool.work.utils.visibleOrGone + +// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ +class QrScanFragment : Fragment(R.layout.fragment_qr_scan) { + private var _binding: FragmentQrScanBinding? = null + private val binding: FragmentQrScanBinding get() = _binding!! + + private var barcodeScanner: BarcodeScanner? = null + private var isCameraInit: Boolean = false + private val permissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> viewModel.onPermissionResult(isGranted) } + + private val viewModel: QrScanViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentQrScanBinding.bind(view) + sendResult(bundleOf()) + subscribe() + initCallback() + } + + private fun initCallback() { + binding.close.setOnClickListener { viewModel.close() } + } + + private fun subscribe() { + viewModel.state.collectWhenStarted(this) { state -> + binding.loading.visibleOrGone(state is QrScanViewModel.State.Loading) + binding.viewFinder.visibleOrGone(state is QrScanViewModel.State.Scan) + if (!isCameraInit && state is QrScanViewModel.State.Scan) { + startCamera() + isCameraInit = true + } + } + + viewModel.action.collectWhenStarted(this) { action -> + when (action) { + is QrScanViewModel.Action.RequestPermission -> requestPermission(action.permission) + is QrScanViewModel.Action.CloseWithCancel -> { + goBack() + } + is QrScanViewModel.Action.CloseWithResult -> { + sendResult(QrScanDestination.packToBundle(action.result)) + goBack() + } + } + } + } + + private fun requestPermission(permission: String) { + permissionLauncher.launch(permission) + } + + private fun startCamera() { + val context = requireContext() + val cameraController = LifecycleCameraController(context) + val previewView: PreviewView = binding.viewFinder + val executor = ContextCompat.getMainExecutor(context) + + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_QR_CODE) + .build() + val barcodeScanner = BarcodeScanning.getClient(options) + this.barcodeScanner = barcodeScanner + + cameraController.setImageAnalysisAnalyzer( + executor, + MlKitAnalyzer( + listOf(barcodeScanner), + ImageAnalysis.COORDINATE_SYSTEM_VIEW_REFERENCED, + executor + ) { result -> + result?.getValue(barcodeScanner)?.firstOrNull()?.let { value -> + viewModel.findBarcode(value) + + } + } + ) + + cameraController.bindToLifecycle(this) + previewView.controller = cameraController + } + + override fun onDestroyView() { + barcodeScanner?.close() + barcodeScanner = null + _binding = null + super.onDestroyView() + } + + private fun goBack() { + findNavControllerOrNull()?.popBackStack() + ?: requireActivity().onBackPressedDispatcher.onBackPressed() + } + + private fun sendResult(bundle: Bundle) { + setFragmentResult( + QrScanDestination.REQUEST_KEY, + bundle + ) + findNavControllerOrNull() + ?.previousBackStackEntry + ?.savedStateHandle + ?.set(QrScanDestination.REQUEST_KEY, bundle) + } + + private fun findNavControllerOrNull(): NavController? { + return try { + findNavController() + } catch (_: Throwable) { + null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt new file mode 100644 index 0000000..14565ab --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/ui/qr/scan/QrScanViewModel.kt @@ -0,0 +1,93 @@ +package ru.myitschool.work.ui.qr.scan + +import android.Manifest +import android.app.Application +import android.content.pm.PackageManager +import androidx.core.content.ContextCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.google.mlkit.vision.barcode.common.Barcode +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import ru.myitschool.work.utils.MutablePublishFlow + +// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ +class QrScanViewModel( + application: Application +) : AndroidViewModel(application) { + + private val _action = MutablePublishFlow() + val action = _action.asSharedFlow() + + private val _state = MutableStateFlow(initialState) + val state = _state.asStateFlow() + + init { + checkPermission() + } + + fun onPermissionResult(isGranted: Boolean) { + viewModelScope.launch { + if (isGranted) { + _state.update { State.Scan } + } else { + _action.emit(Action.CloseWithCancel) + } + } + } + + private fun checkPermission() { + viewModelScope.launch { + val isPermissionGranted = ContextCompat.checkSelfPermission( + getApplication(), + CAMERA_PERMISSION + ) == PackageManager.PERMISSION_GRANTED + if (isPermissionGranted) { + _state.update { State.Scan } + } else { + delay(1000) + _action.emit(Action.RequestPermission(CAMERA_PERMISSION)) + } + } + } + + fun findBarcode(barcode: Barcode) { + viewModelScope.launch { + barcode.rawValue?.let { value -> + _action.emit(Action.CloseWithResult(value)) + } + } + } + + fun close() { + viewModelScope.launch { + _action.emit(Action.CloseWithCancel) + } + } + + sealed interface State { + data object Loading : State + + data object Scan : State + } + + sealed interface Action { + data class RequestPermission( + val permission: String + ) : Action + data object CloseWithCancel : Action + data class CloseWithResult( + val result: String + ) : Action + } + + private companion object { + val initialState = State.Loading + + const val CAMERA_PERMISSION = Manifest.permission.CAMERA + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt new file mode 100644 index 0000000..87bccc2 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt @@ -0,0 +1,10 @@ +package ru.myitschool.work.utils + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow + +fun MutablePublishFlow() = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + BufferOverflow.DROP_OLDEST +) \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt new file mode 100644 index 0000000..8c99ef3 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/FragmentExtesions.kt @@ -0,0 +1,18 @@ +package ru.myitschool.work.utils + +import androidx.fragment.app.Fragment +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +inline fun Flow.collectWhenStarted( + fragment: Fragment, + crossinline collector: (T) -> Unit +) { + fragment.viewLifecycleOwner.lifecycleScope.launch { + flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> + collector.invoke(value) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt new file mode 100644 index 0000000..c81147d --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/TextChangedListener.kt @@ -0,0 +1,12 @@ +package ru.myitschool.work.utils + +import android.text.Editable +import android.text.TextWatcher + +open class TextChangedListener: TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable?) = Unit +} \ No newline at end of file diff --git a/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt new file mode 100644 index 0000000..5c38f67 --- /dev/null +++ b/app/src/main/java/ru/myitschool/work/utils/ViewExtensions.kt @@ -0,0 +1,7 @@ +package ru.myitschool.work.utils + +import android.view.View + +fun View.visibleOrGone(isVisible: Boolean) { + this.visibility = if (isVisible) View.VISIBLE else View.GONE +} \ No newline at end of file diff --git a/app/src/main/java/utils/AuthPreferences.kt b/app/src/main/java/utils/AuthPreferences.kt new file mode 100644 index 0000000..659f726 --- /dev/null +++ b/app/src/main/java/utils/AuthPreferences.kt @@ -0,0 +1,33 @@ +package ru.myitschool.work.utils + +import android.content.Context +import android.content.SharedPreferences + +class AuthPreferences(context: Context) { + private val sharedPreferences: SharedPreferences = context.getSharedPreferences("auth_prefs", Context.MODE_PRIVATE) + + companion object { + private const val KEY_IS_LOGGED_IN = "is_logged_in" + private const val KEY_LOGIN = "login" + } + + fun isLoggedIn(): Boolean { + return sharedPreferences.getBoolean(KEY_IS_LOGGED_IN, false) + } + + fun saveLoginState(isLoggedIn: Boolean) { + sharedPreferences.edit().putBoolean(KEY_IS_LOGGED_IN, isLoggedIn).apply() + } + + fun saveLogin(login: String) { + sharedPreferences.edit().putString(KEY_LOGIN, login).apply() + } + + fun getLogin(): String? { + return sharedPreferences.getString(KEY_LOGIN, null) + } + + fun clear() { + sharedPreferences.edit().clear().apply() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..f8ca0c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..c22a96f --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_no_img.xml b/app/src/main/res/drawable/ic_no_img.xml new file mode 100644 index 0000000..44206c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_no_img.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_qr_code.xml b/app/src/main/res/drawable/ic_qr_code.xml new file mode 100644 index 0000000..b03f9ae --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 0000000..86504d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_root.xml b/app/src/main/res/layout/activity_root.xml new file mode 100644 index 0000000..f63bcd7 --- /dev/null +++ b/app/src/main/res/layout/activity_root.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml new file mode 100644 index 0000000..63e7aa6 --- /dev/null +++ b/app/src/main/res/layout/fragment_login.xml @@ -0,0 +1,35 @@ + + + + +