Merge remote-tracking branch 'origin/main'

# Conflicts:
#	app/src/main/res/values/strings.xml
This commit is contained in:
veronicagtea 2025-02-19 09:58:22 +03:00
commit 8a25830c24
23 changed files with 249 additions and 20 deletions

View File

@ -49,7 +49,7 @@ dependencies {
implementation("com.squareup.picasso:picasso:2.8") implementation("com.squareup.picasso:picasso:2.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")
implementation("androidx.datastore:datastore-preferences:1.1.1") implementation("androidx.datastore:datastore-preferences:1.1.2")
implementation("com.google.mlkit:barcode-scanning:17.3.0") implementation("com.google.mlkit:barcode-scanning:17.3.0")
val cameraX = "1.3.4" val cameraX = "1.3.4"

View File

@ -4,6 +4,9 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import ru.myitschool.work.api.login.ApiServiceLogin
import ru.myitschool.work.api.main.ApiServiceMain
import ru.myitschool.work.api.scan.ApiServiceScan
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -27,4 +30,10 @@ object NetworkModule {
fun provideApiServiceMain(retrofitClient: RetrofitClient): ApiServiceMain { fun provideApiServiceMain(retrofitClient: RetrofitClient): ApiServiceMain {
return retrofitClient.getApiServiceMain() return retrofitClient.getApiServiceMain()
} }
@Provides
@Singleton
fun provideApiServiceScan(retrofitClient: RetrofitClient): ApiServiceScan {
return retrofitClient.getApiServiceScan()
}
} }

View File

@ -1,11 +1,17 @@
package ru.myitschool.work.api package ru.myitschool.work.api
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import ru.myitschool.work.api.login.ApiServiceLogin
import ru.myitschool.work.api.main.ApiServiceMain
import ru.myitschool.work.api.scan.ApiServiceScan
import ru.myitschool.work.core.Constants.SERVER_ADDRESS import ru.myitschool.work.core.Constants.SERVER_ADDRESS
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class RetrofitClient { class RetrofitClient {
private val serverAddress = SERVER_ADDRESS private val serverAddress = SERVER_ADDRESS
@ -30,4 +36,8 @@ class RetrofitClient {
fun getApiServiceMain(): ApiServiceMain { fun getApiServiceMain(): ApiServiceMain {
return retrofit.create(ApiServiceMain::class.java) return retrofit.create(ApiServiceMain::class.java)
} }
fun getApiServiceScan(): ApiServiceScan {
return retrofit.create(ApiServiceScan::class.java)
}
} }

View File

@ -1,4 +1,4 @@
package ru.myitschool.work.api package ru.myitschool.work.api.login
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET

View File

@ -1,4 +1,4 @@
package ru.myitschool.work.api package ru.myitschool.work.api.main
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET

View File

@ -1,4 +1,4 @@
package ru.myitschool.work.api package ru.myitschool.work.api.main
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName

View File

@ -0,0 +1,11 @@
package ru.myitschool.work.api.scan
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.PATCH
import retrofit2.http.Path
interface ApiServiceScan {
@PATCH("api/{login}/info")
fun open(@Path("login") login: String, @Body data: CodeJson): Call<Void>
}

View File

@ -0,0 +1,5 @@
package ru.myitschool.work.api.scan
class CodeJson(
private var value: String? = null
)

View File

@ -1,5 +1,5 @@
package ru.myitschool.work.core package ru.myitschool.work.core
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
object Constants { object Constants {
const val SERVER_ADDRESS = "http://localhost:8090" const val SERVER_ADDRESS = "http://localhost:8090"
} }

View File

@ -15,8 +15,9 @@ import ru.myitschool.work.ui.main.MainDestination
import ru.myitschool.work.ui.main.MainFragment import ru.myitschool.work.ui.main.MainFragment
import ru.myitschool.work.ui.qr.scan.QrScanDestination import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.ui.qr.scan.QrScanFragment import ru.myitschool.work.ui.qr.scan.QrScanFragment
import ru.myitschool.work.ui.result.ResultDestination
import ru.myitschool.work.ui.result.ResultFragment
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint @AndroidEntryPoint
class RootActivity : AppCompatActivity() { class RootActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -34,6 +35,7 @@ class RootActivity : AppCompatActivity() {
fragment<LoginFragment, LoginDestination>() fragment<LoginFragment, LoginDestination>()
fragment<QrScanFragment, QrScanDestination>() fragment<QrScanFragment, QrScanDestination>()
fragment<MainFragment, MainDestination>() fragment<MainFragment, MainDestination>()
fragment<ResultFragment, ResultDestination>()
} }
} }

