Add template
This commit is contained in:
		| @@ -1,6 +1,9 @@ | |||||||
| plugins { | plugins { | ||||||
|     kotlinAndroid |     kotlinAndroid | ||||||
|     androidApplication |     androidApplication | ||||||
|  |     jetbrainsKotlinSerialization version Version.Kotlin.language | ||||||
|  |     kotlinAnnotationProcessor | ||||||
|  |     id("com.google.dagger.hilt.android").version("2.51.1") | ||||||
| } | } | ||||||
|  |  | ||||||
| val packageName = "ru.myitschool.work" | val packageName = "ru.myitschool.work" | ||||||
| @@ -34,4 +37,33 @@ android { | |||||||
| dependencies { | dependencies { | ||||||
|     defaultLibrary() |     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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,7 +2,12 @@ | |||||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:tools="http://schemas.android.com/tools"> |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
|  |  | ||||||
|  |     <uses-feature android:name="android.hardware.camera.any" /> | ||||||
|  |     <uses-permission android:name="android.permission.CAMERA" /> | ||||||
|  |     <uses-permission android:name="android.permission.INTERNET" /> | ||||||
|  |  | ||||||
|     <application |     <application | ||||||
|  |         android:name=".App" | ||||||
|         android:allowBackup="true" |         android:allowBackup="true" | ||||||
|         android:dataExtractionRules="@xml/data_extraction_rules" |         android:dataExtractionRules="@xml/data_extraction_rules" | ||||||
|         android:fullBackupContent="@xml/backup_rules" |         android:fullBackupContent="@xml/backup_rules" | ||||||
| @@ -11,6 +16,16 @@ | |||||||
|         android:roundIcon="@mipmap/ic_launcher_round" |         android:roundIcon="@mipmap/ic_launcher_round" | ||||||
|         android:supportsRtl="true" |         android:supportsRtl="true" | ||||||
|         android:theme="@style/Theme.Default" |         android:theme="@style/Theme.Default" | ||||||
|         tools:targetApi="31" /> |         tools:targetApi="31"> | ||||||
|  |         <activity | ||||||
|  |             android:name=".ui.RootActivity" | ||||||
|  |             android:exported="true"> | ||||||
|  |             <intent-filter> | ||||||
|  |                 <action android:name="android.intent.action.MAIN" /> | ||||||
|  |  | ||||||
|  |                 <category android:name="android.intent.category.LAUNCHER" /> | ||||||
|  |             </intent-filter> | ||||||
|  |         </activity> | ||||||
|  |     </application> | ||||||
|  |  | ||||||
| </manifest> | </manifest> | ||||||
							
								
								
									
										7
									
								
								app/src/main/java/ru/myitschool/work/App.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								app/src/main/java/ru/myitschool/work/App.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | package ru.myitschool.work | ||||||
|  |  | ||||||
|  | import android.app.Application | ||||||
|  | import dagger.hilt.android.HiltAndroidApp | ||||||
|  |  | ||||||
|  | @HiltAndroidApp | ||||||
|  | class App : Application() | ||||||
							
								
								
									
										5
									
								
								app/src/main/java/ru/myitschool/work/core/Constants.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/java/ru/myitschool/work/core/Constants.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | package ru.myitschool.work.core | ||||||
|  | // НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ | ||||||
|  | object Constants { | ||||||
|  |     const val SERVER_ADDRESS = "http://localhost:8090" | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								app/src/main/java/ru/myitschool/work/ui/RootActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/src/main/java/ru/myitschool/work/ui/RootActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | |||||||
|  | package ru.myitschool.work.ui | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import androidx.activity.OnBackPressedCallback | ||||||
|  | import androidx.appcompat.app.AppCompatActivity | ||||||
|  | import androidx.navigation.createGraph | ||||||
|  | import androidx.navigation.findNavController | ||||||
|  | import androidx.navigation.fragment.NavHostFragment | ||||||
|  | import androidx.navigation.fragment.fragment | ||||||
|  | import dagger.hilt.android.AndroidEntryPoint | ||||||
|  | import ru.myitschool.work.R | ||||||
|  | import ru.myitschool.work.ui.login.LoginDestination | ||||||
|  | import ru.myitschool.work.ui.login.LoginFragment | ||||||
|  | import ru.myitschool.work.ui.qr.scan.QrScanDestination | ||||||
|  | import ru.myitschool.work.ui.qr.scan.QrScanFragment | ||||||
|  |  | ||||||
|  | // НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА! | ||||||
|  | @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.graph = navController.createGraph( | ||||||
|  |                 startDestination = LoginDestination | ||||||
|  |             ) { | ||||||
|  |                 fragment<LoginFragment, LoginDestination>() | ||||||
|  |                 fragment<QrScanFragment, QrScanDestination>() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         onBackPressedDispatcher.addCallback( | ||||||
|  |             this, | ||||||
|  |             object : OnBackPressedCallback(true) { | ||||||
|  |                 override fun handleOnBackPressed() { | ||||||
|  |                     onSupportNavigateUp() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onSupportNavigateUp(): Boolean { | ||||||
|  |         val navController = findNavController(R.id.nav_host_fragment) | ||||||
|  |         val popBackResult = if (navController.previousBackStackEntry != null) { | ||||||
|  |             navController.popBackStack() | ||||||
|  |         } else { | ||||||
|  |             false | ||||||
|  |         } | ||||||
|  |         return popBackResult || super.onSupportNavigateUp() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,6 @@ | |||||||
|  | package ru.myitschool.work.ui.login | ||||||
|  |  | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | data object LoginDestination | ||||||
| @@ -0,0 +1,36 @@ | |||||||
|  | package ru.myitschool.work.ui.login | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.View | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.viewModels | ||||||
|  | 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 | ||||||
|  |  | ||||||
|  | @AndroidEntryPoint | ||||||
|  | class LoginFragment : Fragment(R.layout.fragment_login) { | ||||||
|  |     private var _binding: FragmentLoginBinding? = null | ||||||
|  |     private val binding: FragmentLoginBinding get() = _binding!! | ||||||
|  |  | ||||||
|  |     private val viewModel: LoginViewModel by viewModels() | ||||||
|  |  | ||||||
|  |     override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | ||||||
|  |         super.onViewCreated(view, savedInstanceState) | ||||||
|  |         _binding = FragmentLoginBinding.bind(view) | ||||||
|  |         subscribe() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun subscribe() { | ||||||
|  |         viewModel.state.collectWhenStarted(this) { state -> | ||||||
|  |             binding.loading.visibleOrGone(state) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onDestroyView() { | ||||||
|  |         _binding = null | ||||||
|  |         super.onDestroyView() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | package ru.myitschool.work.ui.login | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import androidx.lifecycle.ViewModel | ||||||
|  | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | import dagger.hilt.android.qualifiers.ApplicationContext | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import javax.inject.Inject | ||||||
|  |  | ||||||
|  | @HiltViewModel | ||||||
|  | class LoginViewModel @Inject constructor( | ||||||
|  |     @ApplicationContext private val context: Context, | ||||||
|  | ) : ViewModel() { | ||||||
|  |     private val _state = MutableStateFlow(true) | ||||||
|  |     val state = _state.asStateFlow() | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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<Action>() | ||||||
|  |     val action = _action.asSharedFlow() | ||||||
|  |  | ||||||
|  |     private val _state = MutableStateFlow<State>(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 | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										10
									
								
								app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/java/ru/myitschool/work/utils/FlowExtensions.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | package ru.myitschool.work.utils | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.channels.BufferOverflow | ||||||
|  | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
|  |  | ||||||
|  | fun <T> MutablePublishFlow() = MutableSharedFlow<T>( | ||||||
|  |     replay = 0, | ||||||
|  |     extraBufferCapacity = 1, | ||||||
|  |     BufferOverflow.DROP_OLDEST | ||||||
|  | ) | ||||||
| @@ -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 <T> Flow<T>.collectWhenStarted( | ||||||
|  |     fragment: Fragment, | ||||||
|  |     crossinline collector: (T) -> Unit | ||||||
|  | ) { | ||||||
|  |     fragment.viewLifecycleOwner.lifecycleScope.launch { | ||||||
|  |         flowWithLifecycle(fragment.viewLifecycleOwner.lifecycle).collect { value -> | ||||||
|  |             collector.invoke(value) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_close.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_close.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> | ||||||
|  |      | ||||||
|  | </vector> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_logout.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_logout.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/> | ||||||
|  |      | ||||||
|  | </vector> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_no_img.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_no_img.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M21.9,21.9l-8.49,-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1 0.9,2 2,2h13.17l2.31,2.31L21.9,21.9zM5,18l3.5,-4.5l2.5,3.01L12.17,15l3,3H5zM21,18.17L5.83,3H19c1.1,0 2,0.9 2,2V18.17z"/> | ||||||
|  |      | ||||||
|  | </vector> | ||||||
							
								
								
									
										25
									
								
								app/src/main/res/drawable/ic_qr_code.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/src/main/res/drawable/ic_qr_code.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M19,19h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M13,13h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M15,15h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M13,17h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M15,19h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M17,17h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M17,13h2v2h-2z"/> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M19,15h2v2h-2z"/> | ||||||
|  |      | ||||||
|  | </vector> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_refresh.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_refresh.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp"> | ||||||
|  |        | ||||||
|  |     <path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/> | ||||||
|  |      | ||||||
|  | </vector> | ||||||
							
								
								
									
										13
									
								
								app/src/main/res/layout/activity_root.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								app/src/main/res/layout/activity_root.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |     <androidx.fragment.app.FragmentContainerView | ||||||
|  |         android:id="@+id/nav_host_fragment" | ||||||
|  |         android:name="androidx.navigation.fragment.NavHostFragment" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         app:defaultNavHost="true" /> | ||||||
|  | </FrameLayout> | ||||||
							
								
								
									
										16
									
								
								app/src/main/res/layout/fragment_login.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/src/main/res/layout/fragment_login.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |     <ProgressBar | ||||||
|  |         android:id="@+id/loading" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" /> | ||||||
|  |  | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
							
								
								
									
										35
									
								
								app/src/main/res/layout/fragment_qr_scan.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/src/main/res/layout/fragment_qr_scan.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |     <androidx.camera.view.PreviewView | ||||||
|  |         android:id="@+id/viewFinder" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |     <ProgressBar | ||||||
|  |         android:id="@+id/loading" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  |  | ||||||
|  |     <com.google.android.material.floatingactionbutton.FloatingActionButton | ||||||
|  |         android:id="@+id/close" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_margin="16dp" | ||||||
|  |         android:contentDescription="@string/close_button" | ||||||
|  |         android:src="@drawable/ic_close" | ||||||
|  |         app:elevation="0dp" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" /> | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
							
								
								
									
										4
									
								
								app/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <resources> | ||||||
|  |     <item name="rootContainer" type="id"/> | ||||||
|  | </resources> | ||||||
| @@ -1,3 +1,3 @@ | |||||||
| <resources> | <resources> | ||||||
|     <string name="app_name">Work</string> |     <string name="app_name">NTO Pass</string> | ||||||
| </resources> | </resources> | ||||||
							
								
								
									
										4
									
								
								app/src/main/res/values/strings_qr.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/values/strings_qr.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <resources> | ||||||
|  |     <string name="close_button">Close</string> | ||||||
|  | </resources> | ||||||
| @@ -2,4 +2,5 @@ | |||||||
| plugins { | plugins { | ||||||
|     androidApplication version Version.agp apply false |     androidApplication version Version.agp apply false | ||||||
|     kotlinJvm version Version.Kotlin.language apply false |     kotlinJvm version Version.Kotlin.language apply false | ||||||
|  |     id("com.google.dagger.hilt.android") version "2.51.1" apply false | ||||||
| } | } | ||||||
							
								
								
									
										2
									
								
								buildSrc
									
									
									
									
									
								
							
							
								
								
								
								
								
							
						
						
									
										2
									
								
								buildSrc
									
									
									
									
									
								
							 Submodule buildSrc updated: d959060004...ec48d5f6b8
									
								
							
		Reference in New Issue
	
	Block a user