Merge remote-tracking branch 'origin/master'

This commit is contained in:
senijan 2025-02-19 13:53:27 +03:00
commit a3e2a923b8
40 changed files with 505 additions and 123 deletions

View File

@ -1,3 +1,5 @@
import com.android.sdklib.AndroidVersion.VersionCodes.UPSIDE_DOWN_CAKE
plugins { plugins {
androidApplication androidApplication
jetbrainsKotlinSerialization version Version.Kotlin.language jetbrainsKotlinSerialization version Version.Kotlin.language
@ -9,12 +11,12 @@ plugins {
val packageName = "ru.myitschool.work" val packageName = "ru.myitschool.work"
android { android {
namespace = packageName namespace = packageName
compileSdk = 35 compileSdk = UPSIDE_DOWN_CAKE
defaultConfig { defaultConfig {
applicationId = packageName applicationId = packageName
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = UPSIDE_DOWN_CAKE
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@ -38,12 +40,11 @@ android {
dependencies { dependencies {
implementation(libs.androidx.appcompat) implementation(libs.androidx.appcompat)
implementation(libs.material) implementation(libs.material)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.annotation) implementation(libs.androidx.annotation)
implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)
defaultLibrary()
implementation(Dependencies.AndroidX.activity) implementation(Dependencies.AndroidX.activity)
implementation(Dependencies.AndroidX.fragment) implementation(Dependencies.AndroidX.fragment)
@ -65,10 +66,10 @@ dependencies {
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.mlkit.vision) implementation(libs.androidx.camera.mlkit.vision)
implementation(libs.androidx.paging.runtime.ktx)
val hilt = "2.51.1" defaultLibrary()
implementation(libs.hilt.android) implementation(libs.hilt.android)
kapt("com.google.dagger:hilt-android-compiler:$hilt") kapt(libs.hilt.android.compiler)
} }
kapt { kapt {

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://10.0.2.2:8080" const val SERVER_ADDRESS = "http://192.168.1.145:8080"
} }

View File

@ -12,6 +12,8 @@ class UserDataStoreManager(private val context: Context) {
companion object { companion object {
private val USERNAME_KEY = stringPreferencesKey("username") private val USERNAME_KEY = stringPreferencesKey("username")
private val ROLE_KEY = stringPreferencesKey("role")
private val PASSWORD_KEY = stringPreferencesKey("password")
fun getInstance(context: Context): UserDataStoreManager { fun getInstance(context: Context): UserDataStoreManager {
return UserDataStoreManager(context.applicationContext) return UserDataStoreManager(context.applicationContext)
@ -21,13 +23,24 @@ class UserDataStoreManager(private val context: Context) {
val usernameFlow: Flow<String> = context.applicationContext.dataStore.data.map { prefs -> val usernameFlow: Flow<String> = context.applicationContext.dataStore.data.map { prefs ->
prefs[USERNAME_KEY] ?: "" prefs[USERNAME_KEY] ?: ""
} }
suspend fun saveUsername(username: String) { val passwordFlow: Flow<String> = context.applicationContext.dataStore.data.map { prefs ->
prefs[PASSWORD_KEY] ?: ""
}
val roleFlow: Flow<String> = context.applicationContext.dataStore.data.map{ prefs ->
prefs[ROLE_KEY] ?: ""
}
suspend fun saveCredentials(username: String, password: String) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[USERNAME_KEY] = username prefs[USERNAME_KEY] = username
prefs[PASSWORD_KEY] = password
} }
} }
suspend fun saveRole(role: String){
suspend fun clearUsername() { context.dataStore.edit { prefs ->
prefs[ROLE_KEY] = role
}
}
suspend fun clearCredentials() {
context.applicationContext.dataStore.edit { it.clear() } context.applicationContext.dataStore.edit { it.clear() }
} }
} }

View File

@ -2,18 +2,16 @@ package ru.myitschool.work.data.door
import android.content.Context import android.content.Context
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.basicAuth
import io.ktor.client.request.patch import io.ktor.client.request.patch
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserDataStoreManager import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.dto.OpenRequestDTO
import ru.myitschool.work.utils.NetworkModule import ru.myitschool.work.utils.NetworkModule
class DoorNetworkDataSource( class DoorNetworkDataSource(
@ -21,12 +19,14 @@ class DoorNetworkDataSource(
) { ) {
private val client = NetworkModule.httpClient private val client = NetworkModule.httpClient
private val userDataStoreManager = UserDataStoreManager.getInstance(context) private val userDataStoreManager = UserDataStoreManager.getInstance(context)
suspend fun openDoor(openRequestDTO: OpenRequestDTO): Result<Unit> = withContext(Dispatchers.IO){ suspend fun openDoor(code : String): Result<Unit> = withContext(Dispatchers.IO){
runCatching { runCatching {
val username = userDataStoreManager.usernameFlow.first() val username = userDataStoreManager.usernameFlow.first()
val result = client.patch("${Constants.SERVER_ADDRESS}/api/$username/open"){ val password = userDataStoreManager.passwordFlow.first()
contentType(ContentType.Application.Json) val result = client.patch("${Constants.SERVER_ADDRESS}/api/employee/open?code=$code"){
setBody(openRequestDTO) headers{
basicAuth(username, password)
}
} }
if (result.status != HttpStatusCode.OK) { if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}") error("Status ${result.status}")

View File

@ -1,12 +1,11 @@
package ru.myitschool.work.data.door package ru.myitschool.work.data.door
import ru.myitschool.work.domain.door.DoorRepo import ru.myitschool.work.domain.door.DoorRepo
import ru.myitschool.work.domain.entities.OpenEntity
class DoorRepoImpl( class DoorRepoImpl(
private val networkDataSource: DoorNetworkDataSource private val networkDataSource: DoorNetworkDataSource
) : DoorRepo { ) : DoorRepo {
override suspend fun openDoor(openEntity: OpenEntity): Result<Unit> { override suspend fun openDoor(code: String): Result<Unit> {
return networkDataSource.openDoor(openEntity.toDto()) return networkDataSource.openDoor(code)
} }
} }

View File

@ -0,0 +1,14 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import ru.myitschool.work.utils.DateSerializer
import java.util.Date
@Serializable
data class EmployeeEntranceDTO(
@SerialName("id") val id : Int?,
@SerialName("entryTime") @Serializable(with = DateSerializer::class) val scanTime : Date?,
@SerialName("readerName") val readerName: String?,
@SerialName("type") val type: String?
)

View File

@ -4,6 +4,6 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class OpenRequestDTO( data class EmployeeEntranceListPagingDTO(
@SerialName("value") val value: Long @SerialName("content") val content : List<EmployeeEntranceDTO>?
) )

View File

@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class UserDTO( data class UserDTO(
@SerialName("id") val id: Int, @SerialName("id") val id: Long?,
@SerialName("login") val login: String, @SerialName("login") val login: String?,
@SerialName("name") val name: String, @SerialName("name") val name: String?,
@SerialName("photo") val photo: String, @SerialName("authority") val authority : String?,
@SerialName("position") val position: String, @SerialName("photoUrl") val photoUrl: String?,
@SerialName("lastVisit") val lastVisit: String @SerialName("position") val position: String?
) )

View File

@ -0,0 +1,39 @@
package ru.myitschool.work.data.entrance.allEntrances
import android.content.Context
import io.ktor.client.call.body
import io.ktor.client.request.basicAuth
import io.ktor.client.request.get
import io.ktor.http.HttpStatusCode
import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.dto.EmployeeEntranceListPagingDTO
import ru.myitschool.work.utils.NetworkModule
class AllEntranceListNetworkDataSource(
context: Context
) {
private val client = NetworkModule.httpClient
private val userDataStoreManager = UserDataStoreManager.getInstance(context)
suspend fun getList(pageNum: Int, pageSize: Int):Result<EmployeeEntranceListPagingDTO> = withContext(
Dispatchers.IO){
runCatching {
val username = userDataStoreManager.usernameFlow.first()
val password = userDataStoreManager.passwordFlow.first()
val result = client.get("${Constants.SERVER_ADDRESS}/api/entrance/all?page=$pageNum&size=$pageSize"){
headers{
basicAuth(username, password)
}
}
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
result.body()
}
}
}

View File

@ -0,0 +1,24 @@
package ru.myitschool.work.data.entrance.allEntrances
import ru.myitschool.work.domain.employeeEntrance.allEntrances.AllEntranceListRepo
import ru.myitschool.work.domain.entities.EmployeeEntranceEntity
class AllEntranceListRepoImpl(
private val networkDataSource: AllEntranceListNetworkDataSource
) : AllEntranceListRepo {
override suspend fun getList(
pageNum: Int,
pageSize: Int
): Result<List<EmployeeEntranceEntity>> {
return networkDataSource.getList(pageNum, pageSize).map { pagingDTO ->
pagingDTO.content?.mapNotNull { dto ->
EmployeeEntranceEntity(
id = dto.id ?: return@mapNotNull null,
scanTime = dto.scanTime ?: return@mapNotNull null,
readerName = dto.readerName ?: return@mapNotNull null,
type = dto.type ?: return@mapNotNull null
)
}?: return Result.failure(IllegalStateException("List parse error"))
}
}
}

View File

@ -0,0 +1,40 @@
package ru.myitschool.work.data.entrance.employeeEntrances
import android.content.Context
import io.ktor.client.call.body
import io.ktor.client.request.basicAuth
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.dto.EmployeeEntranceListPagingDTO
import ru.myitschool.work.utils.NetworkModule
class EmployeeEntranceListNetworkDataSource(
context: Context
){
private val client = NetworkModule.httpClient
private val userDataStoreManager = UserDataStoreManager.getInstance(context)
suspend fun getList(pageNum: Int, pageSize: Int):Result<EmployeeEntranceListPagingDTO> = withContext(Dispatchers.IO){
runCatching {
val username = userDataStoreManager.usernameFlow.first()
val password = userDataStoreManager.passwordFlow.first()
val result = client.get("${Constants.SERVER_ADDRESS}/api/entrance?page=$pageNum&size=$pageSize"){
headers{
basicAuth(username, password)
}
}
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")
}
println(result.bodyAsText())
result.body()
}
}
}

View File

@ -0,0 +1,21 @@
package ru.myitschool.work.data.entrance.employeeEntrances
import ru.myitschool.work.domain.entities.EmployeeEntranceEntity
import ru.myitschool.work.domain.employeeEntrance.employeeEntrances.EmployeeEntranceListRepo
class EmployeeEntranceListRepoImpl(
private val networkDataSource: EmployeeEntranceListNetworkDataSource
) : EmployeeEntranceListRepo {
override suspend fun getList(pageNum: Int, pageSize: Int): Result<List<EmployeeEntranceEntity>> {
return networkDataSource.getList(pageNum, pageSize).map { pagingDTO ->
pagingDTO.content?.mapNotNull { dto->
EmployeeEntranceEntity(
id = dto.id ?: return@mapNotNull null,
scanTime = dto.scanTime ?: return@mapNotNull null,
readerName = dto.readerName ?: return@mapNotNull null,
type = dto.type ?: return@mapNotNull null,
)
}?: return Result.failure(IllegalStateException("List parse error"))
}
}
}

View File

@ -2,9 +2,11 @@ package ru.myitschool.work.data.info
import android.content.Context import android.content.Context
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.get import io.ktor.client.request.basicAuth
import io.ktor.client.request.post
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -22,8 +24,12 @@ class InfoNetworkDataSource(
suspend fun getInfo():Result<UserDTO> = withContext(Dispatchers.IO){ suspend fun getInfo():Result<UserDTO> = withContext(Dispatchers.IO){
runCatching { runCatching {
val username = userDataStoreManager.usernameFlow.first() val username = userDataStoreManager.usernameFlow.first()
val result = client.get("${Constants.SERVER_ADDRESS}/api/$username/info") val password = userDataStoreManager.passwordFlow.first()
val result = client.post("${Constants.SERVER_ADDRESS}/api/employee/profile"){
headers{
basicAuth(username, password)
}
}
if (result.status != HttpStatusCode.OK) { if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}") error("Status ${result.status}")
} }

View File

@ -7,13 +7,16 @@ class InfoRepoImpl(
private val networkDataSource: InfoNetworkDataSource private val networkDataSource: InfoNetworkDataSource
): InfoRepo { ): InfoRepo {
override suspend fun getInfo(): Result<UserEntity> { override suspend fun getInfo(): Result<UserEntity> {
return networkDataSource.getInfo().map { dto -> return networkDataSource.getInfo().map { dto->
UserEntity( UserEntity(
name = dto.name, id = dto.id ?: 0,
position = dto.position, login = dto.login ?: "",
lastVisit = dto.lastVisit, name = dto.name ?: "",
photo = dto.photo authority = dto.authority ?: "",
photoUrl = dto.photoUrl,
position = dto.position ?: ""
) )
} }
} }
} }

View File

@ -1,9 +1,11 @@
package ru.myitschool.work.data.login package ru.myitschool.work.data.login
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.get import io.ktor.client.request.basicAuth
import io.ktor.client.request.post
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
@ -11,10 +13,14 @@ import ru.myitschool.work.utils.NetworkModule
class LoginNetworkDataSource { class LoginNetworkDataSource {
private val client = NetworkModule.httpClient private val client = NetworkModule.httpClient
suspend fun login(username: String):Result<Unit> = withContext(Dispatchers.IO){ suspend fun login(username: String, password: String):Result<Unit> = withContext(Dispatchers.IO){
runCatching { runCatching {
val result = client.get("${Constants.SERVER_ADDRESS}/api/$username/auth") println("$username $password")
val result = client.post("${Constants.SERVER_ADDRESS}/api/employee/login"){
headers{
basicAuth(username, password)
}
}
if (result.status != HttpStatusCode.OK) { if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}") error("Status ${result.status}")
} }

View File

@ -5,7 +5,7 @@ import ru.myitschool.work.domain.login.LoginRepo
class LoginRepoImpl( class LoginRepoImpl(
private val networkDataSource: LoginNetworkDataSource private val networkDataSource: LoginNetworkDataSource
) : LoginRepo { ) : LoginRepo {
override suspend fun login(username: String): Result<Unit> { override suspend fun login(username: String, password: String): Result<Unit> {
return networkDataSource.login(username) return networkDataSource.login(username, password)
} }
} }

View File

@ -1,7 +1,5 @@
package ru.myitschool.work.domain.door package ru.myitschool.work.domain.door
import ru.myitschool.work.domain.entities.OpenEntity
interface DoorRepo { interface DoorRepo {
suspend fun openDoor(openEntity: OpenEntity) : Result<Unit> suspend fun openDoor(code: String) : Result<Unit>
} }

View File

@ -1,11 +1,7 @@
package ru.myitschool.work.domain.door package ru.myitschool.work.domain.door
import ru.myitschool.work.domain.entities.OpenEntity
class OpenDoorUseCase( class OpenDoorUseCase(
private val repo: DoorRepo private val repo: DoorRepo
) { ) {
suspend operator fun invoke(openEntity: OpenEntity) = repo.openDoor( suspend operator fun invoke(code: String) = repo.openDoor(code)
openEntity = openEntity
)
} }

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.employeeEntrance.allEntrances
import ru.myitschool.work.domain.entities.EmployeeEntranceEntity
interface AllEntranceListRepo {
suspend fun getList(pageNum : Int, pageSize: Int) : Result<List<EmployeeEntranceEntity>>
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.employeeEntrance.allEntrances
class GetAllEmployeesEntranceList(
private val repo: AllEntranceListRepo
) {
suspend operator fun invoke(pageNum : Int, pageSize: Int) = repo.getList(pageNum, pageSize)
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.employeeEntrance.employeeEntrances
import ru.myitschool.work.domain.entities.EmployeeEntranceEntity
interface EmployeeEntranceListRepo {
suspend fun getList(pageNum : Int, pageSize: Int) : Result<List<EmployeeEntranceEntity>>
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.employeeEntrance.employeeEntrances
class GetEmployeeEntranceListUseCase(
private val repo: EmployeeEntranceListRepo
) {
suspend operator fun invoke(pageNum : Int, pageSize: Int) = repo.getList(pageNum, pageSize)
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.domain.entities
import java.util.Date
data class EmployeeEntranceEntity(
val id : Int?,
val scanTime : Date?,
val readerName: String?,
val type: String?
)

View File

@ -1,13 +0,0 @@
package ru.myitschool.work.domain.entities
import ru.myitschool.work.data.dto.OpenRequestDTO
data class OpenEntity(
val value: Long
){
fun toDto() : OpenRequestDTO{
return OpenRequestDTO(
value = value
)
}
}

View File

@ -1,8 +1,10 @@
package ru.myitschool.work.domain.entities package ru.myitschool.work.domain.entities
data class UserEntity ( data class UserEntity(
val id: Long,
val login: String,
val name: String, val name: String,
val photo: String, val authority: String,
val position: String, val photoUrl: String?,
val lastVisit: String val position: String
) )

View File

@ -1,5 +1,5 @@
package ru.myitschool.work.domain.login package ru.myitschool.work.domain.login
interface LoginRepo { interface LoginRepo {
suspend fun login(username: String): Result<Unit> suspend fun login(username: String, password: String): Result<Unit>
} }

View File

@ -3,5 +3,5 @@ package ru.myitschool.work.domain.login
class LoginUseCase( class LoginUseCase(
private val repo: LoginRepo private val repo: LoginRepo
) { ) {
suspend operator fun invoke(username : String) = repo.login(username) suspend operator fun invoke(username: String, password: String) = repo.login(username, password)
} }

View File

@ -29,25 +29,44 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val username = binding.username.text val username = binding.username.text
binding.loginBtn.isEnabled = username.length >= 3 && !username[0].isDigit() && username.matches(Regex("^[a-zA-Z0-9]*$")) val password = binding.password.text
binding.loginBtn.isEnabled = username.length >= 3 && !username[0].isDigit() && username.matches(Regex("^[a-zA-Z0-9]*$")) &&
password.length >= 6
} }
} }
binding.username.addTextChangedListener(textWatcher) binding.username.addTextChangedListener(textWatcher)
binding.password.addTextChangedListener(textWatcher)
binding.loginBtn.isEnabled = false binding.loginBtn.isEnabled = false
binding.loginBtn.setOnClickListener{ binding.loginBtn.setOnClickListener{
viewModel.login(binding.username.text.toString()) viewModel.login(binding.username.text.toString(), binding.password.text.toString())
} }
lifecycleScope.launch { lifecycleScope.launch {
viewModel.state.collect { state -> viewModel.state.collect { state ->
with(binding) { with(binding) {
error.visibility = if (state is LoginViewModel.State.Error) View.VISIBLE else View.GONE when(state){
username.isEnabled = state !is LoginViewModel.State.Loading is LoginViewModel.State.Error -> {
error.visibility = View.VISIBLE
if (state is LoginViewModel.State.Success) { loading.visibility = View.GONE
findNavController().navigate(R.id.mainFragment) username.isEnabled = true
}
is LoginViewModel.State.Idle -> {
loading.visibility = View.GONE
error.visibility = View.GONE
username.isEnabled = true
}
is LoginViewModel.State.Loading -> {
loading.visibility = View.VISIBLE
error.visibility = View.GONE
username.isEnabled = false
}
is LoginViewModel.State.Success -> {
findNavController().navigate(R.id.mainFragment)
}
} }
} }
} }
} }

View File

@ -27,26 +27,27 @@ class LoginViewModel(
init { init {
viewModelScope.launch{ viewModelScope.launch{
val username = dataStoreManager.usernameFlow.first() val username = dataStoreManager.usernameFlow.first()
if(username != "") val password = dataStoreManager.passwordFlow.first()
login(username) if(username != "" && password != "")
login(username, password)
} }
} }
sealed class State { sealed class State {
object Idle : State() data object Idle : State()
object Loading : State() data object Loading : State()
object Success : State() data object Success : State()
data class Error(val message: String?) : State() data class Error(val message: String?) : State()
} }
fun login(username: String) { fun login(username: String, password: String) {
_state.value = State.Loading _state.value = State.Loading
viewModelScope.launch{ viewModelScope.launch{
useCase.invoke(username).fold( useCase.invoke(username, password).fold(
onSuccess = { data -> onSuccess = { _ ->
dataStoreManager.saveUsername(username) dataStoreManager.saveCredentials(username, password)
_state.value = State.Success _state.value = State.Success
}, },
onFailure = {e-> onFailure = {e->

View File

@ -0,0 +1,43 @@
package ru.myitschool.work.ui.main
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.myitschool.work.databinding.ItemVisitBinding
import ru.myitschool.work.domain.entities.EmployeeEntranceEntity
import ru.myitschool.work.utils.dateConverter
class EmployeeEntranceListAdapter : PagingDataAdapter<EmployeeEntranceEntity, EmployeeEntranceListAdapter.ViewHolder>(DiffUtil) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder{
return ViewHolder(
ItemVisitBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.bind(item)
}
}
inner class ViewHolder(
private val binding: ItemVisitBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: EmployeeEntranceEntity) {
binding.readerName.text = item.readerName
binding.timeVisit.text = dateConverter(item.scanTime)
}
}
object DiffUtil : androidx.recyclerview.widget.DiffUtil.ItemCallback<EmployeeEntranceEntity>() {
override fun areItemsTheSame(oldItem: EmployeeEntranceEntity, newItem: EmployeeEntranceEntity): Boolean {
return oldItem.scanTime == newItem.scanTime
}
override fun areContentsTheSame(oldItem: EmployeeEntranceEntity, newItem: EmployeeEntranceEntity): Boolean {
return oldItem == newItem
}
}
}

View File

@ -0,0 +1,36 @@
package ru.myitschool.work.ui.main
import androidx.paging.PagingSource
import androidx.paging.PagingState
import ru.myitschool.work.domain.entities.EmployeeEntranceEntity
class EmployeeEntranceListPagingSource(
private val request: suspend(pageNum: Int, pageSize: Int) ->Result<List<EmployeeEntranceEntity>>
) : PagingSource<Int, EmployeeEntranceEntity>() {
override fun getRefreshKey(state: PagingState<Int, EmployeeEntranceEntity>): Int? {
return state.anchorPosition?.let{
state.closestPageToPosition(it)?.prevKey?.plus(1) ?:
state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, EmployeeEntranceEntity> {
val pageNum = params.key ?: 0
return request.invoke(
pageNum, params.loadSize
).fold(
onSuccess = { value ->
LoadResult.Page(
data = value,
prevKey = (pageNum - 1).takeIf { it >= 0 },
nextKey = (pageNum + 1).takeIf { value.size == params.loadSize }
)
},
onFailure = { e->
println(e)
LoadResult.Error(e)
}
)
}
}

View File

@ -8,6 +8,8 @@ import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearLayoutManager
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -17,6 +19,7 @@ import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.ui.qr.scan.QrScanDestination import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.utils.UserState import ru.myitschool.work.utils.UserState
import ru.myitschool.work.utils.collectWhenStarted import ru.myitschool.work.utils.collectWhenStarted
import ru.myitschool.work.utils.collectWithLifecycle
class MainFragment : Fragment(R.layout.fragment_main) { class MainFragment : Fragment(R.layout.fragment_main) {
@ -31,7 +34,7 @@ class MainFragment : Fragment(R.layout.fragment_main) {
_binding = FragmentMainBinding.bind(view) _binding = FragmentMainBinding.bind(view)
viewModel.getUserData() viewModel.getUserData()
binding.refresh.setOnClickListener { viewModel.getUserData() }
binding.logout.setOnClickListener { logout() } binding.logout.setOnClickListener { logout() }
binding.scan.setOnClickListener { onScanClick() } binding.scan.setOnClickListener { onScanClick() }
viewModel.userState.collectWhenStarted(this) { state -> viewModel.userState.collectWhenStarted(this) { state ->
@ -57,6 +60,27 @@ class MainFragment : Fragment(R.layout.fragment_main) {
} }
} }
} }
binding.content.layoutManager = LinearLayoutManager(requireContext())
val adapter = EmployeeEntranceListAdapter()
binding.refresh.setOnClickListener {
viewModel.getUserData()
adapter.refresh()
}
binding.content.adapter = adapter
viewModel.listState.collectWithLifecycle(this) { data ->
adapter.submitData(data)
}
adapter.loadStateFlow.collectWithLifecycle(this) { loadState ->
val state = loadState.refresh
binding.error.visibility = if (state is LoadState.Error) View.VISIBLE else View.GONE
binding.loading.visibility = if (state is LoadState.Loading) View.VISIBLE else View.GONE
if (state is LoadState.Error) {
binding.error.text = state.error.message.toString()
}
}
setFragmentResultListener(QrScanDestination.REQUEST_KEY) { _, bundle -> setFragmentResultListener(QrScanDestination.REQUEST_KEY) { _, bundle ->
val qrData = QrScanDestination.getDataIfExist(bundle) val qrData = QrScanDestination.getDataIfExist(bundle)
println(qrData) println(qrData)
@ -81,9 +105,9 @@ class MainFragment : Fragment(R.layout.fragment_main) {
private fun showUserData(userEntity: UserEntity) { private fun showUserData(userEntity: UserEntity) {
binding.apply { binding.apply {
fullname.text = userEntity.name fullname.text = userEntity.name
println(userEntity.name)
position.text = userEntity.position position.text = userEntity.position
lastEntry.text = viewModel.formatDate(userEntity.lastVisit) Picasso.get().load(userEntity.photoUrl).into(photo)
Picasso.get().load(userEntity.photo).into(photo)
error.visibility = View.GONE error.visibility = View.GONE
setViewsVisibility(View.VISIBLE) setViewsVisibility(View.VISIBLE)
@ -98,7 +122,6 @@ class MainFragment : Fragment(R.layout.fragment_main) {
private fun setViewsVisibility(visibility: Int) { private fun setViewsVisibility(visibility: Int) {
binding.fullname.visibility = visibility binding.fullname.visibility = visibility
binding.position.visibility = visibility binding.position.visibility = visibility
binding.lastEntry.visibility = visibility
binding.photo.visibility = visibility binding.photo.visibility = visibility
binding.logout.visibility = visibility binding.logout.visibility = visibility
binding.scan.visibility = visibility binding.scan.visibility = visibility

View File

@ -6,6 +6,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -14,40 +17,52 @@ import kotlinx.coroutines.withContext
import ru.myitschool.work.data.UserDataStoreManager import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.info.InfoNetworkDataSource import ru.myitschool.work.data.info.InfoNetworkDataSource
import ru.myitschool.work.data.info.InfoRepoImpl import ru.myitschool.work.data.info.InfoRepoImpl
import ru.myitschool.work.data.entrance.employeeEntrances.EmployeeEntranceListNetworkDataSource
import ru.myitschool.work.data.entrance.employeeEntrances.EmployeeEntranceListRepoImpl
import ru.myitschool.work.domain.info.GetInfoUseCase import ru.myitschool.work.domain.info.GetInfoUseCase
import ru.myitschool.work.domain.employeeEntrance.employeeEntrances.GetEmployeeEntranceListUseCase
import ru.myitschool.work.utils.UserState import ru.myitschool.work.utils.UserState
import java.text.SimpleDateFormat
import java.util.Locale
class MainViewModel( class MainViewModel(
private val useCase: GetInfoUseCase, private val infoUseCase: GetInfoUseCase,
private val listUseCase: GetEmployeeEntranceListUseCase,
application: Application application: Application
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
val listState = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 30
)
) {
println("Creating PagingSource")
EmployeeEntranceListPagingSource(listUseCase::invoke)
}.flow.cachedIn(viewModelScope)
init {
viewModelScope.launch {
listState.collect { pagingData ->
if (pagingData.toString().isEmpty()) {
println("No data in paging data.")
} else {
println("Data received: $pagingData")
}
}
}
}
private val _userState = MutableStateFlow<UserState>(UserState.Loading) private val _userState = MutableStateFlow<UserState>(UserState.Loading)
val userState: StateFlow<UserState> get() = _userState val userState: StateFlow<UserState> get() = _userState
private val dataStoreManager = UserDataStoreManager(application) private val dataStoreManager = UserDataStoreManager(application)
fun formatDate(date: String): String {
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault())
val outputFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return try {
val formattedDate = inputFormat.parse(date)
if (formattedDate != null) {
outputFormat.format(formattedDate)
} else{}
} catch (_: Exception) {
"Invalid Date"
}.toString()
}
fun getUserData() { fun getUserData() {
_userState.value = UserState.Loading _userState.value = UserState.Loading
viewModelScope.launch { viewModelScope.launch {
useCase.invoke().fold( infoUseCase.invoke().fold(
onSuccess = { data -> _userState.value = UserState.Success(data) }, onSuccess = { data -> _userState.value = UserState.Success(data) },
onFailure = { _userState.value = UserState.Error } onFailure = { e -> _userState.value = UserState.Error
println(e)}
) )
} }
} }
@ -55,7 +70,7 @@ class MainViewModel(
fun clearUsername() { fun clearUsername() {
viewModelScope.launch{ viewModelScope.launch{
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
dataStoreManager.clearUsername() dataStoreManager.clearCredentials()
} }
} }
@ -64,16 +79,22 @@ class MainViewModel(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val repoImpl = InfoRepoImpl( val infoRepoImpl = InfoRepoImpl(
networkDataSource = InfoNetworkDataSource( networkDataSource = InfoNetworkDataSource(
context = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application context = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) )
) )
val listInfoImpl = EmployeeEntranceListRepoImpl(
networkDataSource = EmployeeEntranceListNetworkDataSource(
context = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
)
)
val useCase = GetInfoUseCase(repoImpl) val infoUseCase = GetInfoUseCase(infoRepoImpl)
val listUseCase = GetEmployeeEntranceListUseCase(listInfoImpl)
return MainViewModel( return MainViewModel(
useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application infoUseCase, listUseCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) as T ) as T
} }
} }

View File

@ -7,7 +7,6 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentQrResultBinding import ru.myitschool.work.databinding.FragmentQrResultBinding
import ru.myitschool.work.domain.entities.OpenEntity
import ru.myitschool.work.utils.collectWhenStarted import ru.myitschool.work.utils.collectWhenStarted
class QrResultFragment : Fragment(R.layout.fragment_qr_result) { class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
@ -19,7 +18,7 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
_binding = FragmentQrResultBinding.bind(view) _binding = FragmentQrResultBinding.bind(view)
var qrData = arguments?.getString("qr_data") var qrData = arguments?.getString("qr_data")
if (qrData != null) { if (qrData != null) {
viewModel.openDoor(OpenEntity(qrData.toLong())) viewModel.openDoor(qrData)
} }
else{ else{
binding.result.text = getString(R.string.result_null_text) binding.result.text = getString(R.string.result_null_text)

View File

@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
import ru.myitschool.work.data.door.DoorNetworkDataSource import ru.myitschool.work.data.door.DoorNetworkDataSource
import ru.myitschool.work.data.door.DoorRepoImpl import ru.myitschool.work.data.door.DoorRepoImpl
import ru.myitschool.work.domain.door.OpenDoorUseCase import ru.myitschool.work.domain.door.OpenDoorUseCase
import ru.myitschool.work.domain.entities.OpenEntity
class QrResultViewModel( class QrResultViewModel(
private val useCase: OpenDoorUseCase, private val useCase: OpenDoorUseCase,
@ -23,15 +22,15 @@ class QrResultViewModel(
val state: StateFlow<State> = _state.asStateFlow() val state: StateFlow<State> = _state.asStateFlow()
sealed class State{ sealed class State{
object Success : State() data object Success : State()
object Loading : State() data object Loading : State()
object Error : State() data object Error : State()
} }
fun openDoor(openEntity: OpenEntity){ fun openDoor(code: String){
_state.value = State.Loading _state.value = State.Loading
viewModelScope.launch{ viewModelScope.launch{
useCase.invoke(openEntity).fold( useCase.invoke(code).fold(
onSuccess = { data-> onSuccess = { _ ->
_state.value = State.Success _state.value = State.Success
}, },
onFailure = { _ -> onFailure = { _ ->

View File

@ -0,0 +1,15 @@
package ru.myitschool.work.utils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
fun dateConverter(date: Date?) : String {
if (date != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
println(dateFormat.format(date).toString())
return dateFormat.format(date).toString()
}
return ""
}

View File

@ -0,0 +1,25 @@
package ru.myitschool.work.utils
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object DateSerializer : KSerializer<Date> {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Date {
return dateFormat.parse(decoder.decodeString())!!
}
override fun serialize(encoder: Encoder, value: Date) {
encoder.encodeString(dateFormat.format(value))
}
}

View File

@ -1,8 +1,10 @@
package ru.myitschool.work.utils package ru.myitschool.work.utils
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -15,4 +17,14 @@ inline fun <T> Flow<T>.collectWhenStarted(
collector.invoke(value) collector.invoke(value)
} }
} }
}
fun <T> Flow<T>.collectWithLifecycle(
fragment: Fragment,
function: suspend (T) -> Unit
){
fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
collect { function.invoke(it) }
}
}
} }

View File

@ -17,6 +17,7 @@ lifecycleLivedataKtx = "2.8.7"
lifecycleViewmodelKtx = "2.8.7" lifecycleViewmodelKtx = "2.8.7"
navigationFragmentKtx = "2.8.7" navigationFragmentKtx = "2.8.7"
navigationUiKtx = "2.8.7" navigationUiKtx = "2.8.7"
pagingRuntimeKtx = "3.3.6"
picasso = "2.8" picasso = "2.8"
[libraries] [libraries]
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" } androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
@ -29,8 +30,10 @@ androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationUiKtx" } androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigationUiKtx" }
androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntimeKtx" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientContentNegotiation" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientContentNegotiation" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientContentNegotiation" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientContentNegotiation" }