View File

@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import ru.myitschool.work.api.ApiServiceLogin import ru.myitschool.work.api.login.ApiServiceLogin
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@ -22,7 +22,7 @@ class LoginViewModel @Inject constructor(
fun authenticate(login: String) { fun authenticate(login: String) {
viewModelScope.launch { viewModelScope.launch {
_state.value = LoginState.Loading // Устанавливаем состояние загрузки _state.value = LoginState.Loading
apiService.authenticate(login).enqueue(object : retrofit2.Callback<Void> { apiService.authenticate(login).enqueue(object : retrofit2.Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) { override fun onResponse(call: Call<Void>, response: Response<Void>) {

View File

@ -82,7 +82,6 @@ class MainFragment : Fragment(R.layout.main_fragment) {
username.text = state.userInfo.name username.text = state.userInfo.name
position.text = state.userInfo.position position.text = state.userInfo.position
// Загрузка изображения из URL
loadImageFromUrl(state.userInfo.photoUrl) loadImageFromUrl(state.userInfo.photoUrl)
val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()) val inputFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())

View File

@ -10,8 +10,8 @@ import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import ru.myitschool.work.api.ApiServiceMain import ru.myitschool.work.api.main.ApiServiceMain
import ru.myitschool.work.api.UserInfo import ru.myitschool.work.api.main.UserInfo
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel

View File

@ -4,7 +4,6 @@ import android.os.Bundle
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
@Serializable @Serializable
data object QrScanDestination { data object QrScanDestination {
const val REQUEST_KEY = "qr_result" const val REQUEST_KEY = "qr_result"

View File

@ -23,7 +23,6 @@ import ru.myitschool.work.databinding.FragmentQrScanBinding
import ru.myitschool.work.utils.collectWhenStarted import ru.myitschool.work.utils.collectWhenStarted
import ru.myitschool.work.utils.visibleOrGone import ru.myitschool.work.utils.visibleOrGone
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
class QrScanFragment : Fragment(R.layout.fragment_qr_scan) { class QrScanFragment : Fragment(R.layout.fragment_qr_scan) {
private var _binding: FragmentQrScanBinding? = null private var _binding: FragmentQrScanBinding? = null
private val binding: FragmentQrScanBinding get() = _binding!! private val binding: FragmentQrScanBinding get() = _binding!!

View File

@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.utils.MutablePublishFlow import ru.myitschool.work.utils.MutablePublishFlow
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
class QrScanViewModel( class QrScanViewModel(
application: Application application: Application
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {

View File

@ -0,0 +1,63 @@
package ru.myitschool.work.ui.result
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Response
import ru.myitschool.work.api.scan.ApiServiceScan
import ru.myitschool.work.api.scan.CodeJson
import javax.inject.Inject
@HiltViewModel
class ResultViewModel @Inject constructor(
private val apiService: ApiServiceScan
) : ViewModel() {
private val _state = MutableStateFlow<ResultState>(ResultState.Initial)
val state = _state.asStateFlow()
fun open(login: String, data: CodeJson) {
viewModelScope.launch {
_state.value = ResultState.Loading
apiService.open(login, data).enqueue(object : retrofit2.Callback<Void> {
override fun onResponse(call: Call<Void>, response: Response<Void>) {
Log.d("Open", "Response code: ${response.code()}")
when (response.code()) {
200 -> {
_state.value = ResultState.Success
}
500 -> {
_state.value = ResultState.InvalidCredentials
}
401 -> {
_state.value = ResultState.Error
}
else -> {
_state.value = ResultState.InvalidCredentials
}
}
}
override fun onFailure(call: Call<Void>, t: Throwable) {
_state.value = ResultState.Error
}
})
}
}
sealed class ResultState {
data object Initial : ResultState()
data object InvalidCredentials : ResultState()
data object Loading : ResultState()
data object Success : ResultState()
data object Error : ResultState()
}
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.result
import kotlinx.serialization.Serializable
@Serializable
data object ResultDestination

View File

@ -0,0 +1,99 @@
package ru.myitschool.work.ui.result
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R
import androidx.navigation.fragment.findNavController
import androidx.transition.Visibility
import ru.myitschool.work.databinding.FragmentScanResultBinding
import ru.myitschool.work.ui.login.LoginViewModel
import ru.myitschool.work.ui.main.MainDestination
import ru.myitschool.work.utils.AuthPreferences
import ru.myitschool.work.utils.collectWhenStarted
import ru.myitschool.work.utils.visibleOrGone
@AndroidEntryPoint
class ResultFragment : Fragment(R.layout.fragment_scan_result) {
private var _binding: FragmentScanResultBinding? = null
private val binding: FragmentScanResultBinding get() = _binding!!
private val viewModel: ResultViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentScanResultBinding.bind(view)
setupLoginComponents()
}
private fun setupLoginComponents() {
binding.apply {
result.visibleOrGone(false)
successIcon.visibleOrGone(false)
}
}
private fun observeLoginState() {
viewModel.state.collectWhenStarted(this) { state ->
when (state) {
is ResultViewModel.ResultState.Loading -> {
binding.result.visibleOrGone(false)
binding.successIcon.visibleOrGone(false)
binding.error.visibleOrGone(false)
binding.loading.visibleOrGone(true)
}
is ResultViewModel.ResultState.Success -> {
binding.result.visibleOrGone(true)
binding.successIcon.visibleOrGone(true)
binding.error.visibleOrGone(false)
binding.loading.visibleOrGone(false)
}
is ResultViewModel.ResultState.InvalidCredentials -> {
binding.result.visibleOrGone(false)
binding.successIcon.visibleOrGone(false)
binding.loading.visibleOrGone(false)
binding.error.visibleOrGone(true)
}
is ResultViewModel.ResultState.Error -> {
binding.loading.visibleOrGone(false)
binding.result.apply {
visibleOrGone(true)
text = RiootoString(R.string.userNotFing)
}
Log.d("Authentication", "Ошибка сканирования")
}
LoginViewModel.LoginState.Initial -> binding.loading.visibleOrGone(false)
}
}
}
private fun navigateToMainScreen() {
try {
findNavController().apply {
popBackStack(MainDestination, false)
navigate(MainDestination)
}
} catch (e: Exception) {
Log.e("ResultFragment", "Navigation error", e)
Toast.makeText(context, "Ошибка перехода", Toast.LENGTH_SHORT).show()
}
}
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.result
object TextStatus {
const val error = "Вход был отменён/Operation was cancelled"
const val success = "Успешно/Success"
}

View File

@ -12,7 +12,7 @@ class AuthPreferences(context: Context) {
fun saveLoginState(isLoggedIn: Boolean) { fun saveLoginState(isLoggedIn: Boolean) {
sharedPreferences.edit().apply { sharedPreferences.edit().apply {
putBoolean(KEY_IS_LOGGED_IN, isLoggedIn) putBoolean(KEY_IS_LOGGED_IN, isLoggedIn)
apply() // Асинхронное сохранение apply()
} }
} }

View File

@ -5,6 +5,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_top" android:id="@+id/guideline_top"
@ -13,6 +21,20 @@
android:orientation="horizontal" android:orientation="horizontal"
app:layout_constraintGuide_percent="0.25" /> app:layout_constraintGuide_percent="0.25" />
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/serverError"
android:textSize="14sp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
<androidx.constraintlayout.widget.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_bottom" android:id="@+id/guideline_bottom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -49,10 +71,11 @@
android:textColor="@color/black" android:textColor="@color/black"
android:textSize="25sp" android:textSize="25sp"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_lastenter" app:layout_constraintBottom_toTopOf="@+id/guideline_bottom"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<!--
<TextView <TextView
android:id="@+id/text_lastenter" android:id="@+id/text_lastenter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -61,6 +84,6 @@
app:layout_constraintBottom_toTopOf="@+id/guideline_bottom" app:layout_constraintBottom_toTopOf="@+id/guideline_bottom"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
-->
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,6 @@
<resources> <resources>
<string name="app_name">NTO Pass</string> <string name="app_name">NTO Pass</string>
<string name="loginText">логин</string> <string name="loginText">Логин</string>
<string name="welcomeText">Добро пожаловать!</string> <string name="welcomeText">Добро пожаловать!</string>
<string name="inputLoginText">Введите свой логин</string> <string name="inputLoginText">Введите свой логин</string>
<string name="welcomeDescriptionLoginText">для авторизации в приложении</string> <string name="welcomeDescriptionLoginText">для авторизации в приложении</string>
@ -19,5 +19,4 @@
<string name="closeText">Закрыть/Close</string> <string name="closeText">Закрыть/Close</string>
<string name="errorLoginText">Ошибка входа</string> <string name="errorLoginText">Ошибка входа</string>
<string name="text_last_enter">Последний вход 12.12.1212</string> <string name="text_last_enter">Последний вход 12.12.1212</string>
<string name="passwordText">пароль</string>
</resources> </resources>