Merge remote-tracking branch 'origin/master'

This commit is contained in:
mariamusinkina 2025-02-20 20:14:54 +05:00
commit 46616380aa
73 changed files with 1604 additions and 356 deletions

View File

@ -5,6 +5,11 @@ plugins {
kotlinAnnotationProcessor
id("com.google.dagger.hilt.android").version("2.51.1")
}
configurations.all {
resolutionStrategy {
force("org.hamcrest:hamcrest-junit:2.0.0.0")
}
}
val packageName = "ru.myitschool.work"
@ -29,15 +34,37 @@ android {
targetCompatibility = Version.Kotlin.javaSource
}
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
kotlinOptions {
jvmTarget = Version.Kotlin.jvmTarget
}
}
dependencies {
val fragmentVersion = "1.8.6"
debugImplementation("androidx.fragment:fragment-testing-manifest:$fragmentVersion")
androidTestImplementation("androidx.fragment:fragment-testing:$fragmentVersion")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
testImplementation("androidx.test:core:1.6.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.robolectric:robolectric:4.14")
defaultLibrary()
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
implementation("androidx.paging:paging-runtime:3.3.6")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val ktorClientCore = "3.0.3"
@ -54,9 +81,6 @@ dependencies {
implementation(Dependencies.AndroidX.Navigation.fragment)
implementation(Dependencies.AndroidX.Navigation.navigationUi)
implementation(Dependencies.Retrofit.library)
implementation(Dependencies.Retrofit.gsonConverter)
implementation("com.squareup.picasso:picasso:2.8")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("androidx.datastore:datastore-preferences:1.1.2")

View File

@ -0,0 +1,26 @@
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import ru.myitschool.work.R
import ru.myitschool.work.ui.login.LoginFragment
import utils.SwipeRefreshLayoutMatchers.isRefreshing
@RunWith(AndroidJUnit4ClassRunner::class)
@LargeTest
class LoginFragmentTest {
@get:Rule
val fragmentRule = launchFragmentInContainer<LoginFragment>()
@Test
fun onSwipeDataRefreshes() {
onView(withId(R.id.refresh)).perform(swipeDown())
onView(withId(R.id.refresh)).check(matches(isRefreshing()))
}
}

View File

@ -0,0 +1,25 @@
package utils
import android.view.View
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.test.espresso.matcher.BoundedMatcher
import org.hamcrest.Description
import org.hamcrest.Matcher
object SwipeRefreshLayoutMatchers {
@JvmStatic
fun isRefreshing(): Matcher<View> {
return object : BoundedMatcher<View, SwipeRefreshLayout>(
SwipeRefreshLayout::class.java) {
override fun describeTo(description: Description) {
description.appendText("is refreshing")
}
override fun matchesSafely(view: SwipeRefreshLayout): Boolean {
return view.isRefreshing
}
}
}
}

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:name=".App"

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

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

View File

@ -0,0 +1,18 @@
package ru.myitschool.work.data
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.AdminNetworkDataSource
import ru.myitschool.work.domain.admin.AdminRepository
class AdminRepositoryImpl(
private val networkDataSource: AdminNetworkDataSource,
private val localCredentialsLocalDataSource: CredentialsLocalDataSource
) : AdminRepository {
override suspend fun blockUser(login: String): Result<Unit> {
return networkDataSource.blockUser(login, localCredentialsLocalDataSource.getToken())
}
override suspend fun unblockUser(login: String): Result<Unit> {
return networkDataSource.unblockUser(login, localCredentialsLocalDataSource.getToken())
}
}

View File

@ -0,0 +1,52 @@
package ru.myitschool.work.data
import android.util.Log
import ru.myitschool.work.data.dto.PassDto
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.PassNetworkDataSource
import ru.myitschool.work.domain.entities.PassEntity
import ru.myitschool.work.domain.passes.PassRepository
class PassRepositoryImpl(
private val networkDataSource: PassNetworkDataSource,
private val credentialsLocalDataSource: CredentialsLocalDataSource
) : PassRepository {
override suspend fun getCurrentPasses(pageNum: Int, pageSize: Int): Result<List<PassEntity>> {
return map(
networkDataSource.getCurrentPasses(
pageNum,
pageSize,
credentialsLocalDataSource.getToken()
)
)
}
override suspend fun getUsersPasses(
pageNum: Int,
pageSize: Int,
login: String
): Result<List<PassEntity>> {
return map(
networkDataSource.getUsersPasses(
login = login,
pageNum = pageNum,
pageSize = pageSize,
token = credentialsLocalDataSource.getToken()
)
)
}
private fun map(listDto: Result<List<PassDto>>): Result<List<PassEntity>> {
return listDto.map { successListDto ->
successListDto.mapNotNull { dto ->
PassEntity(
type = dto.terminal?.type ?: return@mapNotNull null,
name = dto.terminal.name ?: return@mapNotNull null,
time = dto.time ?: return@mapNotNull null
)
}
}
}
}

View File

@ -1,6 +1,7 @@
package ru.myitschool.work.data
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.QrNetworkDataSource
import ru.myitschool.work.domain.entities.QrEntity
import ru.myitschool.work.domain.qr.QrRepository

View File

@ -1,6 +1,5 @@
package ru.myitschool.work.data
import android.util.Log
import ru.myitschool.work.data.dto.UserDto
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource
@ -19,16 +18,29 @@ class UserRepositoryImpl(
return runCatching {
networkDataSource.login(credentialsLocalDataSource.updateToken(login, password))
.onSuccess { dto ->
map(dto).onSuccess { userLocalDataSource.cacheData(it) }
}
.fold(
onSuccess = { dto ->
map(dto).fold(
onSuccess = {
userLocalDataSource.cacheData(it)
},
onFailure = { error(it) }
)
},
onFailure = { error(it) }
)
}
}
override suspend fun authorize(token: String): Result<Unit> {
return networkDataSource.login(token).fold(
onSuccess = { Result.success(Unit) },
onFailure = { error -> Result.failure(error) }
onSuccess = { dto ->
map(dto).fold(
onSuccess = { Result.success(userLocalDataSource.cacheData(it)) },
onFailure = { Result.failure(it) }
)
},
onFailure = { Result.failure(it) }
)
}
@ -45,15 +57,27 @@ class UserRepositoryImpl(
return userLocalDataSource.getUser()!!
}
private fun map(userDto: UserDto): Result<UserEntity> {
private suspend fun map(userDto: UserDto): Result<UserEntity> {
return runCatching {
UserEntity(
id = userDto.id ?: error("Null user id"),
name = userDto.name ?: error("Null user name"),
lastVisit = userDto.lastVisit ?: error("Null user lastVisit"),
lastVisit = userDto.lastVisit ?: "",
photoUrl = userDto.photoUrl ?: error("Null user photoUrl"),
position = userDto.position ?: error("Null user position")
position = userDto.position ?: error("Null user position"),
isAdmin = userDto.roleId?.let { it ->
networkDataSource.isRoleHasAdminPermissions(
it,
credentialsLocalDataSource.getToken()
).fold(onSuccess = { it }, onFailure = { error(it) })
} ?: error("Null user roleId"),
isCardBlocked = userDto.isCardBlocked ?: error("Null user isCardBlocked")
)
}
}
override suspend fun getUserByLogin(login: String): Result<UserEntity> {
return networkDataSource.getUserByLogin(login, credentialsLocalDataSource.getToken()).fold(
onSuccess = { map(it) }, onFailure = { Result.failure(it) }
)
}
}

View File

@ -0,0 +1,21 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PassDto(
@SerialName("localDateTime")
val time: String?,
@SerialName("terminal")
val terminal: TerminalDto?
)
@Serializable
data class TerminalDto(
@SerialName("type")
val type: String?,
@SerialName("name")
val name: String?
)

View File

@ -1,31 +0,0 @@
package ru.myitschool.work.data.dto;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import kotlinx.serialization.Serializable;
@Serializable
public class UserDto {
@Nullable
@SerializedName("id")
public String id;
@Nullable
@SerializedName("name")
public String name;
@Nullable
@SerializedName("lastVisit")
public String lastVisit;
@Nullable
@SerializedName("photo")
public String photoUrl;
@Nullable
@SerializedName("position")
public String position;
@Nullable
@SerializedName("login")
public String login;
}

View File

@ -0,0 +1,22 @@
package ru.myitschool.work.data.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserDto(
@SerialName("authority_id")
val roleId: Int?,
@SerialName("name")
val name: String?,
@SerialName("lastVisit")
val lastVisit: String?,
@SerialName("photo")
val photoUrl: String?,
@SerialName("position")
val position: String?,
@SerialName("username")
val login: String?,
@SerialName("isCardBlocked")
val isCardBlocked: Boolean?
)

View File

@ -0,0 +1,44 @@
package ru.myitschool.work.data.network
import io.ktor.client.request.headers
import io.ktor.client.request.patch
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants.SERVER_ADDRESS
object AdminNetworkDataSource {
private val client = KtorClient.client
suspend fun blockUser(login: String, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching {
val response = client.patch("$SERVER_ADDRESS/api/users/block?username=$login") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
}
suspend fun unblockUser(login: String, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching {
val response = client.patch("$SERVER_ADDRESS/api/users/unblock?username=$login") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
}
}

View File

@ -0,0 +1,56 @@
package ru.myitschool.work.data.network
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants.SERVER_ADDRESS
import ru.myitschool.work.data.dto.PassDto
object PassNetworkDataSource {
private val client = KtorClient.client
suspend fun getCurrentPasses(
pageNum: Int,
pageSize: Int,
token: String
): Result<List<PassDto>> =
withContext(Dispatchers.IO) {
runCatching {
val response =
client.get("$SERVER_ADDRESS/api/passes/paginated/?page=$pageNum&size=$pageSize") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (response.status != HttpStatusCode.OK)
error("${response.status}")
response.body()
}
}
suspend fun getUsersPasses(
login: String,
pageNum: Int,
pageSize: Int,
token: String
): Result<List<PassDto>> = withContext(Dispatchers.IO) {
runCatching {
val response =
client.get("$SERVER_ADDRESS/api/passes?login=$login&pageNum=$pageNum&pageSize=$pageSize") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
response.body()
}
}
}

View File

@ -1,4 +1,4 @@
package ru.myitschool.work.data
package ru.myitschool.work.data.network
import io.ktor.client.request.headers
import io.ktor.client.request.patch
@ -11,28 +11,25 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.dto.QrDto
import ru.myitschool.work.data.network.KtorClient
import ru.myitschool.work.domain.entities.QrEntity
object QrNetworkDataSource {
suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> = withContext(Dispatchers.IO) {
runCatching {
val response = KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/push_qr") {
headers {
append(HttpHeaders.Authorization, token)
}
contentType(ContentType.Application.Json)
setBody(
QrDto(code = qrEntity.code)
)
suspend fun pushQr(qrEntity: QrEntity, token: String): Result<Unit> =
withContext(Dispatchers.IO) {
runCatching {
val response =
KtorClient.client.patch("${Constants.SERVER_ADDRESS}/api/open") {
headers {
append(HttpHeaders.Authorization, token)
}
contentType(ContentType.Application.Json)
setBody(QrDto(qrEntity.code))
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
Unit
}
}
}

View File

@ -22,16 +22,45 @@ object UserNetworkDataSource {
suspend fun login(token: String): Result<UserDto> =
withContext(Dispatchers.IO) {
runCatching {
val result = KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") {
headers {
append(HttpHeaders.Authorization, token)
val result =
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/login") {
headers {
append(HttpHeaders.Authorization, token)
}
}
}
if (result.status != HttpStatusCode.OK)
error("Status ${result.status}")
result.body()
}
}
suspend fun getUserByLogin() {}
suspend fun isRoleHasAdminPermissions(roleId: Int, token: String): Result<Boolean> =
withContext(Dispatchers.IO) {
runCatching {
val response =
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/authority/$roleId") {
headers {
append(HttpHeaders.Authorization, token)
}
}
response.status == HttpStatusCode.OK
}
}
suspend fun getUserByLogin(login: String, token: String): Result<UserDto> =
withContext(Dispatchers.IO) {
runCatching {
val response =
KtorClient.client.get("${Constants.SERVER_ADDRESS}/api/users/get?username=${login}") {
headers {
append(HttpHeaders.Authorization, token)
}
}
if (response.status != HttpStatusCode.OK)
error("Status ${response.status}")
response.body()
}
}
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.domain.admin
interface AdminRepository {
suspend fun blockUser(login: String): Result<Unit>
suspend fun unblockUser(login: String): Result<Unit>
}

View File

@ -0,0 +1,8 @@
package ru.myitschool.work.domain.admin
class BlockUserUseCase(
private val repository: AdminRepository
) {
suspend operator fun invoke(login: String) = repository.blockUser(login)
}

View File

@ -0,0 +1,8 @@
package ru.myitschool.work.domain.admin
class UnblockUserUseCase(
private val repository: AdminRepository
) {
suspend operator fun invoke(login: String) = repository.unblockUser(login)
}

View File

@ -0,0 +1,7 @@
package ru.myitschool.work.domain.entities
data class PassEntity(
val type: String,
val name: String,
val time: String
)

View File

@ -1,9 +1,10 @@
package ru.myitschool.work.domain.entities
data class UserEntity(
val id: String,
val isAdmin: Boolean,
val name: String,
val lastVisit: String,
val photoUrl: String,
val position: String,
val isCardBlocked: Boolean
)

View File

@ -0,0 +1,11 @@
package ru.myitschool.work.domain.passes
import ru.myitschool.work.domain.entities.PassEntity
class GetCurrentPassesUseCase(
private val repository: PassRepository
) {
suspend operator fun invoke(pageNum: Int, pageSize: Int): Result<List<PassEntity>> =
repository.getCurrentPasses(pageNum, pageSize)
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.domain.passes
class GetUsersPassesUseCase(
private val repository: PassRepository
) {
suspend operator fun invoke(pageNum: Int, pageSize: Int, login: String) =
repository.getUsersPasses(pageNum, pageSize, login)
}

View File

@ -0,0 +1,9 @@
package ru.myitschool.work.domain.passes
import ru.myitschool.work.domain.entities.PassEntity
interface PassRepository {
suspend fun getCurrentPasses(pageNum: Int, pageSize: Int): Result<List<PassEntity>>
suspend fun getUsersPasses(pageNum: Int, pageSize: Int, login: String): Result<List<PassEntity>>
}

View File

@ -0,0 +1,10 @@
package ru.myitschool.work.domain.user
import ru.myitschool.work.domain.entities.UserEntity
class GetUserByLoginUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(login: String): Result<UserEntity> = repository.getUserByLogin(login)
}

View File

@ -5,4 +5,5 @@ import ru.myitschool.work.domain.entities.UserEntity
interface UserRepository {
suspend fun getCurrentUser(): UserEntity
suspend fun getUserByLogin(login: String) : Result<UserEntity>
}

View File

@ -0,0 +1,27 @@
package ru.myitschool.work.ui
import android.os.Bundle
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentNoInternetNotificationBinding
import ru.myitschool.work.utils.isOnline
class NoInternetNotificationFragment: BottomSheetDialogFragment(R.layout.fragment_no_internet_notification) {
private var _binding: FragmentNoInternetNotificationBinding? = null
private val binding: FragmentNoInternetNotificationBinding get() = _binding!!
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentNoInternetNotificationBinding.bind(view)
binding.close.setOnClickListener {
if (isOnline(requireActivity())) dismiss()
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@ -4,13 +4,18 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import ru.myitschool.work.R
import ru.myitschool.work.utils.isOnline
// НЕ ИЗМЕНЯЙТЕ НАЗВАНИЕ КЛАССА!
@AndroidEntryPoint
class RootActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_root)
if (!isOnline(this)) {
val dialog = NoInternetNotificationFragment()
dialog.isCancelable = false
dialog.show(supportFragmentManager, "NO_INTERNET")
}
}
}

View File

@ -0,0 +1,41 @@
package ru.myitschool.work.ui.admin.search
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentFindEmployeeBinding
import ru.myitschool.work.utils.collectWithLifecycle
class AdminFragment : Fragment(R.layout.fragment_find_employee) {
private var _binding: FragmentFindEmployeeBinding? = null
private val binding: FragmentFindEmployeeBinding get() = _binding!!
private val viewModel by viewModels<AdminViewModel> { AdminViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentFindEmployeeBinding.bind(view)
viewModel.state.collectWithLifecycle(this) { state ->
binding.username.isEnabled = state !is AdminViewModel.State.Loading
when (state) {
is AdminViewModel.State.Error -> binding.error.text = state.errorMessage
is AdminViewModel.State.Loading -> Unit
is AdminViewModel.State.Show -> findNavController().navigate(R.id.action_adminFragment_to_viewUserAsAdminFragment)
is AdminViewModel.State.Waiting -> Unit
}
}
binding.find.setOnClickListener {
viewModel.onFind(binding.username.text.toString())
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@ -0,0 +1,139 @@
package ru.myitschool.work.ui.admin.search
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.PagingData
import androidx.paging.cachedIn
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.AdminRepositoryImpl
import ru.myitschool.work.data.PassRepositoryImpl
import ru.myitschool.work.data.UserRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource
import ru.myitschool.work.data.network.AdminNetworkDataSource
import ru.myitschool.work.data.network.PassNetworkDataSource
import ru.myitschool.work.data.network.UserNetworkDataSource
import ru.myitschool.work.domain.admin.BlockUserUseCase
import ru.myitschool.work.domain.admin.UnblockUserUseCase
import ru.myitschool.work.domain.entities.PassEntity
import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.domain.passes.GetUsersPassesUseCase
import ru.myitschool.work.domain.user.GetUserByLoginUseCase
import ru.myitschool.work.ui.admin.view.UsersPassesPagingSource
class AdminViewModel(
private val getUserByLoginUseCase: GetUserByLoginUseCase,
private val getUsersPassesUseCase: GetUsersPassesUseCase,
private val unBlockUserUseCase: UnblockUserUseCase,
private val blockUserUseCase: BlockUserUseCase
) : ViewModel() {
private val _state = MutableStateFlow<State>(State.Waiting)
val state = _state.asStateFlow()
// private var _listState: Flow<PagingData<PassEntity>>? = null
// val listState: Flow<PagingData<PassEntity>> get() = _listState!!
private var currentLogin: String = "pivanov"
fun onFind(login: String) {
viewModelScope.launch {
_state.emit(State.Loading)
getUserByLoginUseCase(login).fold(
onSuccess = { data ->
currentLogin = login
_state.emit(State.Show(data))
setUpPager(login)
},
onFailure = { _state.emit(State.Error(it.message.toString())) }
)
}
}
fun onRefresh() {
updateState()
}
private fun updateState() {
viewModelScope.launch {
_state.emit(State.Loading)
getUserByLoginUseCase(currentLogin!!).fold(
onSuccess = { _state.emit(State.Show(it)) },
onFailure = { _state.emit(State.Error(it.message.toString())) }
)
}
}
private fun setUpPager(login: String) {
// _listState = Pager(
// config = PagingConfig(
// pageSize = 10,
// enablePlaceholders = false,
// maxSize = 50
// )
// ) {
// UsersPassesPagingSource(getUsersPassesUseCase::invoke, login)
// }.flow
// .cachedIn(viewModelScope)
}
fun onBlock() {
viewModelScope.launch {
blockUserUseCase(currentLogin!!)
}
}
fun unblock() {
viewModelScope.launch {
unBlockUserUseCase(currentLogin!!)
}
}
sealed interface State {
data class Show(val user: UserEntity) : State
data object Waiting : State
data object Loading : State
data class Error(val errorMessage: String) : State
}
companion object {
val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
val adminRepository = AdminRepositoryImpl(
networkDataSource = AdminNetworkDataSource,
localCredentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
return AdminViewModel(
getUserByLoginUseCase = GetUserByLoginUseCase(
repository = UserRepositoryImpl(
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance(),
userLocalDataSource = UserLocalDataSource,
networkDataSource = UserNetworkDataSource
)
),
getUsersPassesUseCase = GetUsersPassesUseCase(
repository = PassRepositoryImpl(
networkDataSource = PassNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
),
blockUserUseCase = BlockUserUseCase(
repository = adminRepository
),
unBlockUserUseCase = UnblockUserUseCase(
repository = adminRepository
)
) as T
}
}
}
}

View File

@ -0,0 +1,31 @@
package ru.myitschool.work.ui.admin.view
import androidx.paging.PagingSource
import androidx.paging.PagingState
import ru.myitschool.work.domain.entities.PassEntity
class UsersPassesPagingSource(
private val request: suspend (pageNum: Int, pageSize: Int, login: String) -> Result<List<PassEntity>>,
private val login: String
) : PagingSource<Int, PassEntity>() {
override fun getRefreshKey(state: PagingState<Int, PassEntity>): 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, PassEntity> {
val pageNum = params.key ?: 0
return request.invoke(pageNum, params.loadSize, login).fold(
onSuccess = { value ->
LoadResult.Page(
data = value,
prevKey = (pageNum - 1).takeIf { it >= 0 },
nextKey = (pageNum + 1).takeIf { value.size == params.loadSize }
)
},
onFailure = { error -> LoadResult.Error(error) }
)
}
}

View File

@ -0,0 +1,90 @@
package ru.myitschool.work.ui.admin.view
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.paging.LoadState
import com.squareup.picasso.Picasso
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentUserBinding
import ru.myitschool.work.ui.admin.search.AdminViewModel
import ru.myitschool.work.ui.profile.PassesListAdapter
import ru.myitschool.work.utils.collectWithLifecycle
import ru.myitschool.work.utils.visibleOrGone
class ViewUserAsAdminFragment : Fragment(R.layout.fragment_user) {
private var _binding: FragmentUserBinding? = null
private val binding: FragmentUserBinding get() = _binding!!
private val viewModel by viewModels<AdminViewModel> { AdminViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentUserBinding.bind(view)
binding.findUser.visibleOrGone(false)
binding.logout.visibleOrGone(false)
viewModel.state.collectWithLifecycle(this) { state ->
binding.refresh.isRefreshing = state is AdminViewModel.State.Loading
binding.content.visibleOrGone(state is AdminViewModel.State.Show)
when (state) {
is AdminViewModel.State.Loading -> Unit
is AdminViewModel.State.Show -> {
val user = state.user
binding.block.visibleOrGone(!user.isCardBlocked)
binding.unblock.visibleOrGone(user.isCardBlocked)
binding.fullname.text = user.name
binding.position.text = user.position
binding.lastEntry.text = user.lastVisit
Picasso.get().load(user.photoUrl).into(binding.photo)
}
is AdminViewModel.State.Error -> binding.error.text = state.errorMessage
is AdminViewModel.State.Waiting -> Unit
}
}
// viewModel.listState.collectWithLifecycle(this) { listState ->
// adapter.submitData(listState)
// }
// adapter.loadStateFlow.collectWithLifecycle(this) { data ->
// val dataState = data.refresh
// binding.refresh.isRefreshing = dataState is LoadState.Loading
// binding.error.visibleOrGone(dataState is LoadState.Error)
//
// if (dataState is LoadState.Error) {
// binding.error.text = dataState.error.toString()
// }
// }
binding.refresh.setOnRefreshListener {
viewModel.onRefresh()
// adapter.refresh()
}
binding.block.setOnClickListener {
AlertDialog.Builder(requireContext())
.setTitle("Блокировка доступа")
.setMessage("Вы уверены, что хотите заблокировать доступ работнику: ${binding.fullname}?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.onBlock() }
.show()
}
binding.unblock.setOnClickListener {
viewModel.unblock()
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@ -1,8 +1,9 @@
package ru.myitschool.work.ui.login
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
@ -32,11 +33,18 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
binding.error.visibleOrGone(state is LoginViewModel.State.Error)
when (state) {
is LoginViewModel.State.Error -> binding.error.text = state.errorMessage
is LoginViewModel.State.Loading -> Unit
is LoginViewModel.State.Error -> {
binding.error.text = state.errorMessage
setButtonActive()
}
is LoginViewModel.State.Loading -> setButtonInactive()
is LoginViewModel.State.Waiting -> Unit
is LoginViewModel.State.LoginCheckCompleted -> binding.login.isEnabled =
state.isCompleted
is LoginViewModel.State.LoginCheckCompleted -> {
state.isCompleted.let {
if (it) setButtonActive() else setButtonInactive()
binding.login.isEnabled = it
}
}
}
}
@ -57,6 +65,18 @@ class LoginFragment : Fragment(R.layout.fragment_login) {
}
}
private fun setButtonInactive() {
binding.login.setTextColor(Color.BLACK)
binding.login.background =
ContextCompat.getDrawable(requireContext(), R.drawable.inactive_button)
}
private fun setButtonActive() {
binding.login.setTextColor(Color.WHITE)
binding.login.background =
ContextCompat.getDrawable(requireContext(), R.drawable.main_button)
}
override fun onDestroy() {
_binding = null
super.onDestroy()

View File

@ -9,14 +9,10 @@ import androidx.navigation.fragment.findNavController
import ru.myitschool.work.R
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.databinding.FragmentSplashBinding
import ru.myitschool.work.utils.collectWithLifecycle
class SplashFragment : Fragment(R.layout.fragment_splash) {
private var _binding: FragmentSplashBinding? = null
private val binding: FragmentSplashBinding get() = _binding!!
private val viewModel by viewModels<LoginViewModel> { LoginViewModel.Factory }
override fun onCreate(savedInstanceState: Bundle?) {
@ -30,21 +26,15 @@ class SplashFragment : Fragment(R.layout.fragment_splash) {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentSplashBinding.bind(view)
viewModel.action.collectWithLifecycle(this) { action ->
val navController = findNavController()
when (action) {
is LoginViewModel.Action.GoToLogin -> navController.navigate(R.id.loginFragment)
is LoginViewModel.Action.OpenApp ->
navController.navigate(R.id.action_loginFragment_to_userFragment)
navController.navigate(R.id.action_splashFragment_to_userFragment)
}
}
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@ -0,0 +1,45 @@
package ru.myitschool.work.ui.profile
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import ru.myitschool.work.databinding.PassItemBinding
import ru.myitschool.work.domain.entities.PassEntity
class PassesListAdapter:
ListAdapter<PassEntity, PassesListAdapter.ViewHolder>(CenterDiff) {
class ViewHolder(
private val binding: PassItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: PassEntity) {
binding.time.text = item.time
binding.name.text = item.name
binding.type.text = item.type
}
}
object CenterDiff : DiffUtil.ItemCallback<PassEntity>() {
override fun areContentsTheSame(oldItem: PassEntity, newItem: PassEntity): Boolean {
return oldItem.name == newItem.name
}
override fun areItemsTheSame(oldItem: PassEntity, newItem: PassEntity): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
PassItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
}

View File

@ -0,0 +1,31 @@
package ru.myitschool.work.ui.profile
import android.util.Log
import androidx.paging.PagingSource
import androidx.paging.PagingState
import ru.myitschool.work.domain.entities.PassEntity
class PassesPagingSource(
private val request: suspend (pageNum: Int, pageSize: Int) -> Result<List<PassEntity>>
) : PagingSource<Int, PassEntity>() {
override fun getRefreshKey(state: PagingState<Int, PassEntity>): 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, PassEntity> {
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 = { error -> LoadResult.Error(error) }
)
}
}

View File

@ -1,17 +1,21 @@
package ru.myitschool.work.ui.profile
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.squareup.picasso.Picasso
import ru.myitschool.work.R
import ru.myitschool.work.databinding.FragmentUserBinding
import ru.myitschool.work.ui.qr.result.RESPONSE_KEY
import ru.myitschool.work.ui.qr.scan.QrScanDestination
import ru.myitschool.work.utils.collectWithLifecycle
import ru.myitschool.work.utils.visibleOrGone
class UserFragment : Fragment(R.layout.fragment_user) {
private var _binding: FragmentUserBinding? = null
@ -22,30 +26,59 @@ class UserFragment : Fragment(R.layout.fragment_user) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentUserBinding.bind(view)
val adapter = PassesListAdapter()
binding.passes.adapter = adapter
viewModel.state.collectWithLifecycle(this) { state ->
(binding.refresh as SwipeRefreshLayout).isRefreshing =
state is UserViewModel.State.Loading
binding.content?.visibleOrGone(state is UserViewModel.State.Show)
binding.refresh.isRefreshing = state is UserViewModel.State.Loading
binding.content.visibleOrGone(state is UserViewModel.State.Show)
when (state) {
is UserViewModel.State.Loading -> Unit
is UserViewModel.State.Show -> {
val user = state.userEntity
adapter.submitList(state.passes)
binding.scan.visibleOrGone(!user.isCardBlocked)
binding.findUser.visibleOrGone(user.isAdmin)
binding.fullname.text = user.name
binding.position.text = user.position
binding.lastEntry.text = user.lastVisit
Picasso.get().load(user.photoUrl).into(binding.photo)
}
}
(binding.refresh as SwipeRefreshLayout).setOnRefreshListener {
viewModel.onRefresh()
}
binding.logout.setOnClickListener {
viewModel.onLogout()
findNavController().navigate(R.id.action_userFragment_to_loginFragment)
}
}
binding.refresh.setOnRefreshListener {
viewModel.onRefresh()
}
binding.logout.setOnClickListener {
AlertDialog.Builder(requireContext())
.setTitle("Выход")
.setMessage("Вы уверены, что хотите выйти из аккаунта?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.onLogout()
findNavController().navigate(R.id.action_userFragment_to_loginFragment)
}
.show()
}
binding.findUser.setOnClickListener {
findNavController().navigate(R.id.action_userFragment_to_adminFragment)
}
binding.scan.setOnClickListener {
findNavController().navigate(R.id.action_userFragment_to_qrScanFragment)
}
parentFragmentManager.setFragmentResultListener(
QrScanDestination.REQUEST_KEY, this
) { _, result ->
parentFragmentManager.setFragmentResult(RESPONSE_KEY, result)
findNavController().navigate(R.id.action_userFragment_to_qrResultFragment)
}
}
override fun onDestroy() {

View File

@ -1,35 +1,63 @@
package ru.myitschool.work.ui.profile
import android.util.Log
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.PassRepositoryImpl
import ru.myitschool.work.data.UserRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource
import ru.myitschool.work.data.network.PassNetworkDataSource
import ru.myitschool.work.data.network.UserNetworkDataSource
import ru.myitschool.work.domain.entities.PassEntity
import ru.myitschool.work.domain.entities.UserEntity
import ru.myitschool.work.domain.login.LogoutUseCase
import ru.myitschool.work.domain.passes.GetCurrentPassesUseCase
import ru.myitschool.work.domain.user.GetCurrentUserUseCase
class UserViewModel(
private val getCurrentUserUseCase: GetCurrentUserUseCase,
private val logoutUseCase: LogoutUseCase
private val logoutUseCase: LogoutUseCase,
private val getCurrentPassesUseCase: GetCurrentPassesUseCase
) : ViewModel() {
private val _state = MutableStateFlow<State>(State.Loading)
val state = _state.asStateFlow()
val listState = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
maxSize = 50
)
) {
PassesPagingSource(getCurrentPassesUseCase::invoke)
}.flow
.cachedIn(viewModelScope)
init {
updateState()
}
private fun updateState() {
viewModelScope.launch {
State.Show(getCurrentUserUseCase())
_state.emit(State.Loading)
_state.emit(
State.Show(
getCurrentUserUseCase(),
getCurrentPassesUseCase(0, 30).getOrNull()!!
)
)
}
}
@ -43,7 +71,7 @@ class UserViewModel(
sealed interface State {
data object Loading : State
data class Show(val userEntity: UserEntity) : State
data class Show(val userEntity: UserEntity, val passes: List<PassEntity>) : State
}
companion object {
@ -57,7 +85,13 @@ class UserViewModel(
)
return UserViewModel(
getCurrentUserUseCase = GetCurrentUserUseCase(repository = repository),
logoutUseCase = LogoutUseCase(repository = repository)
logoutUseCase = LogoutUseCase(repository = repository),
getCurrentPassesUseCase = GetCurrentPassesUseCase(
repository = PassRepositoryImpl(
networkDataSource = PassNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
)
) as T
}
}

View File

@ -8,7 +8,7 @@ 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.ui.qr.scan.QrScanDestination
import ru.myitschool.work.domain.entities.QrEntity
import ru.myitschool.work.ui.qr.scan.QrScanDestination.getDataIfExist
import ru.myitschool.work.utils.collectWithLifecycle
@ -20,25 +20,18 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
private var _binding: FragmentQrResultBinding? = null
private val binding: FragmentQrResultBinding get() = _binding!!
private var _resultQr: String? = null
private val resultQr: String = _resultQr!!
private val viewModel by viewModels<QrResultViewModel> { QrResultViewModel.Factory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
_binding = FragmentQrResultBinding.bind(view)
if (savedInstanceState != null) {
_resultQr = savedInstanceState.getString(QrScanDestination.REQUEST_KEY)
}
parentFragmentManager.setFragmentResultListener(RESPONSE_KEY, this) { _, result ->
_resultQr = getDataIfExist(result)
viewModel.update(resultQr)
getDataIfExist(result)?.let { viewModel.setQr(QrEntity(it)) }
viewModel.update()
}
viewModel.state.collectWithLifecycle(this) { state ->
if (_resultQr == null) {
if (viewModel._qrEntity == null) {
binding.result.setText(R.string.door_closed)
binding.close.background =
ContextCompat.getDrawable(requireContext(), R.drawable.warn_button)
@ -65,12 +58,6 @@ class QrResultFragment : Fragment(R.layout.fragment_qr_result) {
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(QrScanDestination.REQUEST_KEY, resultQr)
}
override fun onDestroy() {
_binding = null
super.onDestroy()

View File

@ -7,7 +7,7 @@ import androidx.lifecycle.viewmodel.CreationExtras
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.data.QrNetworkDataSource
import ru.myitschool.work.data.network.QrNetworkDataSource
import ru.myitschool.work.data.QrRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.domain.entities.QrEntity
@ -20,9 +20,16 @@ class QrResultViewModel(
private val _state = MutableStateFlow<State>(State.Loading)
val state = _state.asStateFlow()
fun update(qrValue: String) {
var _qrEntity: QrEntity? = null
private val qrEntity: QrEntity get() = _qrEntity!!
fun setQr(qrEntity: QrEntity) {
_qrEntity = qrEntity
}
fun update() {
viewModelScope.launch {
pushQrUseCase(QrEntity(code = qrValue)).fold(
pushQrUseCase(qrEntity).fold(
onSuccess = { _state.emit(State.Show) },
onFailure = { _state.emit(State.Error(it.message.toString())) }
)

View File

@ -0,0 +1,22 @@
package ru.myitschool.work.utils
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
fun isOnline(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val capabilities =
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
if (capabilities != null) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
return true
}
}
return false
}

View File

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

View File

@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@ -1,30 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="221"
android:viewportHeight="221">
<group>
<clip-path
android:pathData="M0,0h221v221h-221z"/>
<path
android:pathData="M0,0h221v221h-221z"
android:fillColor="#001A8C"/>
<path
android:pathData="M66.3,0.4V19.58H39.06L39.06,81.83H66.3L66.3,101H-7.2V81.83H19.89L19.89,19.58H-7.2V0.4H66.3ZM100.84,77.79V19.58H92.92L92.92,101H73.75L73.75,23.61C73.75,10.73 84.08,0.4 96.96,0.4C103.09,0.4 108.99,2.81 113.26,7.24C117.61,11.5 120.01,17.48 120.01,23.61V81.83H128.09V0.4H147.26V77.79C147.26,90.68 136.78,101 124.05,101C117.92,101 112.02,98.59 107.67,94.17C103.4,89.9 100.84,83.92 100.84,77.79ZM177.99,73.91C165.11,73.91 154.71,63.43 154.71,50.62V0.4H173.88V54.74H177.68C183.89,54.74 189.79,57.14 194.21,61.49C198.56,65.92 200.97,71.82 200.97,78.02V81.83H209.04V0.4H228.21V77.79C228.21,90.68 217.73,101 205,101C192.27,101 181.8,90.68 181.8,77.79V73.91H177.99ZM-51.58,175.74H-20.45V167.67H-51.58C-64.46,167.67 -74.79,157.34 -74.79,144.61C-74.79,131.73 -64.46,121.4 -51.58,121.4H25.81L25.81,140.58H-55.61V148.49H-24.49C-11.76,148.49 -1.28,158.97 -1.28,171.7C-1.28,184.43 -11.76,194.91 -24.49,194.91H-55.61V202.83H25.81V222H-51.58C-64.46,222 -74.79,211.68 -74.79,198.79C-74.79,186.06 -64.46,175.74 -51.58,175.74ZM52.45,140.58V222H33.27V144.69C33.27,131.81 43.68,121.4 56.56,121.4H83.49C96.3,121.4 106.78,131.81 106.78,144.69V171.63C106.78,184.43 96.3,194.91 83.49,194.91H79.54V202.83H106.78V222H83.57C77.44,222 71.54,219.59 67.19,215.17C62.93,210.9 60.36,204.92 60.36,198.79C60.36,186.06 70.84,175.74 83.57,175.74H87.61V140.58H52.45ZM187.74,198.79C187.74,204.92 185.17,210.9 180.9,215.17C176.56,219.59 170.66,222 164.53,222H114.23V202.83H168.56V194.91H164.6C158.47,194.91 152.57,192.35 148.23,188C143.88,183.66 141.32,177.76 141.32,171.63V167.67H137.44C131.31,167.67 125.33,165.26 121.06,160.91C116.64,156.64 114.23,150.67 114.23,144.61C114.23,138.48 116.64,132.51 121.06,128.24C125.33,123.81 131.31,121.4 137.44,121.4H187.74V140.58H133.4V148.49H137.21C143.41,148.49 149.31,151.06 153.74,155.4C158.09,159.75 160.49,165.65 160.49,171.78V175.74H164.53C170.66,175.74 176.56,178.15 180.9,182.49C185.17,186.76 187.74,192.74 187.74,198.79ZM218.39,175.74H249.52V167.67H218.39C205.51,167.67 195.18,157.34 195.18,144.61C195.18,131.73 205.51,121.4 218.39,121.4H295.78V140.58H214.36V148.49H245.48C258.21,148.49 268.69,158.97 268.69,171.7C268.69,184.43 258.21,194.91 245.48,194.91H214.36V202.83H295.78V222H218.39C205.51,222 195.18,211.68 195.18,198.79C195.18,186.06 205.51,175.74 218.39,175.74Z"
android:strokeAlpha="0.3"
android:fillColor="#ffffff"
android:fillAlpha="0.3"/>
<path
android:pathData="M109.12,53C141.65,53 168,78.74 168,110.5C168,142.26 141.64,168 109.12,168C90.77,168 73.8,159.74 62.72,145.88C62.41,145.5 62.18,145.05 62.04,144.58C61.91,144.1 61.87,143.6 61.93,143.11C61.99,142.61 62.15,142.14 62.4,141.71C62.65,141.28 62.98,140.9 63.38,140.61C64.19,139.99 65.2,139.71 66.21,139.84C67.22,139.96 68.14,140.47 68.78,141.26C73.63,147.28 79.78,152.14 86.76,155.47C93.75,158.8 101.39,160.51 109.13,160.48C137.39,160.48 160.3,138.1 160.3,110.5C160.3,82.9 137.39,60.52 109.13,60.52C101.5,60.49 93.96,62.15 87.06,65.39C80.15,68.63 74.05,73.36 69.2,79.24C68.55,80.02 67.62,80.52 66.61,80.63C65.6,80.73 64.58,80.44 63.78,79.82C63.39,79.51 63.06,79.14 62.82,78.7C62.57,78.27 62.42,77.79 62.36,77.3C62.31,76.81 62.35,76.31 62.49,75.83C62.64,75.35 62.87,74.91 63.19,74.53C74.29,61.02 91.03,53 109.12,53ZM112.26,92.46L127.82,108.02C129.33,109.53 129.36,111.93 127.89,113.4L112.68,128.61C112.33,128.96 111.9,129.24 111.44,129.43C110.98,129.62 110.48,129.71 109.98,129.7C109.48,129.7 108.98,129.59 108.52,129.39C108.06,129.2 107.65,128.91 107.3,128.55C106.94,128.2 106.65,127.78 106.45,127.32C106.26,126.86 106.15,126.37 106.15,125.87C106.14,125.37 106.24,124.87 106.42,124.4C106.61,123.94 106.89,123.52 107.24,123.16L116.14,114.26L56.85,114.27C56.35,114.27 55.85,114.18 55.39,114C54.92,113.81 54.5,113.54 54.14,113.18C53.78,112.83 53.5,112.42 53.3,111.96C53.11,111.5 53,111 53,110.5C53,108.42 54.72,106.74 56.85,106.74H115.89L106.93,97.78C106.58,97.43 106.29,97.01 106.09,96.55C105.89,96.09 105.79,95.59 105.78,95.09C105.78,94.59 105.87,94.1 106.06,93.63C106.25,93.17 106.53,92.74 106.88,92.39C107.23,92.04 107.66,91.76 108.12,91.58C108.58,91.39 109.08,91.3 109.58,91.3C110.08,91.31 110.57,91.41 111.03,91.61C111.49,91.81 111.9,92.09 112.25,92.45"
android:fillColor="#ffffff"/>
<path
android:pathData="M112.26,92.46L127.82,108.02C129.33,109.53 129.36,111.93 127.89,113.4L112.68,128.61C112.33,128.96 111.9,129.24 111.44,129.43C110.98,129.62 110.48,129.71 109.98,129.7C109.48,129.7 108.98,129.59 108.52,129.39C108.06,129.2 107.65,128.91 107.3,128.55C106.94,128.2 106.65,127.78 106.45,127.32C106.26,126.86 106.15,126.37 106.15,125.87C106.14,125.37 106.24,124.87 106.42,124.4C106.61,123.94 106.89,123.52 107.24,123.16L116.14,114.26L56.85,114.27C56.35,114.27 55.85,114.18 55.39,114C54.92,113.81 54.5,113.54 54.14,113.18C53.78,112.83 53.5,112.42 53.3,111.96C53.11,111.5 53,111 53,110.5C53,108.42 54.72,106.74 56.85,106.74H115.89L106.93,97.78C106.58,97.43 106.29,97.01 106.09,96.55C105.89,96.09 105.79,95.59 105.78,95.09C105.78,94.59 105.87,94.1 106.06,93.63C106.25,93.17 106.53,92.74 106.88,92.39C107.23,92.04 107.66,91.76 108.12,91.58C108.58,91.39 109.08,91.3 109.58,91.3C110.08,91.31 110.57,91.41 111.03,91.61C111.49,91.81 111.9,92.09 112.25,92.45M109.12,53C141.65,53 168,78.74 168,110.5C168,142.26 141.64,168 109.12,168C90.77,168 73.8,159.74 62.72,145.88C62.41,145.5 62.18,145.05 62.04,144.58C61.91,144.1 61.87,143.6 61.93,143.11C61.99,142.61 62.15,142.14 62.4,141.71C62.65,141.28 62.98,140.9 63.38,140.61C64.19,139.99 65.2,139.71 66.21,139.84C67.22,139.96 68.14,140.47 68.78,141.26C73.63,147.28 79.78,152.14 86.76,155.47C93.75,158.8 101.39,160.51 109.13,160.48C137.39,160.48 160.3,138.1 160.3,110.5C160.3,82.9 137.39,60.52 109.13,60.52C101.5,60.49 93.96,62.15 87.06,65.39C80.15,68.63 74.05,73.36 69.2,79.24C68.55,80.02 67.62,80.52 66.61,80.63C65.6,80.73 64.58,80.44 63.78,79.82C63.39,79.51 63.06,79.14 62.82,78.7C62.57,78.27 62.42,77.79 62.36,77.3C62.31,76.81 62.35,76.31 62.49,75.83C62.64,75.35 62.87,74.91 63.19,74.53C74.29,61.02 91.03,53 109.12,53Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="375dp"
android:height="655dp"
android:viewportWidth="375"
android:viewportHeight="655">
<path
android:pathData="M102.93,0.51V37.2H50.8L50.8,156.31H102.93V193H-37.72V156.31H14.11L14.11,37.2H-37.72V0.51L102.93,0.51ZM169.02,148.59L169.02,37.2H153.87L153.87,193H117.18L117.18,44.92C117.18,20.27 136.94,0.51 161.59,0.51C173.32,0.51 184.61,5.12 192.78,13.58C201.1,21.75 205.7,33.19 205.7,44.92L205.7,156.31H221.15V0.51H257.84V148.59C257.84,173.25 237.79,193 213.43,193C201.69,193 190.41,188.4 182.09,179.93C173.92,171.76 169.02,160.32 169.02,148.59ZM316.65,141.16C291.99,141.16 272.09,121.11 272.09,96.61V0.51L308.77,0.51V104.48H316.05C327.93,104.48 339.22,109.08 347.69,117.4C356.01,125.87 360.61,137.15 360.61,149.04V156.31H376.06V0.51H412.74V148.59C412.74,173.25 392.69,193 368.33,193C343.98,193 323.92,173.25 323.92,148.59V141.16H316.65ZM58.2,335.48H117.76V320.03H58.2C33.55,320.03 13.8,300.28 13.8,275.92C13.8,251.26 33.55,231.51 58.2,231.51H206.28V268.2H50.48V283.35H110.04C134.4,283.35 154.45,303.4 154.45,327.76C154.45,352.11 134.4,372.17 110.04,372.17H50.48V387.31H206.28V424H58.2C33.55,424 13.8,404.25 13.8,379.59C13.8,355.23 33.55,335.48 58.2,335.48ZM257.26,268.2V424H220.57V276.07C220.57,251.41 240.47,231.51 265.13,231.51H316.67C341.17,231.51 361.22,251.41 361.22,276.07V327.61C361.22,352.11 341.17,372.17 316.67,372.17H309.09V387.31H361.22V424H316.81C305.08,424 293.79,419.4 285.48,410.93C277.31,402.76 272.4,391.33 272.4,379.59C272.4,355.23 292.46,335.48 316.81,335.48H324.54V268.2H257.26ZM154.45,610.59C154.45,622.33 149.55,633.76 141.38,641.93C133.06,650.4 121.77,655 110.04,655H13.8L13.8,618.31H117.76V603.16H110.19C98.45,603.16 87.17,598.26 78.85,589.95C70.53,581.63 65.63,570.34 65.63,558.61V551.03H58.2C46.47,551.03 35.03,546.43 26.87,538.11C18.4,529.94 13.8,518.51 13.8,506.92C13.8,495.19 18.4,483.75 26.87,475.58C35.03,467.12 46.47,462.51 58.2,462.51H154.45L154.45,499.2H50.48V514.35H57.76C69.64,514.35 80.93,519.25 89.39,527.57C97.71,535.88 102.32,547.17 102.32,558.9V566.48H110.04C121.77,566.48 133.06,571.08 141.38,579.4C149.55,587.57 154.45,599.01 154.45,610.59ZM213.11,566.48H272.67V551.03H213.11C188.46,551.03 168.7,531.28 168.7,506.92C168.7,482.27 188.46,462.51 213.11,462.51H361.19V499.2H205.39V514.35H264.95C289.3,514.35 309.36,534.4 309.36,558.76C309.36,583.11 289.3,603.16 264.95,603.16H205.39V618.31H361.19V655H213.11C188.46,655 168.7,635.25 168.7,610.59C168.7,586.23 188.46,566.48 213.11,566.48Z"
android:strokeAlpha="0.3"
android:fillColor="#ffffff"
android:fillAlpha="0.3"/>
</vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="117dp"
android:height="117dp"
android:viewportWidth="117"
android:viewportHeight="117">
<path
android:pathData="M57.12,1C89.65,1 116,26.74 116,58.5C116,90.26 89.64,116 57.12,116C38.77,116 21.8,107.74 10.72,93.89C10.41,93.5 10.18,93.05 10.04,92.58C9.91,92.1 9.87,91.6 9.93,91.11C9.99,90.61 10.15,90.14 10.4,89.71C10.65,89.28 10.98,88.91 11.38,88.61C12.19,87.99 13.2,87.72 14.21,87.84C15.22,87.96 16.14,88.47 16.78,89.26C21.63,95.28 27.78,100.14 34.76,103.47C41.75,106.8 49.39,108.51 57.13,108.48C85.39,108.48 108.3,86.1 108.3,58.5C108.3,30.9 85.39,8.52 57.13,8.52C49.5,8.49 41.96,10.15 35.06,13.39C28.15,16.63 22.05,21.36 17.2,27.24C16.55,28.02 15.62,28.52 14.61,28.63C13.6,28.73 12.58,28.44 11.78,27.82C11.39,27.51 11.06,27.14 10.82,26.7C10.57,26.27 10.42,25.79 10.36,25.3C10.31,24.81 10.35,24.31 10.49,23.83C10.64,23.35 10.87,22.91 11.19,22.53C22.29,9.02 39.03,1 57.12,1ZM60.26,40.46L75.82,56.02C77.33,57.53 77.36,59.93 75.89,61.4L60.69,76.61C60.33,76.96 59.91,77.24 59.44,77.43C58.98,77.62 58.48,77.71 57.98,77.7C57.48,77.7 56.98,77.59 56.52,77.39C56.06,77.2 55.65,76.91 55.3,76.55C54.94,76.2 54.65,75.78 54.45,75.32C54.26,74.86 54.15,74.37 54.15,73.87C54.14,73.37 54.23,72.87 54.42,72.41C54.61,71.94 54.89,71.52 55.24,71.16L64.14,62.26L4.85,62.27C4.35,62.27 3.85,62.18 3.39,62C2.92,61.81 2.5,61.54 2.14,61.19C1.78,60.84 1.5,60.42 1.3,59.96C1.11,59.5 1,59 1,58.5C1,56.42 2.72,54.74 4.85,54.74H63.89L54.94,45.78C54.58,45.43 54.29,45.01 54.09,44.55C53.89,44.09 53.79,43.59 53.78,43.09C53.78,42.59 53.87,42.1 54.06,41.63C54.25,41.17 54.53,40.74 54.88,40.39C55.23,40.04 55.66,39.76 56.12,39.58C56.58,39.39 57.08,39.3 57.58,39.3C58.08,39.31 58.57,39.41 59.03,39.61C59.49,39.81 59.91,40.09 60.25,40.45"
android:fillColor="#ffffff"/>
<path
android:pathData="M60.26,40.46L75.82,56.02C77.33,57.53 77.36,59.93 75.89,61.4L60.69,76.61C60.33,76.96 59.91,77.24 59.44,77.43C58.98,77.62 58.48,77.71 57.98,77.7C57.48,77.7 56.98,77.59 56.52,77.39C56.06,77.2 55.65,76.91 55.3,76.55C54.94,76.2 54.65,75.78 54.45,75.32C54.26,74.86 54.15,74.37 54.15,73.87C54.14,73.37 54.23,72.87 54.42,72.41C54.61,71.94 54.89,71.52 55.24,71.16L64.14,62.26L4.85,62.27C4.35,62.27 3.85,62.18 3.39,62C2.92,61.81 2.5,61.54 2.14,61.19C1.78,60.84 1.5,60.42 1.3,59.96C1.11,59.5 1,59 1,58.5C1,56.42 2.72,54.74 4.85,54.74H63.89L54.94,45.78C54.58,45.43 54.29,45.01 54.09,44.55C53.89,44.09 53.79,43.59 53.78,43.09C53.78,42.59 53.87,42.1 54.06,41.63C54.25,41.17 54.53,40.74 54.88,40.39C55.23,40.04 55.66,39.76 56.12,39.58C56.58,39.39 57.08,39.3 57.58,39.3C58.08,39.31 58.57,39.41 59.03,39.61C59.49,39.81 59.91,40.09 60.25,40.45M57.12,1C89.65,1 116,26.74 116,58.5C116,90.26 89.64,116 57.12,116C38.77,116 21.8,107.74 10.72,93.89C10.41,93.5 10.18,93.05 10.04,92.58C9.91,92.1 9.87,91.6 9.93,91.11C9.99,90.61 10.15,90.14 10.4,89.71C10.65,89.28 10.98,88.91 11.38,88.61C12.19,87.99 13.2,87.72 14.21,87.84C15.22,87.96 16.14,88.47 16.78,89.26C21.63,95.28 27.78,100.14 34.76,103.47C41.75,106.8 49.39,108.51 57.13,108.48C85.39,108.48 108.3,86.1 108.3,58.5C108.3,30.9 85.39,8.52 57.13,8.52C49.5,8.49 41.96,10.15 35.06,13.39C28.15,16.63 22.05,21.36 17.2,27.24C16.55,28.02 15.62,28.52 14.61,28.63C13.6,28.73 12.58,28.44 11.78,27.82C11.39,27.51 11.06,27.14 10.82,26.7C10.57,26.27 10.42,25.79 10.36,25.3C10.31,24.81 10.35,24.31 10.49,23.83C10.64,23.35 10.87,22.91 11.19,22.53C22.29,9.02 39.03,1 57.12,1Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</vector>

View File

@ -14,6 +14,7 @@
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="20dp"
@ -132,7 +133,21 @@
android:background="@drawable/warn_button_outline"
android:foreground="?attr/selectableItemBackground"
android:text="@string/block_card_button_text"
android:textColor="@color/warn_button_color" />
android:textColor="@color/warn_button_color"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/unblock"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/main_button_outline"
android:foreground="?attr/selectableItemBackground"
android:text="@string/unblock_card_button_text"
android:textColor="@color/main_button_color"
android:visibility="gone" />
<ImageButton
android:id="@+id/logout"

View File

@ -5,19 +5,19 @@
android:background="@color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="24dp"
android:gravity="center"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:orientation="vertical">
<TextView
android:layout_gravity="start"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:fontFamily="@font/manrope_bold"
android:text="@string/find_employee_header"
android:text="@string/find_employee_button_text"
android:textSize="24sp" />
<EditText
@ -41,6 +41,16 @@
android:foreground="?attr/selectableItemBackground"
android:text="@string/find_button_text"
android:textColor="@color/white" />
<TextView
android:id="@+id/error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/error"
android:textAlignment="center"
android:textColor="@color/warn_button_color"
android:visibility="gone" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8" ?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="20dp"
android:gravity="center_horizontal"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@drawable/no_wifi_pic" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/offline_error"
android:textFontWeight="700"
android:textSize="24sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/no_internet_instructions"
android:textFontWeight="400"
android:textSize="18sp" />
<Button
android:foreground="?attr/selectableItemBackground"
android:textColor="@color/white"
android:layout_marginTop="16dp"
android:id="@+id/close"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/to_main_menu"
android:background="@drawable/main_button" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
android:id="@+id/result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24dp"
android:textSize="24sp"
tools:text="Успешно!"/>
<Button

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent" >
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,9 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/warn_button_color"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Some error" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh"
android:layout_width="match_parent"
@ -95,15 +107,6 @@
android:foreground="?attr/selectableItemBackground"
android:text="@string/scan_qr_text"
android:textColor="@color/white" />
<TextView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/warn_button_color"
android:visibility="gone"
tools:text="Some error" />
</LinearLayout>
<LinearLayout
@ -116,17 +119,31 @@
android:id="@+id/block"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/warn_button_outline"
android:foreground="?attr/selectableItemBackground"
android:text="@string/block_card_button_text"
android:textColor="@color/warn_button_color" />
android:textColor="@color/warn_button_color"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/unblock"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:background="@drawable/main_button_outline"
android:foreground="?attr/selectableItemBackground"
android:text="@string/unblock_card_button_text"
android:textColor="@color/main_button_color"
android:visibility="gone" />
<ImageButton
android:id="@+id/logout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:background="@drawable/warn_button"
android:padding="12dp"
android:src="@drawable/exit" />
@ -143,6 +160,7 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/passes"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -22,6 +22,9 @@
<action
android:id="@+id/action_userFragment_to_qrScanFragment"
app:destination="@id/qrScanFragment" />
<action
android:id="@+id/action_userFragment_to_adminFragment"
app:destination="@id/adminFragment" />
</fragment>
<fragment
android:id="@+id/qrScanFragment"
@ -62,5 +65,24 @@
<action
android:id="@+id/action_splashFragment_to_loginFragment"
app:destination="@id/loginFragment" />
<action
android:id="@+id/action_splashFragment_to_userFragment"
app:destination="@id/userFragment"
app:popUpTo="@id/nav_graph"
app:popUpToInclusive="true" />
</fragment>
<fragment
android:id="@+id/adminFragment"
android:name="ru.myitschool.work.ui.admin.search.AdminFragment"
android:label="AdminFragment"
tools:layout="@layout/fragment_find_employee">
<action
android:id="@+id/action_adminFragment_to_viewUserAsAdminFragment"
app:destination="@id/viewUserAsAdminFragment" />
</fragment>
<fragment
android:id="@+id/viewUserAsAdminFragment"
android:name="ru.myitschool.work.ui.admin.view.ViewUserAsAdminFragment"
android:label="ViewUserAsAdminFragment"
tools:layout="@layout/fragment_user" />
</navigation>

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NTO Pass</string>
<string name="app_name">QR Pass</string>
<string name="scan_qr_text">Scan QR</string>
<string name="login_hint">Enter login</string>
<string name="welcome_text">Welcome to NTO Pass app</string>
<string name="welcome_text">Welcome to\nQR Pass app</string>
<string name="login">Login</string>
<string name="refresh_data">Refresh</string>
<string name="to_main_menu">Close</string>
@ -13,4 +13,13 @@
<string name="close_button">Close</string>
<string name="logout_text">Logout</string>
<string name="last_visit">"Last visit:"</string>
<string name="password_input_hint">Password</string>
<string name="block_card_button_text">Block card</string>
<string name="card_using_header">Pass using</string>
<string name="find_employee_button_text">Find employee</string>
<string name="find_button_text">Find</string>
<string name="offline_error">Seems you are offline...</string>
<string name="no_internet_instructions">Check your internet connection\\nand try again</string>
<string name="reload_text">" Reload"</string>
<string name="unblock_card_button_text">Unblock card</string>
</resources>

View File

@ -1,8 +1,8 @@
<resources>
<string name="app_name">NTO Pass</string>
<string name="app_name">QR Pass</string>
<string name="scan_qr_text">Сканировать QR-код</string>
<string name="login_hint">Введите логин</string>
<string name="welcome_text">Добро пожаловать в NTO Pass</string>
<string name="welcome_text">Добро пожаловать в\nQR Pass</string>
<string name="login">Войти</string>
<string name="refresh_data">Обновить данные</string>
<string name="to_main_menu">Закрыть</string>
@ -12,10 +12,12 @@
<string name="logout_text">Выйти из аккаунта</string>
<string name="last_visit">Последний вход: </string>
<string name="password_input_hint">Введите пароль</string>
<string name="register_process_button_text">Зарегестрироваться</string>
<string name="block_card_button_text">Заблокировать карту</string>
<string name="card_using_header">Использование пропуска</string>
<string name="find_employee_button_text">Поиск сотрудника</string>
<string name="find_button_text">Найти</string>
<string name="find_employee_header">Поиск сотрудника</string>
<string name="offline_error">Кажется вы оффлайн...</string>
<string name="no_internet_instructions">Проверьте подключение к сети\nи попробуйте еще раз</string>
<string name="reload_text">Перезагрузить</string>
<string name="unblock_card_button_text">Разблокировать карту</string>
</resources>

View File

@ -0,0 +1,49 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.AdminRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.AdminNetworkDataSource
import ru.myitschool.work.domain.admin.AdminRepository
@RunWith(RobolectricTestRunner::class)
class AdminRepositoryImplTest {
private lateinit var context: Context
private lateinit var repository: AdminRepository
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.TOKEN_KEY,
Context.MODE_PRIVATE
)
)
repository = AdminRepositoryImpl(
networkDataSource = AdminNetworkDataSource,
localCredentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
}
@Test
fun `When user have admin permissions and blocked user exist process` () = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertNull(repository.blockUser("").exceptionOrNull())
}
@Test
fun `When user don not have admin permissions error` () = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNotNull(repository.blockUser("").exceptionOrNull())
}
}

View File

@ -0,0 +1,68 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.PassRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.network.PassNetworkDataSource
import ru.myitschool.work.domain.entities.PassEntity
@RunWith(RobolectricTestRunner::class)
class PassesRepositoryImplTest {
private lateinit var repository: PassRepositoryImpl
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.TOKEN_KEY,
Context.MODE_PRIVATE
)
)
repository = PassRepositoryImpl(
networkDataSource = PassNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
}
@Test
fun `When user authorized as user gets only its passes`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertEquals(
listOf(
PassEntity("", "", ""),
PassEntity("", "", ""),
PassEntity("", "", "")
),
repository.getCurrentPasses(1, 3).getOrNull()
)
}
@Test
fun `When user authorized as admin gets any user's passes`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertEquals(
listOf(
PassEntity("", "", ""),
PassEntity("", "", ""),
PassEntity("", "", "")
),
repository.getUsersPasses(1, 3, "").getOrNull()
)
}
@Test
fun `When user tries to access admin method error` () = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNull(repository.getUsersPasses(1, 3, ""))
}
}

View File

@ -0,0 +1,55 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.network.QrNetworkDataSource
import ru.myitschool.work.data.QrRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.domain.entities.QrEntity
@RunWith(RobolectricTestRunner::class)
class QrRepositoryImplTest {
private lateinit var context: Context
private lateinit var repository: QrRepositoryImpl
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.SERVER_ADDRESS,
Context.MODE_PRIVATE
)
)
repository = QrRepositoryImpl(
networkDataSource = QrNetworkDataSource,
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance()
)
}
@Test
fun `When QR valid and user logged process`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNull(repository.pushQr(QrEntity("")).exceptionOrNull())
}
@Test
fun `When QR invalid and user logged error`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNotNull(repository.pushQr(QrEntity("*")).exceptionOrNull())
}
@Test
fun `When QR valid and user not logged error`() = runTest {
runCatching {
repository.pushQr(QrEntity("*")).exceptionOrNull()
}.let { assert(it.isFailure) }
}
}

View File

@ -0,0 +1,130 @@
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.UserRepositoryImpl
import ru.myitschool.work.data.local.CredentialsLocalDataSource
import ru.myitschool.work.data.local.UserLocalDataSource
import ru.myitschool.work.data.network.UserNetworkDataSource
import ru.myitschool.work.domain.entities.UserEntity
@RunWith(RobolectricTestRunner::class)
class UserRepositoryImplTest {
private lateinit var context: Context
private lateinit var repository: UserRepositoryImpl
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
CredentialsLocalDataSource.buildSource(
context.getSharedPreferences(
Constants.TOKEN_KEY,
Context.MODE_PRIVATE
)
)
repository = UserRepositoryImpl(
credentialsLocalDataSource = CredentialsLocalDataSource.getInstance(),
userLocalDataSource = UserLocalDataSource,
networkDataSource = UserNetworkDataSource
)
}
@Test
fun `When user exist return true`() = runTest {
assertEquals(true, repository.isUserExist("pivanov").getOrNull())
}
@Test
fun `When user does not exist return false`() = runTest {
assertEquals(false, repository.isUserExist("pivanov").getOrNull())
}
@Test
fun `When user existing and password matches success login`() = runTest {
assertEquals(
UserEntity(
isAdmin = true,
name = "",
lastVisit = "",
photoUrl = "",
position = "",
isCardBlocked = false
), repository.login("pivanov", "admin").getOrNull()
)
}
@Test
fun `When user enter incorrect login`() = runTest {
assertNull(repository.login("***", "admin").getOrNull())
}
@Test
fun `When user enter incorrect password`() = runTest {
assertNull(repository.login("pivanov", "***").getOrNull())
}
@Test
fun `When user enter incorrect login and password`() = runTest {
assertNull(repository.login("***", "***").getOrNull())
}
@Test
fun `When user logouts local credentials wipes`() = runTest {
repository.logout()
runCatching {
CredentialsLocalDataSource.getInstance().getToken()
}.let { assert(it.isFailure) }
}
@Test
fun `Get user by login returns correct user`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertEquals(
UserEntity(
isAdmin = false,
name = "",
lastVisit = "",
photoUrl = "",
position = "",
isCardBlocked = false
),
repository.getUserByLogin("").getOrNull()
)
}
@Test
fun `Get user by login works with admin roots`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("pivanov", "admin")
assertNotNull(repository.getUserByLogin("").getOrNull())
}
@Test
fun `Get user by login don not works without admin roots`() = runTest {
CredentialsLocalDataSource.getInstance().updateToken("", "")
assertNull(repository.getUserByLogin("").getOrNull())
}
@Test
fun `When user successfully logged for the first time it will authorize automatically`() =
runTest {
val loggedUser = repository.login("pivanov", "admin").getOrNull()
assertEquals(
loggedUser,
repository.authorize(CredentialsLocalDataSource.getInstance().getToken())
)
}
@Test
fun `When user successfully logged its info saved locally for current session`() = runTest {
val loggedUser = repository.login("pivanov", "admin").getOrNull()
assertEquals(loggedUser, repository.getCurrentUser())
}
}