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

View File

@ -1,5 +1,5 @@
package ru.myitschool.work.core
// БЕРИТЕ И ИЗМЕНЯЙТЕ ХОСТ ТОЛЬКО ЗДЕСЬ И НЕ БЕРИТЕ ИЗ ДРУГИХ МЕСТ. ФАЙЛ ПЕРЕМЕЩАТЬ НЕЛЬЗЯ
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 {
private val USERNAME_KEY = stringPreferencesKey("username")
private val ROLE_KEY = stringPreferencesKey("role")
private val PASSWORD_KEY = stringPreferencesKey("password")
fun getInstance(context: Context): UserDataStoreManager {
return UserDataStoreManager(context.applicationContext)
@ -21,13 +23,24 @@ class UserDataStoreManager(private val context: Context) {
val usernameFlow: Flow<String> = context.applicationContext.dataStore.data.map { prefs ->
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 ->
prefs[USERNAME_KEY] = username
prefs[PASSWORD_KEY] = password
}
}
suspend fun clearUsername() {
suspend fun saveRole(role: String){
context.dataStore.edit { prefs ->
prefs[ROLE_KEY] = role
}
}
suspend fun clearCredentials() {
context.applicationContext.dataStore.edit { it.clear() }
}
}

View File

@ -2,18 +2,16 @@ package ru.myitschool.work.data.door
import android.content.Context
import io.ktor.client.call.body
import io.ktor.client.request.basicAuth
import io.ktor.client.request.patch
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
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.OpenRequestDTO
import ru.myitschool.work.utils.NetworkModule
class DoorNetworkDataSource(
@ -21,12 +19,14 @@ class DoorNetworkDataSource(
) {
private val client = NetworkModule.httpClient
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 {
val username = userDataStoreManager.usernameFlow.first()
val result = client.patch("${Constants.SERVER_ADDRESS}/api/$username/open"){
contentType(ContentType.Application.Json)
setBody(openRequestDTO)
val password = userDataStoreManager.passwordFlow.first()
val result = client.patch("${Constants.SERVER_ADDRESS}/api/employee/open?code=$code"){
headers{
basicAuth(username, password)
}
}
if (result.status != HttpStatusCode.OK) {
error("Status ${result.status}")

View File

@ -1,12 +1,11 @@
package ru.myitschool.work.data.door
import ru.myitschool.work.domain.door.DoorRepo
import ru.myitschool.work.domain.entities.OpenEntity
class DoorRepoImpl(
private val networkDataSource: DoorNetworkDataSource
) : DoorRepo {
override suspend fun openDoor(openEntity: OpenEntity): Result<Unit> {
return networkDataSource.openDoor(openEntity.toDto())
override suspend fun openDoor(code: String): Result<Unit> {
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
@Serializable
data class OpenRequestDTO(
@SerialName("value") val value: Long
)
data class EmployeeEntranceListPagingDTO(
@SerialName("content") val content : List<EmployeeEntranceDTO>?
)

View File

@ -5,11 +5,11 @@ import kotlinx.serialization.Serializable
@Serializable
data class UserDTO(
@SerialName("id") val id: Int,
@SerialName("login") val login: String,
@SerialName("name") val name: String,
@SerialName("photo") val photo: String,
@SerialName("position") val position: String,
@SerialName("lastVisit") val lastVisit: String
@SerialName("id") val id: Long?,
@SerialName("login") val login: String?,
@SerialName("name") val name: String?,
@SerialName("authority") val authority : String?,
@SerialName("photoUrl") val photoUrl: 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 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.http.HttpStatusCode
import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
@ -22,8 +24,12 @@ class InfoNetworkDataSource(
suspend fun getInfo():Result<UserDTO> = withContext(Dispatchers.IO){
runCatching {
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) {
error("Status ${result.status}")
}

View File

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

View File

@ -1,9 +1,11 @@
package ru.myitschool.work.data.login
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.http.HttpStatusCode
import io.ktor.http.headers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
@ -11,10 +13,14 @@ import ru.myitschool.work.utils.NetworkModule
class LoginNetworkDataSource {
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 {
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) {
error("Status ${result.status}")
}

View File

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

View File

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

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
data class UserEntity (
data class UserEntity(
val id: Long,
val login: String,
val name: String,
val photo: String,
val position: String,
val lastVisit: String
val authority: String,
val photoUrl: String?,
val position: String
)

View File

@ -1,5 +1,5 @@
package ru.myitschool.work.domain.login
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(
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) {
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.password.addTextChangedListener(textWatcher)
binding.loginBtn.isEnabled = false
binding.loginBtn.setOnClickListener{
viewModel.login(binding.username.text.toString())
viewModel.login(binding.username.text.toString(), binding.password.text.toString())
}
lifecycleScope.launch {
viewModel.state.collect { state ->
with(binding) {
error.visibility = if (state is LoginViewModel.State.Error) View.VISIBLE else View.GONE
username.isEnabled = state !is LoginViewModel.State.Loading
if (state is LoginViewModel.State.Success) {
findNavController().navigate(R.id.mainFragment)
when(state){
is LoginViewModel.State.Error -> {
error.visibility = View.VISIBLE
loading.visibility = View.GONE
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 {
viewModelScope.launch{
val username = dataStoreManager.usernameFlow.first()
if(username != "")
login(username)
val password = dataStoreManager.passwordFlow.first()
if(username != "" && password != "")
login(username, password)
}
}
sealed class State {
object Idle : State()
object Loading : State()
object Success : State()
data object Idle : State()
data object Loading : State()
data object Success : State()
data class Error(val message: String?) : State()
}
fun login(username: String) {
fun login(username: String, password: String) {
_state.value = State.Loading
viewModelScope.launch{
useCase.invoke(username).fold(
onSuccess = { data ->
dataStoreManager.saveUsername(username)
useCase.invoke(username, password).fold(
onSuccess = { _ ->
dataStoreManager.saveCredentials(username, password)
_state.value = State.Success
},
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.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearLayoutManager
import com.squareup.picasso.Picasso
import kotlinx.coroutines.delay
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.utils.UserState
import ru.myitschool.work.utils.collectWhenStarted
import ru.myitschool.work.utils.collectWithLifecycle
class MainFragment : Fragment(R.layout.fragment_main) {
@ -31,7 +34,7 @@ class MainFragment : Fragment(R.layout.fragment_main) {
_binding = FragmentMainBinding.bind(view)
viewModel.getUserData()
binding.refresh.setOnClickListener { viewModel.getUserData() }
binding.logout.setOnClickListener { logout() }
binding.scan.setOnClickListener { onScanClick() }
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 ->
val qrData = QrScanDestination.getDataIfExist(bundle)
println(qrData)
@ -81,9 +105,9 @@ class MainFragment : Fragment(R.layout.fragment_main) {
private fun showUserData(userEntity: UserEntity) {
binding.apply {
fullname.text = userEntity.name
println(userEntity.name)
position.text = userEntity.position
lastEntry.text = viewModel.formatDate(userEntity.lastVisit)
Picasso.get().load(userEntity.photo).into(photo)
Picasso.get().load(userEntity.photoUrl).into(photo)
error.visibility = View.GONE
setViewsVisibility(View.VISIBLE)
@ -98,7 +122,6 @@ class MainFragment : Fragment(R.layout.fragment_main) {
private fun setViewsVisibility(visibility: Int) {
binding.fullname.visibility = visibility
binding.position.visibility = visibility
binding.lastEntry.visibility = visibility
binding.photo.visibility = visibility
binding.logout.visibility = visibility
binding.scan.visibility = visibility

View File

@ -6,6 +6,9 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -14,40 +17,52 @@ import kotlinx.coroutines.withContext
import ru.myitschool.work.data.UserDataStoreManager
import ru.myitschool.work.data.info.InfoNetworkDataSource
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.employeeEntrance.employeeEntrances.GetEmployeeEntranceListUseCase
import ru.myitschool.work.utils.UserState
import java.text.SimpleDateFormat
import java.util.Locale
class MainViewModel(
private val useCase: GetInfoUseCase,
private val infoUseCase: GetInfoUseCase,
private val listUseCase: GetEmployeeEntranceListUseCase,
application: 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)
val userState: StateFlow<UserState> get() = _userState
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() {
_userState.value = UserState.Loading
viewModelScope.launch {
useCase.invoke().fold(
infoUseCase.invoke().fold(
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() {
viewModelScope.launch{
withContext(Dispatchers.IO) {
dataStoreManager.clearUsername()
dataStoreManager.clearCredentials()
}
}
@ -64,16 +79,22 @@ class MainViewModel(
@Suppress("UNCHECKED_CAST")
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val repoImpl = InfoRepoImpl(
val infoRepoImpl = InfoRepoImpl(
networkDataSource = InfoNetworkDataSource(
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(
useCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
infoUseCase, listUseCase, extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] as Application
) as T
}
}

View File

@ -7,7 +7,6 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentQrResultBinding
import ru.myitschool.work.domain.entities.OpenEntity
import ru.myitschool.work.utils.collectWhenStarted
class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
@ -19,7 +18,7 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
_binding = FragmentQrResultBinding.bind(view)
var qrData = arguments?.getString("qr_data")
if (qrData != null) {
viewModel.openDoor(OpenEntity(qrData.toLong()))
viewModel.openDoor(qrData)
}
else{
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.DoorRepoImpl
import ru.myitschool.work.domain.door.OpenDoorUseCase
import ru.myitschool.work.domain.entities.OpenEntity
class QrResultViewModel(
private val useCase: OpenDoorUseCase,
@ -23,15 +22,15 @@ class QrResultViewModel(
val state: StateFlow<State> = _state.asStateFlow()
sealed class State{
object Success : State()
object Loading : State()
object Error : State()
data object Success : State()
data object Loading : State()
data object Error : State()
}
fun openDoor(openEntity: OpenEntity){
fun openDoor(code: String){
_state.value = State.Loading
viewModelScope.launch{
useCase.invoke(openEntity).fold(
onSuccess = { data->
useCase.invoke(code).fold(
onSuccess = { _ ->
_state.value = State.Success
},
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
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
@ -15,4 +17,14 @@ inline fun <T> Flow<T>.collectWhenStarted(
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"
navigationFragmentKtx = "2.8.7"
navigationUiKtx = "2.8.7"
pagingRuntimeKtx = "3.3.6"
picasso = "2.8"
[libraries]
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-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-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "pagingRuntimeKtx" }
barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "barcodeScanning" }
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" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientContentNegotiation" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorClientContentNegotiation" }