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("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")
val cameraX = "1.3.4"

View File

@ -4,6 +4,9 @@ import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
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
@Module
@ -27,4 +30,10 @@ object NetworkModule {
fun provideApiServiceMain(retrofitClient: RetrofitClient): ApiServiceMain {
return retrofitClient.getApiServiceMain()
}
@Provides
@Singleton
fun provideApiServiceScan(retrofitClient: RetrofitClient): ApiServiceScan {
return retrofitClient.getApiServiceScan()
}
}

View File

@ -1,11 +1,17 @@
package ru.myitschool.work.api
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
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 java.util.concurrent.TimeUnit
class RetrofitClient {
private val serverAddress = SERVER_ADDRESS
@ -30,4 +36,8 @@ class RetrofitClient {
fun getApiServiceMain(): ApiServiceMain {
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.http.GET

View File

@ -1,4 +1,4 @@
package ru.myitschool.work.api
package ru.myitschool.work.api.main
import retrofit2.Call
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

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
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
object Constants {
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.qr.scan.QrScanDestination
import ru.myitschool.work.ui.qr.scan.QrScanFragment
import ru.myitschool.work.ui.result.ResultDestination
import ru.myitschool.work.ui.result.ResultFragment
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint
class RootActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -34,6 +35,7 @@ class RootActivity : AppCompatActivity() {
fragment<LoginFragment, LoginDestination>()
fragment<QrScanFragment, QrScanDestination>()
fragment<MainFragment, MainDestination>()
fragment<ResultFragment, ResultDestination>()
}
}

View File

@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Response
import ru.myitschool.work.api.ApiServiceLogin
import ru.myitschool.work.api.login.ApiServiceLogin
import javax.inject.Inject
@HiltViewModel
@ -22,7 +22,7 @@ class LoginViewModel @Inject constructor(
fun authenticate(login: String) {
viewModelScope.launch {
_state.value = LoginState.Loading // Устанавливаем состояние загрузки
_state.value = LoginState.Loading
apiService.authenticate(login).enqueue(object : retrofit2.Callback<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
position.text = state.userInfo.position
// Загрузка изображения из URL
loadImageFromUrl(state.userInfo.photoUrl)
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.Callback
import retrofit2.Response
import ru.myitschool.work.api.ApiServiceMain
import ru.myitschool.work.api.UserInfo
import ru.myitschool.work.api.main.ApiServiceMain
import ru.myitschool.work.api.main.UserInfo
import javax.inject.Inject
@HiltViewModel

View File

@ -4,7 +4,6 @@ import android.os.Bundle
import androidx.core.os.bundleOf
import kotlinx.serialization.Serializable
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
@Serializable
data object QrScanDestination {
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.visibleOrGone
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
class QrScanFragment : Fragment(R.layout.fragment_qr_scan) {
private var _binding: FragmentQrScanBinding? = null
private val binding: FragmentQrScanBinding get() = _binding!!

View File

@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.utils.MutablePublishFlow
// НЕ ИЗМЕНЯЙТЕ ЭТОТ ФАЙЛ. В ТЕСТАХ ОН БУДЕМ ВОЗВРАЩЁН В ИСХОДНОЕ СОСТОЯНИЕ
class QrScanViewModel(
application: 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) {
sharedPreferences.edit().apply {
putBoolean(KEY_IS_LOGGED_IN, isLoggedIn)
apply() // Асинхронное сохранение
apply()
}
}

View File

@ -5,6 +5,14 @@
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.Guideline
android:id="@+id/guideline_top"
@ -13,12 +21,26 @@
android:orientation="horizontal"
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
android:id="@+id/guideline_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent= "0.7" />
app:layout_constraintGuide_percent="0.7" />
<View
android:id="@+id/close"
@ -49,10 +71,11 @@
android:textColor="@color/black"
android:textSize="25sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/text_lastenter"
app:layout_constraintBottom_toTopOf="@+id/guideline_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!--
<TextView
android:id="@+id/text_lastenter"
android:layout_width="wrap_content"
@ -61,6 +84,6 @@
app:layout_constraintBottom_toTopOf="@+id/guideline_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
-->
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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