new_select_screen #9
3
README.MD
Normal file
3
README.MD
Normal file
@ -0,0 +1,3 @@
|
||||
penpot deck: https://pp.sicampus.ru/#/workspace?team-id=14a6b474-d5fa-807f-8007-9f0741c1b7ad&project-id=14a6b474-d5fa-807f-8007-9f0741c1b7ae&file-id=8e3f3321-7df1-807a-8007-9f6a7201af74&page-id=50b061ab-362d-8056-8007-9d94752aa99f&layout=layers&board-id=6fa8b836-315d-80ad-8007-9f6ddff01da2
|
||||
login: anna
|
||||
password: qwerty1!
|
||||
BIN
S-APP-MINIPIGS.penpot
Normal file
BIN
S-APP-MINIPIGS.penpot
Normal file
Binary file not shown.
@ -17,8 +17,9 @@
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".ui.root.RootActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:windowSoftInputMode="adjustNothing"
|
||||
android:label="@string/title_activity_root">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package ru.myitschool.work.core
|
||||
|
||||
object Constants {
|
||||
const val HOST = "http://localhost:8090"
|
||||
const val AUTH_URL = "/auth"
|
||||
const val HOST = "http://10.0.0.12:49165"
|
||||
const val AUTH_URL = "/login"
|
||||
const val INFO_URL = "/info"
|
||||
const val BOOKING_URL = "/booking"
|
||||
const val BOOK_URL = "/book"
|
||||
const val ROOM_BOOKING_URL = "/room/booking"
|
||||
const val AUTH_DELAY = 60
|
||||
}
|
||||
58
app/src/main/java/ru/myitschool/work/data/AESEncryption.kt
Normal file
58
app/src/main/java/ru/myitschool/work/data/AESEncryption.kt
Normal file
@ -0,0 +1,58 @@
|
||||
package ru.myitschool.work.data
|
||||
|
||||
import android.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKeyFactory
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.PBEKeySpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
|
||||
object AESEncyption {
|
||||
|
||||
const val secretKey = "tK5Ugskdkipokuodvknfdk3434weofnf="
|
||||
const val salt = "QLlGNHNhYTJTQWZ2bGhpV3U="
|
||||
const val iv = "bVQqNFNhRkQ1Njc4UUFaPA=="
|
||||
|
||||
fun encrypt(strToEncrypt: String): String? {
|
||||
try {
|
||||
val ivParameterSpec = IvParameterSpec(Base64.decode(iv, Base64.DEFAULT))
|
||||
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val spec =
|
||||
PBEKeySpec(secretKey.toCharArray(), Base64.decode(salt, Base64.DEFAULT), 10000, 256)
|
||||
val tmp = factory.generateSecret(spec)
|
||||
val secretKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec)
|
||||
return Base64.encodeToString(
|
||||
cipher.doFinal(strToEncrypt.toByteArray(Charsets.UTF_8)),
|
||||
Base64.DEFAULT
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
println("Error while encrypting: $e")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun decrypt(strToDecrypt: String?): String? {
|
||||
try {
|
||||
|
||||
val ivParameterSpec = IvParameterSpec(Base64.decode(iv, Base64.DEFAULT))
|
||||
|
||||
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val spec =
|
||||
PBEKeySpec(secretKey.toCharArray(), Base64.decode(salt, Base64.DEFAULT), 10000, 256)
|
||||
val tmp = factory.generateSecret(spec)
|
||||
val secretKey = SecretKeySpec(tmp.encoded, "AES")
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec)
|
||||
return String(cipher.doFinal(Base64.decode(strToDecrypt, Base64.DEFAULT)))
|
||||
} catch (e: Exception) {
|
||||
println("Error while decrypting: $e")
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package ru.myitschool.work.data.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AuthRequestDto(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("password")
|
||||
val password: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthResponseDto(
|
||||
@SerialName("token")
|
||||
val token: String,
|
||||
@SerialName("expired")
|
||||
val expired: Int,
|
||||
@SerialName("role")
|
||||
val role: String
|
||||
)
|
||||
@ -8,5 +8,5 @@ data class BookRequestDto(
|
||||
@SerialName("date")
|
||||
val date: String,
|
||||
@SerialName("placeId")
|
||||
val placeId: String,
|
||||
val placeId: Int,
|
||||
)
|
||||
@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.data.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RoomBookingRequestDto(
|
||||
@SerialName("roomId")
|
||||
val roomId: Int,
|
||||
@SerialName("date")
|
||||
val date: String,
|
||||
)
|
||||
@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.data.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RoomBookingsDto(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("data")
|
||||
val data: Map<String, String?>
|
||||
)
|
||||
@ -8,41 +8,72 @@ import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import ru.myitschool.work.App
|
||||
import ru.myitschool.work.data.AESEncyption
|
||||
import ru.myitschool.work.data.dto.AuthRequestDto
|
||||
import ru.myitschool.work.data.dto.AuthResponseDto
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object AuthRepository {
|
||||
private const val STORE = "AUTH-STORE"
|
||||
private const val CODE_KEY = "CODE"
|
||||
private const val TOKEN_KEY = "TOKEN"
|
||||
private const val ROLE_KEY = "ROLE"
|
||||
|
||||
private var codeCache: String? = null
|
||||
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
if (success) {
|
||||
codeCache = text
|
||||
private var tokenCache: String? = null
|
||||
private var roleCache: String? = null
|
||||
|
||||
|
||||
suspend fun checkAndSave(login: String, password: String): Result<AuthResponseDto> {
|
||||
val data = AuthRequestDto(login=login, password=password)
|
||||
return NetworkDataSource.checkAuth(data).onSuccess { success ->
|
||||
val encryptedTokenCache = AESEncyption.encrypt(success.token)
|
||||
tokenCache = encryptedTokenCache
|
||||
if (encryptedTokenCache != null) {
|
||||
App.context.userDataStore.edit { preferences ->
|
||||
val prefKey = stringPreferencesKey(CODE_KEY)
|
||||
preferences[prefKey] = text
|
||||
val prefKey = stringPreferencesKey(TOKEN_KEY)
|
||||
preferences[prefKey] = encryptedTokenCache
|
||||
}
|
||||
App.context.userDataStore.edit { preferences ->
|
||||
val prefKey = stringPreferencesKey(ROLE_KEY)
|
||||
preferences[prefKey] = success.role
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCode(): String? {
|
||||
if (codeCache == null) {
|
||||
codeCache = App.context.userDataStore.data
|
||||
suspend fun getToken(): String? {
|
||||
if (tokenCache == null) {
|
||||
tokenCache = App.context.userDataStore.data
|
||||
.firstOrNull()
|
||||
?.let { preferences ->
|
||||
preferences[stringPreferencesKey(CODE_KEY)]
|
||||
preferences[stringPreferencesKey(TOKEN_KEY)]
|
||||
}
|
||||
}
|
||||
return codeCache
|
||||
if (tokenCache != null) {
|
||||
return AESEncyption.decrypt(tokenCache)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun getRole(): String? {
|
||||
if (roleCache == null) {
|
||||
roleCache = App.context.userDataStore.data
|
||||
.firstOrNull()
|
||||
?.let { preferences ->
|
||||
preferences[stringPreferencesKey(ROLE_KEY)]
|
||||
}
|
||||
}
|
||||
return roleCache
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
codeCache = null
|
||||
tokenCache = null
|
||||
App.context.userDataStore.edit { preferences ->
|
||||
val prefKey = stringPreferencesKey(CODE_KEY)
|
||||
val prefKey = stringPreferencesKey(TOKEN_KEY)
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
App.context.userDataStore.edit { preferences ->
|
||||
val prefKey = stringPreferencesKey(ROLE_KEY)
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,8 @@ class BookRepository(
|
||||
private val authRepository: AuthRepository
|
||||
) {
|
||||
suspend fun getInfo(): Result<MainInfoEntity> {
|
||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getInfo(code).mapCatching { dto ->
|
||||
val token = authRepository.getToken() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getInfo(token).mapCatching { dto ->
|
||||
MainInfoEntity(
|
||||
name = dto.name ?: error("Name is null"),
|
||||
photoUrl = dto.photoUrl ?: error("Photo url is null"),
|
||||
@ -26,8 +26,8 @@ class BookRepository(
|
||||
}
|
||||
|
||||
suspend fun getBookingInfo(): Result<List<BookingData>> {
|
||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getBooking(code).mapCatching { dto ->
|
||||
val token = authRepository.getToken() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getBooking(token).mapCatching { dto ->
|
||||
dto?.map { (date, places) ->
|
||||
BookingData(
|
||||
date = date,
|
||||
@ -43,9 +43,9 @@ class BookRepository(
|
||||
}
|
||||
|
||||
suspend fun sendBook(data: BookRequestData): Result<Boolean> {
|
||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
||||
val token = authRepository.getToken() ?: return getNoAuthResult()
|
||||
val dto = BookRequestDto(data.date, data.placeId)
|
||||
return NetworkDataSource.addBook(code, dto)
|
||||
return NetworkDataSource.addBook(token, dto)
|
||||
}
|
||||
private fun <T> getNoAuthResult() = Result.failure<T>(
|
||||
IllegalStateException("No auth")
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import ru.myitschool.work.data.dto.BookRequestDto
|
||||
import ru.myitschool.work.data.dto.RoomBookingRequestDto
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
import ru.myitschool.work.domain.room.entities.RoomEntity
|
||||
|
||||
class RoomBookingRepository(
|
||||
private val authRepository: AuthRepository
|
||||
) {
|
||||
suspend fun getRoomBookings(roomId: Int): Result<RoomEntity> {
|
||||
val token = authRepository.getToken() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getRoomBookings(token, roomId = roomId).mapCatching { dto ->
|
||||
RoomEntity(name=dto.name, data=dto.data)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteRoomBooking(roomId: Int, date: String): Result<Boolean> {
|
||||
val token = authRepository.getToken() ?: return getNoAuthResult()
|
||||
val data = RoomBookingRequestDto(roomId = roomId, date = date)
|
||||
return NetworkDataSource.deleteRoomBooking(token, data)
|
||||
}
|
||||
|
||||
suspend fun sendRoomBooking(roomId: Int, date: String): Result<Boolean> {
|
||||
val token = authRepository.getToken() ?: return getNoAuthResult()
|
||||
val dto = BookRequestDto(date, roomId)
|
||||
return NetworkDataSource.addBook(token, dto)
|
||||
}
|
||||
private fun <T> getNoAuthResult() = Result.failure<T>(
|
||||
IllegalStateException("No auth")
|
||||
)
|
||||
}
|
||||
@ -4,7 +4,9 @@ import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.delete
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.headers
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
@ -16,9 +18,15 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.myitschool.work.core.Constants
|
||||
import ru.myitschool.work.data.dto.AuthRequestDto
|
||||
import ru.myitschool.work.data.dto.AuthResponseDto
|
||||
import ru.myitschool.work.data.dto.PlaceDto
|
||||
import ru.myitschool.work.data.dto.BookRequestDto
|
||||
import ru.myitschool.work.data.dto.RoomBookingRequestDto
|
||||
import ru.myitschool.work.data.dto.RoomBookingsDto
|
||||
import ru.myitschool.work.data.dto.UserDto
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.room.entities.RoomEntity
|
||||
|
||||
object NetworkDataSource {
|
||||
private val client by lazy {
|
||||
@ -36,54 +44,116 @@ object NetworkDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
suspend fun checkAuth(data: AuthRequestDto): Result<AuthResponseDto> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
||||
val response = client.post(getUrl(Constants.AUTH_URL)) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(data)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> true
|
||||
else -> false
|
||||
HttpStatusCode.OK -> response.body<AuthResponseDto>()
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
||||
suspend fun getInfo(token: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
println("!!!!!!!!!!!!!! getInfo $code")
|
||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||
println("!!!!!!!!!!!!!! getInfo")
|
||||
val response = client.get(getUrl(Constants.INFO_URL)) {
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
||||
response.body<UserDto>()
|
||||
} else {
|
||||
if (response.status == HttpStatusCode.Unauthorized) {
|
||||
AuthRepository.logout()
|
||||
}
|
||||
println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
|
||||
suspend fun getBooking(token: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||
val response = client.get(getUrl(Constants.BOOKING_URL)) {
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
response.body<Map<String, List<PlaceDto>>>()
|
||||
} else {
|
||||
if (response.status == HttpStatusCode.Unauthorized) {
|
||||
AuthRepository.logout()
|
||||
}
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
suspend fun addBook(token: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||
val response = client.post(getUrl(Constants.BOOK_URL)) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(data)
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.Created -> true
|
||||
HttpStatusCode.Conflict -> false
|
||||
else -> error(response.bodyAsText())
|
||||
else -> {
|
||||
if (response.status == HttpStatusCode.Unauthorized) {
|
||||
AuthRepository.logout()
|
||||
}
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
||||
suspend fun getRoomBookings(token: String, roomId: Int): Result<RoomBookingsDto> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl("$Constants.ROOM_BOOKING_URL/$roomId")) {
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
response.body<RoomBookingsDto>()
|
||||
} else {
|
||||
if (response.status == HttpStatusCode.Unauthorized) {
|
||||
AuthRepository.logout()
|
||||
}
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteRoomBooking(token: String, data: RoomBookingRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.delete(getUrl(Constants.ROOM_BOOKING_URL)) {
|
||||
headers {
|
||||
append("Authorization", "Bearer $token")
|
||||
}
|
||||
setBody(data)
|
||||
}
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
true
|
||||
} else {
|
||||
if (response.status == HttpStatusCode.Unauthorized) {
|
||||
AuthRepository.logout()
|
||||
}
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl(targetUrl: String) = "${Constants.HOST}/api$targetUrl"
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class CheckAndSaveAuthCodeUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
text: String
|
||||
): Result<Unit> {
|
||||
return repository.checkAndSave(text).mapCatching { success ->
|
||||
if (!success) error("Code is incorrect")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.dto.AuthResponseDto
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class CheckAndSaveAuthTokenUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
login: String,
|
||||
password: String
|
||||
): Result<AuthResponseDto> {
|
||||
return repository.checkAndSave(login, password)
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
class CheckCodeFormatUseCase {
|
||||
operator fun invoke(
|
||||
text: String
|
||||
): Boolean {
|
||||
return text.length == 4 && text.all { char ->
|
||||
char.isLetterOrDigit() &&
|
||||
((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
class CheckCredsFormatUseCase {
|
||||
operator fun invoke(
|
||||
login: String, password: String
|
||||
): Boolean {
|
||||
val passwordList = password.toList()
|
||||
var passwordCorrect = true
|
||||
for (i in 1..(passwordList.size - 3)) {
|
||||
val s = StringBuilder().append(passwordList[i]).append(passwordList[i+1]).append(passwordList[i+2]).toString()
|
||||
if (login.contains(s)) {
|
||||
passwordCorrect = false
|
||||
}
|
||||
}
|
||||
return login.isNotEmpty() && password.length >= 8 && passwordCorrect && login.all { char ->
|
||||
char.isLetterOrDigit() && ((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
|
||||
} && password.all({ char -> password.count { it == char } < 3 }) && password.count { it == '!' } + password.count { it == '@' } + password.count { it == '#' } + password.count { it == '"' } + password.count { it == '№' } + password.count { it == ';' } + password.count { it == '$' } + password.count { it == '%' } + password.count { it == '^' } + password.count { it == ':' } + password.count { it == '&' } + password.count { it == '?' } + password.count { it == '*' } >= 1
|
||||
}
|
||||
}
|
||||
@ -2,10 +2,10 @@ package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class GetCodeUseCase(
|
||||
class GetTokenUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(): String? {
|
||||
return repository.getCode()
|
||||
return repository.getToken()
|
||||
}
|
||||
}
|
||||
@ -2,5 +2,5 @@ package ru.myitschool.work.domain.book.entities
|
||||
|
||||
data class BookRequestData(
|
||||
val date: String,
|
||||
val placeId: String
|
||||
val placeId: Int
|
||||
)
|
||||
@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.room
|
||||
|
||||
import ru.myitschool.work.data.repo.RoomBookingRepository
|
||||
|
||||
class DeleteRoomBookingsUseCase(
|
||||
private val repository: RoomBookingRepository
|
||||
) {
|
||||
suspend operator fun invoke(roomId: Int, date: String): Result<Boolean> {
|
||||
return repository.deleteRoomBooking(roomId = roomId, date = date)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.room
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class GetRoleUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(): String? {
|
||||
return repository.getRole()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.room
|
||||
|
||||
import ru.myitschool.work.data.repo.RoomBookingRepository
|
||||
import ru.myitschool.work.domain.room.entities.RoomEntity
|
||||
|
||||
class GetRoomBookingsUseCase(
|
||||
private val repository: RoomBookingRepository
|
||||
) {
|
||||
suspend operator fun invoke(roomId: Int): Result<RoomEntity> {
|
||||
return repository.getRoomBookings(roomId = roomId)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package ru.myitschool.work.domain.room
|
||||
|
||||
import ru.myitschool.work.data.repo.RoomBookingRepository
|
||||
|
||||
class SendRoomBookingRequestUseCase(
|
||||
private val repository: RoomBookingRepository
|
||||
) {
|
||||
suspend operator fun invoke(roomId: Int, date: String): Result<Unit> {
|
||||
return repository.sendRoomBooking(roomId, date).mapCatching { success ->
|
||||
if (!success) error("Book error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.domain.room.entities
|
||||
|
||||
|
||||
class RoomEntity (
|
||||
val name: String,
|
||||
val data: Map<String, String?>
|
||||
)
|
||||
@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class RoomScreenDestination(val roomId: Int): AppDestination
|
||||
@ -1,6 +1,7 @@
|
||||
package ru.myitschool.work.ui.root
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager.LayoutParams
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
@ -15,6 +16,8 @@ class RootActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
actionBar?.hide()
|
||||
window.setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE)
|
||||
setContent {
|
||||
WorkTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
|
||||
@ -10,19 +10,24 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.delay
|
||||
import androidx.navigation.navArgument
|
||||
import androidx.navigation.toRoute
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.GetCodeUseCase
|
||||
import ru.myitschool.work.domain.auth.GetTokenUseCase
|
||||
import ru.myitschool.work.domain.room.GetRoleUseCase
|
||||
import ru.myitschool.work.ui.nav.AppDestination
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
import ru.myitschool.work.ui.nav.RoomScreenDestination
|
||||
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
||||
import ru.myitschool.work.ui.screen.book.BookScreen
|
||||
import ru.myitschool.work.ui.screen.main.MainScreen
|
||||
import ru.myitschool.work.ui.screen.room.RoomScreen
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(
|
||||
@ -31,12 +36,15 @@ fun AppNavHost(
|
||||
) {
|
||||
var destination by remember { mutableStateOf<AppDestination?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
val code = GetCodeUseCase(AuthRepository).invoke()
|
||||
destination = if (code == null) {
|
||||
val code = GetTokenUseCase(AuthRepository).invoke()
|
||||
val role = GetRoleUseCase(AuthRepository).invoke()
|
||||
destination = (if (code == null) {
|
||||
AuthScreenDestination
|
||||
} else if (role == "ROOM") {
|
||||
RoomScreenDestination
|
||||
} else {
|
||||
MainScreenDestination
|
||||
}
|
||||
}) as AppDestination?
|
||||
}
|
||||
if (destination != null) {
|
||||
NavHost(
|
||||
@ -55,6 +63,13 @@ fun AppNavHost(
|
||||
composable<BookScreenDestination> {
|
||||
BookScreen(navController = navController)
|
||||
}
|
||||
composable<RoomScreenDestination> { backStackEntry ->
|
||||
val room: RoomScreenDestination = backStackEntry.toRoute()
|
||||
RoomScreen(
|
||||
roomId = room.roomId,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
sealed interface AuthIntent {
|
||||
data class Send(val text: String): AuthIntent
|
||||
data class TextInput(val text: String): AuthIntent
|
||||
data class Send(val login: String, val password: String): AuthIntent
|
||||
data class TextInput(val login: String, val password: String): AuthIntent
|
||||
}
|
||||
@ -1,17 +1,33 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.imeNestedScroll
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.isImeVisible
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@ -21,8 +37,8 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -31,12 +47,14 @@ import androidx.navigation.NavController
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AuthScreen(
|
||||
viewModel: AuthViewModel = viewModel(),
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
val isIme = WindowInsets.isImeVisible
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.actionFlow.collect { action ->
|
||||
@ -48,20 +66,19 @@ fun AuthScreen(
|
||||
}
|
||||
}
|
||||
|
||||
BoxWithConstraints {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
.padding(horizontal = if(maxWidth < 400.dp) 48.dp else 200.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.imePadding()
|
||||
.imeNestedScroll(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
when (val currentState = state) {
|
||||
is AuthState.Data -> Content(viewModel, currentState)
|
||||
is AuthState.Data -> Content(viewModel, currentState, isIme)
|
||||
is AuthState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
@ -70,39 +87,95 @@ fun AuthScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
viewModel: AuthViewModel,
|
||||
state: AuthState.Data
|
||||
state: AuthState.Data,
|
||||
isIme: Boolean
|
||||
) {
|
||||
var inputText by remember { mutableStateOf("") }
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
TextField(
|
||||
var login by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
|
||||
|
||||
AnimatedVisibility(!isIme ) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.difference),
|
||||
contentDescription = stringResource(R.string.icon_alter),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(184.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title_1),
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title_2),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
||||
value = inputText,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
value = login,
|
||||
onValueChange = {
|
||||
inputText = it
|
||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
||||
login = it
|
||||
viewModel.onIntent(AuthIntent.TextInput(login, password))
|
||||
},
|
||||
label = { Text(stringResource(R.string.auth_label)) }
|
||||
placeholder = { Text(stringResource(R.string.auth_placeholder_login)) },
|
||||
label = { Text(stringResource(R.string.auth_label_login)) }
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||
onClick = {
|
||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
value = password,
|
||||
onValueChange = {
|
||||
password = it
|
||||
viewModel.onIntent(AuthIntent.TextInput(login, password))
|
||||
},
|
||||
enabled = state.isEnabledSend
|
||||
) {
|
||||
Text(stringResource(R.string.auth_sign_in))
|
||||
}
|
||||
placeholder = { Text(stringResource(R.string.auth_placeholder_password)) },
|
||||
label = { Text(stringResource(R.string.auth_label_passord)) }
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
if (state.error != null) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||
modifier = Modifier.testTag(TestIds.Auth.ERROR).padding(16.dp),
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Red,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(modifier = Modifier.size(20.dp))
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||
.fillMaxWidth()
|
||||
.height(64.dp),
|
||||
onClick = {
|
||||
viewModel.onIntent(AuthIntent.Send(login, password))
|
||||
},
|
||||
enabled = state.isEnabledSend,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.auth_sign_in),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@ -9,14 +10,15 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.core.Constants
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase
|
||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthTokenUseCase
|
||||
import ru.myitschool.work.domain.auth.CheckCredsFormatUseCase
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
|
||||
class AuthViewModel : ViewModel() {
|
||||
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() }
|
||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||
private val checkCodeFormatUseCase by lazy { CheckCredsFormatUseCase() }
|
||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthTokenUseCase(AuthRepository) }
|
||||
private val _uiState = MutableStateFlow<AuthState>(
|
||||
AuthState.Data(
|
||||
isEnabledSend = false,
|
||||
@ -28,29 +30,68 @@ class AuthViewModel : ViewModel() {
|
||||
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
||||
|
||||
private var authTries: Int = 0
|
||||
private var timerSecs: Int = 0
|
||||
|
||||
private var login: String = ""
|
||||
private var password: String = ""
|
||||
|
||||
fun onIntent(intent: AuthIntent) {
|
||||
when (intent) {
|
||||
is AuthIntent.Send -> {
|
||||
if (authTries >= 5) {
|
||||
timerSecs = Constants.AUTH_DELAY
|
||||
val timer = object : CountDownTimer((timerSecs * 1000).toLong(), 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
timerSecs -= 1
|
||||
updateStateIfData { oldState ->
|
||||
oldState.copy(
|
||||
isEnabledSend = false,
|
||||
error = "Слишком много попыток входа, попробуйте через $timerSecs секунд"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
authTries = 0
|
||||
updateStateIfData { oldState ->
|
||||
oldState.copy(
|
||||
isEnabledSend = checkCodeFormatUseCase.invoke(
|
||||
login = login,
|
||||
password = password
|
||||
),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.start()
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||
_uiState.update { AuthState.Loading }
|
||||
checkAndSaveAuthCodeUseCase.invoke(intent.login, intent.password).fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
|
||||
},
|
||||
onFailure = { error ->
|
||||
updateStateIfData { oldState ->
|
||||
oldState.copy(
|
||||
error = error.message
|
||||
)
|
||||
}
|
||||
authTries += 1
|
||||
_uiState.update { AuthState.Data(isEnabledSend = false, error = error.message) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is AuthIntent.TextInput -> {
|
||||
login = intent.login
|
||||
password = intent.password
|
||||
updateStateIfData { oldState ->
|
||||
oldState.copy(
|
||||
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text),
|
||||
error = null
|
||||
isEnabledSend = checkCodeFormatUseCase.invoke(
|
||||
login = intent.login,
|
||||
password = intent.password
|
||||
) && authTries <= 5,
|
||||
error = if (authTries >= 5) oldState.error else null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,6 @@ sealed interface BookIntent {
|
||||
data object Refresh: BookIntent
|
||||
data class Add(
|
||||
val date: String,
|
||||
val placeId: String
|
||||
val placeId: Int
|
||||
): BookIntent
|
||||
}
|
||||
@ -170,7 +170,7 @@ private fun ContentState(
|
||||
mutableIntStateOf(startDestination.index)
|
||||
}
|
||||
var selectedPlaceId by rememberSaveable {
|
||||
mutableStateOf<String?>(null)
|
||||
mutableStateOf<Int?>(null)
|
||||
}
|
||||
Box {
|
||||
Column {
|
||||
@ -213,7 +213,7 @@ private fun ContentState(
|
||||
startDestination = startDestination,
|
||||
state = state,
|
||||
onPlaceSelected = { id ->
|
||||
selectedPlaceId = id
|
||||
selectedPlaceId = id.toInt()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.ui.screen.room
|
||||
|
||||
sealed interface CardState {
|
||||
data object Booked: CardState
|
||||
data object Open: CardState
|
||||
data object BookedByMe: CardState
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.ui.screen.room
|
||||
|
||||
sealed interface RoomIntent {
|
||||
data class Refresh(val placeId: Int): RoomIntent
|
||||
data class Booking(val placeId: Int, val date: String): RoomIntent
|
||||
data class UnBook(val placeId: Int, val date: String): RoomIntent
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
package ru.myitschool.work.ui.screen.room
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import ru.myitschool.work.core.TestIds
|
||||
import ru.myitschool.work.ui.screen.auth.AuthViewModel
|
||||
|
||||
@Composable
|
||||
fun RoomScreen(
|
||||
viewModel: RoomViewModel = viewModel(),
|
||||
roomId: Int,
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||
) {
|
||||
when(val currentState = state) {
|
||||
is RoomState.Data -> {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "TEST_LOCATOIN",
|
||||
style = MaterialTheme.typography.displayMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.size(12.dp))
|
||||
LazyColumn {
|
||||
itemsIndexed(currentState.data.entries.toList()) { index, item ->
|
||||
DayCard(item.key, item.value.toString(), index)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Button(
|
||||
onClick = { viewModel.onIntent(RoomIntent.Booking(roomId, currentState.data.keys.toList()[0])) },
|
||||
shape = RoundedCornerShape(64, 0, 0, 64)
|
||||
) {
|
||||
Text("Book")
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.onIntent(RoomIntent.UnBook(roomId, currentState.data.keys.toList()[0])) },
|
||||
shape = RoundedCornerShape(0, 64, 64, 0)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
is RoomState.Error -> {
|
||||
Card(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Auth.ERROR).padding(16.dp),
|
||||
text = currentState.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
)
|
||||
}
|
||||
Button(
|
||||
onClick = { viewModel.onIntent(RoomIntent.Refresh(roomId)) }
|
||||
) {
|
||||
Text("Refresh")
|
||||
}
|
||||
}
|
||||
is RoomState.Loading -> {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DayCard(name: String?, day: String, index: Int) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (name == null) {
|
||||
MaterialTheme.colorScheme.surfaceContainerLowest
|
||||
} else if (name == "hitler") {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.tertiaryContainer
|
||||
}, //REMAKE TO STATE
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(24.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = "${name}",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "${day}",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
Text("${index}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package ru.myitschool.work.ui.screen.room
|
||||
|
||||
import ru.myitschool.work.domain.room.entities.RoomEntity
|
||||
import ru.myitschool.work.ui.screen.main.MainState
|
||||
|
||||
sealed interface RoomState {
|
||||
data object Loading: RoomState
|
||||
data class Error(
|
||||
val error: String
|
||||
): RoomState
|
||||
data class Data(
|
||||
val data: Map<String, String?>,
|
||||
val name: String
|
||||
): RoomState
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
package ru.myitschool.work.ui.screen.room
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.data.repo.RoomBookingRepository
|
||||
import ru.myitschool.work.domain.room.DeleteRoomBookingsUseCase
|
||||
import ru.myitschool.work.domain.room.GetRoomBookingsUseCase
|
||||
import ru.myitschool.work.domain.room.SendRoomBookingRequestUseCase
|
||||
import ru.myitschool.work.ui.nav.RoomScreenDestination
|
||||
|
||||
|
||||
class RoomViewModel(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
): ViewModel() {
|
||||
|
||||
private val roomScreen = savedStateHandle.toRoute<RoomScreenDestination>()
|
||||
|
||||
private val roomBookingRepository = RoomBookingRepository(
|
||||
AuthRepository
|
||||
)
|
||||
private val getRoomBookingsDataUseCase by lazy {
|
||||
GetRoomBookingsUseCase(
|
||||
RoomBookingRepository(
|
||||
AuthRepository
|
||||
)
|
||||
)
|
||||
}
|
||||
private val deleteRoomBookingRequestUseCase by lazy {
|
||||
DeleteRoomBookingsUseCase(
|
||||
roomBookingRepository
|
||||
)
|
||||
}
|
||||
|
||||
private val sendRoomBookingRequestUseCase by lazy {
|
||||
SendRoomBookingRequestUseCase(
|
||||
roomBookingRepository
|
||||
)
|
||||
}
|
||||
private val _uiState = MutableStateFlow<RoomState>(RoomState.Loading)
|
||||
val uiState: StateFlow<RoomState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
refresh(roomScreen.roomId)
|
||||
// Timer().schedule(object : TimerTask() {
|
||||
// override fun run() {
|
||||
// Log.d("mytimer", "A Kiss every 5 seconds")
|
||||
// }
|
||||
// }, 0, 5000)
|
||||
}
|
||||
|
||||
fun onIntent(intent: RoomIntent) {
|
||||
when (intent) {
|
||||
is RoomIntent.Refresh -> {
|
||||
refresh(intent.placeId)
|
||||
}
|
||||
|
||||
is RoomIntent.Booking -> {
|
||||
viewModelScope.launch {
|
||||
sendRoomBookingRequestUseCase.invoke(
|
||||
intent.placeId,
|
||||
intent.date
|
||||
).fold(
|
||||
onSuccess = {
|
||||
refresh(intent.placeId)
|
||||
},
|
||||
onFailure = { error ->
|
||||
RoomState.Error(
|
||||
error = error.message.orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is RoomIntent.UnBook -> {
|
||||
viewModelScope.launch {
|
||||
deleteRoomBookingRequestUseCase.invoke(
|
||||
intent.placeId,
|
||||
intent.date
|
||||
).fold(
|
||||
onSuccess = {
|
||||
refresh(intent.placeId)
|
||||
},
|
||||
onFailure = { error ->
|
||||
RoomState.Error(
|
||||
error = error.message.orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh(roomId: Int) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { RoomState.Loading }
|
||||
_uiState.update {
|
||||
getRoomBookingsDataUseCase.invoke(roomId = roomId).fold(
|
||||
onSuccess = { data ->
|
||||
RoomState.Data(
|
||||
data = data.data,
|
||||
name = data.name
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
RoomState.Error(
|
||||
error = error.message.orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/main/res/drawable/difference.xml
Normal file
9
app/src/main/res/drawable/difference.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="284.03dp"
|
||||
android:height="345.01dp"
|
||||
android:viewportWidth="284.03"
|
||||
android:viewportHeight="345.01">
|
||||
<path
|
||||
android:pathData="M284.03,232.8C281.79,277.45 275.6,316.26 264.81,332.14C255.31,346.12 224.16,350.21 197.88,336.87C171.73,323.6 154,299.01 143,298.26C132,299.01 113.06,323.57 87.11,336.73C60.83,350.07 29.69,345.98 20.19,332C10.6,317.9 4.65,285.69 1.87,247.37C8.57,245.74 26.72,242.23 51,243.01C82,244.01 170,254.01 227,247.01C255.62,243.49 273.4,237.71 284.01,232.82M-0,191.11C0.02,181.74 0.19,172.34 0.5,163.06C16.31,172.82 46.19,184.27 101,187.01C201,192.01 187,216.01 169,224.01C151,232.01 118.71,236.27 78,230.01C47.59,225.33 17.17,210.92 0.04,191.05M7.06,86.76C10.85,62.13 16.09,43.3 22.58,35.36C52.8,-1.59 143,0.01 143,0.01C143,0.01 233.57,0.22 262.42,35.5C267.09,41.21 271.11,52.57 274.41,67.66C253.93,70.28 222.7,71.01 176,66.01C92.79,57.1 35.53,71.91 7.07,86.68M281.46,115.48C282.28,123.97 282.96,132.82 283.5,141.88C272.8,137.26 252.16,131 214,127.01C147,120.01 125.17,117.16 127,98.01C129,77.01 169,76.01 199,82.01C225.31,87.27 259.31,94.84 281.44,115.51"
|
||||
android:fillColor="#6750A4"/>
|
||||
</vector>
|
||||
@ -1,8 +1,12 @@
|
||||
<resources>
|
||||
<string name="app_name">Work</string>
|
||||
<string name="title_activity_root">RootActivity</string>
|
||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
||||
<string name="auth_label">Код</string>
|
||||
<string name="auth_title_1">Войдите</string>
|
||||
<string name="auth_title_2">Используя аккаунт S-App</string>
|
||||
<string name="auth_label_login">Логин</string>
|
||||
<string name="auth_placeholder_login">Введите логин</string>
|
||||
<string name="auth_label_passord">Пароль</string>
|
||||
<string name="auth_placeholder_password">Введите пароль</string>
|
||||
<string name="auth_sign_in">Войти</string>
|
||||
|
||||
<string name="main_refresh">Обновить</string>
|
||||
@ -12,4 +16,6 @@
|
||||
<string name="book_add">Забронировать</string>
|
||||
<string name="book_back">Назад</string>
|
||||
<string name="book_empty">Всё забронировано</string>
|
||||
|
||||
<string name="icon_alter">Иконка</string>
|
||||
</resources>
|
||||
Loading…
x
Reference in New Issue
Block a user