checkpoint 0 #5
2
README.md
Normal file
2
README.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Wind of Win
|
||||||
|
### PenPot: https://pp.sicampus.ru/#/workspace?team-id=14a6b474-d5fa-807f-8007-9f077869482e&file-id=14a6b474-d5fa-807f-8007-9f510e3bd124&page-id=14a6b474-d5fa-807f-8007-9f510e3bd125&layout=assets
|
||||||
@ -34,6 +34,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation("androidx.compose.ui:ui-tooling-preview:1.10.3")
|
||||||
|
implementation("androidx.wear.tiles:tiles-tooling-preview:1.5.0")
|
||||||
|
debugImplementation("androidx.compose.ui:ui-tooling:1.10.3")
|
||||||
defaultComposeLibrary()
|
defaultComposeLibrary()
|
||||||
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
||||||
|
|||||||
@ -8,7 +8,6 @@ class App: Application() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
context = this
|
context = this
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package ru.myitschool.work.core
|
package ru.myitschool.work.core
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
const val HOST = "http://localhost:8090"
|
const val HOST = "http://10.0.0.172:49182" // "http://10.0.0.14:49182" or "http://10.0.2.2:8080"
|
||||||
const val AUTH_URL = "/auth"
|
const val AUTH_URL = "/auth"
|
||||||
const val INFO_URL = "/info"
|
const val INFO_URL = "/info"
|
||||||
const val BOOKING_URL = "/booking"
|
const val BOOKING_URL = "/booking"
|
||||||
|
|||||||
@ -4,7 +4,8 @@ object TestIds {
|
|||||||
object Auth {
|
object Auth {
|
||||||
const val ERROR = "auth_error"
|
const val ERROR = "auth_error"
|
||||||
const val SIGN_BUTTON = "auth_sign_button"
|
const val SIGN_BUTTON = "auth_sign_button"
|
||||||
const val CODE_INPUT = "auth_code_input"
|
const val LOGIN_INPUT = "auth_login_input"
|
||||||
|
const val PASSWORD_INPUT = "auth_password_input"
|
||||||
}
|
}
|
||||||
object Main {
|
object Main {
|
||||||
const val ERROR = "main_error"
|
const val ERROR = "main_error"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package ru.myitschool.work.data.repo
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
@ -12,37 +13,47 @@ import ru.myitschool.work.data.source.NetworkDataSource
|
|||||||
|
|
||||||
object AuthRepository {
|
object AuthRepository {
|
||||||
private const val STORE = "AUTH-STORE"
|
private const val STORE = "AUTH-STORE"
|
||||||
private const val CODE_KEY = "CODE"
|
private const val TOKEN_KEY = "TOKEN"
|
||||||
|
private var tokenCache: String? = null
|
||||||
private var codeCache: String? = null
|
|
||||||
|
|
||||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||||
if (success) {
|
if (success) {
|
||||||
codeCache = text
|
tokenCache = text
|
||||||
App.context.userDataStore.edit { preferences ->
|
App.context.userDataStore.edit { preferences ->
|
||||||
val prefKey = stringPreferencesKey(CODE_KEY)
|
val prefKey = stringPreferencesKey(TOKEN_KEY)
|
||||||
preferences[prefKey] = text
|
preferences[prefKey] = text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCode(): String? {
|
/**
|
||||||
if (codeCache == null) {
|
* Из памяти
|
||||||
codeCache = App.context.userDataStore.data
|
*/
|
||||||
|
suspend fun auth(): String? {
|
||||||
|
if (tokenCache == null) {
|
||||||
|
tokenCache = App.context.userDataStore.data
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.let { preferences ->
|
?.let { preferences ->
|
||||||
preferences[stringPreferencesKey(CODE_KEY)]
|
preferences[stringPreferencesKey(TOKEN_KEY)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return codeCache
|
Log.e("getTokenCache", tokenCache.toString())
|
||||||
|
return tokenCache
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* При обращении к серверу
|
||||||
|
*/
|
||||||
|
suspend fun auth(login : String, password : String) : Result<String> {
|
||||||
|
Log.e("getTokenNDSInAR", NetworkDataSource.auth(login, password).toString())
|
||||||
|
return NetworkDataSource.auth(login, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun logout() {
|
suspend fun logout() {
|
||||||
codeCache = null
|
tokenCache = null
|
||||||
App.context.userDataStore.edit { preferences ->
|
App.context.userDataStore.edit { preferences ->
|
||||||
val prefKey = stringPreferencesKey(CODE_KEY)
|
val prefKey = stringPreferencesKey(TOKEN_KEY)
|
||||||
preferences.remove(prefKey)
|
preferences.remove(prefKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.data.repo
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import ru.myitschool.work.data.dto.BookRequestDto
|
import ru.myitschool.work.data.dto.BookRequestDto
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
import ru.myitschool.work.domain.book.entities.BookRequestData
|
import ru.myitschool.work.domain.book.entities.BookRequestData
|
||||||
@ -10,8 +11,10 @@ class BookRepository(
|
|||||||
private val authRepository: AuthRepository
|
private val authRepository: AuthRepository
|
||||||
) {
|
) {
|
||||||
suspend fun getInfo(): Result<MainInfoEntity> {
|
suspend fun getInfo(): Result<MainInfoEntity> {
|
||||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
val code = authRepository.auth() ?: return getNoAuthResult()
|
||||||
|
Log.e("getInfoCode", "!!!!!!!!!!!!!! getInfo ERROR $code")
|
||||||
return NetworkDataSource.getInfo(code).mapCatching { dto ->
|
return NetworkDataSource.getInfo(code).mapCatching { dto ->
|
||||||
|
Log.e("getInfoCode", "!!!!!!!!!!!!!! getInfo ERROR ${dto.booking}")
|
||||||
MainInfoEntity(
|
MainInfoEntity(
|
||||||
name = dto.name ?: error("Name is null"),
|
name = dto.name ?: error("Name is null"),
|
||||||
photoUrl = dto.photoUrl ?: error("Photo url is null"),
|
photoUrl = dto.photoUrl ?: error("Photo url is null"),
|
||||||
@ -26,7 +29,7 @@ class BookRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getBookingInfo(): Result<List<BookingData>> {
|
suspend fun getBookingInfo(): Result<List<BookingData>> {
|
||||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
val code = authRepository.auth() ?: return getNoAuthResult()
|
||||||
return NetworkDataSource.getBooking(code).mapCatching { dto ->
|
return NetworkDataSource.getBooking(code).mapCatching { dto ->
|
||||||
dto?.map { (date, places) ->
|
dto?.map { (date, places) ->
|
||||||
BookingData(
|
BookingData(
|
||||||
@ -43,7 +46,7 @@ class BookRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendBook(data: BookRequestData): Result<Boolean> {
|
suspend fun sendBook(data: BookRequestData): Result<Boolean> {
|
||||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
val code = authRepository.auth() ?: return getNoAuthResult()
|
||||||
val dto = BookRequestDto(data.date, data.placeId)
|
val dto = BookRequestDto(data.date, data.placeId)
|
||||||
return NetworkDataSource.addBook(code, dto)
|
return NetworkDataSource.addBook(code, dto)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
|
object MeetingsRepository {
|
||||||
|
private var roomCache: String? = null
|
||||||
|
|
||||||
|
fun getRoom(): String? {
|
||||||
|
if (roomCache == null) {
|
||||||
|
NetworkDataSource
|
||||||
|
}
|
||||||
|
return roomCache
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.data.source
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
@ -35,10 +36,32 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
suspend fun auth(login: String, password: String): Result<String> = withContext(Dispatchers.IO) {
|
||||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
val response = client.post("${Constants.HOST}/api${Constants.AUTH_URL}") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(
|
||||||
|
"""{
|
||||||
|
"login" : "$login",
|
||||||
|
"password" : "$password"
|
||||||
|
} """.trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.e("getTokenInNDS", response.body())
|
||||||
|
if (response.status != HttpStatusCode.OK) {
|
||||||
|
Log.e("auth", response.status.toString())
|
||||||
|
throw Exception("Неизвестная ошибка ${response.status}")
|
||||||
|
}
|
||||||
|
else if (response.status == HttpStatusCode.Unauthorized) {
|
||||||
|
throw Exception("Неверный логин или пароль")
|
||||||
|
}
|
||||||
|
response.body<String>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suspend fun checkAuth(token: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
Log.e("token", token)
|
||||||
|
val response = client.get(getUrl(token, Constants.AUTH_URL))
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> true
|
HttpStatusCode.OK -> true
|
||||||
else -> false
|
else -> false
|
||||||
@ -46,23 +69,25 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
suspend fun getInfo(token: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
||||||
return@withContext runCatching {
|
return@withContext runCatching {
|
||||||
println("!!!!!!!!!!!!!! getInfo $code")
|
println("!!!!!!!!!!!!!! getInfo $token")
|
||||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
val response = client.get(getUrl(token, Constants.INFO_URL))
|
||||||
if (response.status == HttpStatusCode.OK) {
|
if (response.status == HttpStatusCode.OK) {
|
||||||
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
||||||
|
Log.d("1", "!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
||||||
response.body<UserDto>()
|
response.body<UserDto>()
|
||||||
} else {
|
} else {
|
||||||
println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
|
println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
|
||||||
|
Log.e("getInfo", "!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
|
||||||
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 {
|
return@withContext runCatching {
|
||||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
val response = client.get(getUrl(token, Constants.BOOKING_URL))
|
||||||
if (response.status == HttpStatusCode.OK) {
|
if (response.status == HttpStatusCode.OK) {
|
||||||
response.body<Map<String, List<PlaceDto>>>()
|
response.body<Map<String, List<PlaceDto>>>()
|
||||||
} else {
|
} else {
|
||||||
@ -71,9 +96,9 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
return@withContext runCatching {
|
||||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
val response = client.post(getUrl(token, Constants.BOOK_URL)) {
|
||||||
contentType(ContentType.Application.Json)
|
contentType(ContentType.Application.Json)
|
||||||
setBody(data)
|
setBody(data)
|
||||||
}
|
}
|
||||||
@ -85,5 +110,5 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
private fun getUrl(token: String, targetUrl: String) = "${Constants.HOST}/api/$targetUrl"
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@ package ru.myitschool.work.domain.auth
|
|||||||
|
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
class CheckAndSaveAuthCodeUseCase(
|
class CheckAndSaveAuthUseCase(
|
||||||
private val repository: AuthRepository
|
private val repository: AuthRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
@ -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,9 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
class CheckLoginFormatUseCase {
|
||||||
|
operator fun invoke(login: String): Boolean {
|
||||||
|
return login.all { char -> char.isLetterOrDigit() &&
|
||||||
|
((char in 'A'..'Z') || (char in 'a'..'z') || char.isDigit())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
import kotlin.text.all
|
||||||
|
|
||||||
|
class CheckPasswordFormatUseCase {
|
||||||
|
operator fun invoke(
|
||||||
|
password: String
|
||||||
|
): Boolean {
|
||||||
|
return password.length >= 8 && password.all{char ->
|
||||||
|
(char in 'A'..'Z' || char in 'a'..'z' || char.isDigit()) || (char == '!' || char == '@' ||
|
||||||
|
char == '#' || char == '$' || char == '&' || char == '*')}
|
||||||
|
}
|
||||||
|
fun validatePassword(password: String, login: String): Boolean {
|
||||||
|
val loginChars = login.lowercase().toSet()
|
||||||
|
return !password.lowercase().any{
|
||||||
|
it in loginChars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,10 +2,10 @@ package ru.myitschool.work.domain.auth
|
|||||||
|
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
class GetCodeUseCase(
|
class GetTokenLocalUseCase(
|
||||||
private val repository: AuthRepository
|
private val repository: AuthRepository
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(): String? {
|
suspend operator fun invoke(): String? {
|
||||||
return repository.getCode()
|
return repository.auth()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package ru.myitschool.work.domain.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
|
||||||
|
class GetTokenNetworkUseCase(
|
||||||
|
private val repository: AuthRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(login : String, password: String): Result<String> {
|
||||||
|
Log.e("GetTokenNetworkUseCase", repository.auth(login, password).toString())
|
||||||
|
return repository.auth(login, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.domain.meetings
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.repo.MeetingsRepository
|
||||||
|
|
||||||
|
class GetRoomUseCase (
|
||||||
|
private val repository: MeetingsRepository
|
||||||
|
) {
|
||||||
|
suspend operator fun invoke(): String? {
|
||||||
|
return repository.getRoom()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.domain.meetings
|
||||||
|
|
||||||
|
|
||||||
|
data class MeetingsInfoEntity(
|
||||||
|
val id: String,
|
||||||
|
val book: List<Book>
|
||||||
|
) {
|
||||||
|
data class Book(
|
||||||
|
val date: String
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,64 @@
|
|||||||
|
package ru.myitschool.work.ui.custom.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ru.myitschool.work.ui.theme.gray_gradient
|
||||||
|
import ru.myitschool.work.ui.theme.orange_gradient
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomButon(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
round : Dp = 10.dp,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clickable { onClick() }
|
||||||
|
.background(if (enabled) orange_gradient else gray_gradient, RoundedCornerShape(round))
|
||||||
|
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.defaultMinSize(
|
||||||
|
minWidth = ButtonDefaults.MinWidth,
|
||||||
|
minHeight = ButtonDefaults.MinHeight,
|
||||||
|
)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RoundCustomButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
CustomButon(
|
||||||
|
onClick, modifier, enabled, content = content,
|
||||||
|
round = 50.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.nav
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object MeetingsScreenDestination: AppDestination
|
||||||
@ -13,16 +13,18 @@ import androidx.navigation.NavHostController
|
|||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
import ru.myitschool.work.domain.auth.GetCodeUseCase
|
import ru.myitschool.work.domain.auth.GetTokenLocalUseCase
|
||||||
|
import ru.myitschool.work.domain.auth.GetTokenNetworkUseCase
|
||||||
import ru.myitschool.work.ui.nav.AppDestination
|
import ru.myitschool.work.ui.nav.AppDestination
|
||||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
|
import ru.myitschool.work.ui.nav.MeetingsScreenDestination
|
||||||
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
||||||
import ru.myitschool.work.ui.screen.book.BookScreen
|
import ru.myitschool.work.ui.screen.book.BookScreen
|
||||||
import ru.myitschool.work.ui.screen.main.MainScreen
|
import ru.myitschool.work.ui.screen.main.MainScreen
|
||||||
|
import ru.myitschool.work.ui.screen.meetings.MeetingsScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
@ -31,12 +33,13 @@ fun AppNavHost(
|
|||||||
) {
|
) {
|
||||||
var destination by remember { mutableStateOf<AppDestination?>(null) }
|
var destination by remember { mutableStateOf<AppDestination?>(null) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val code = GetCodeUseCase(AuthRepository).invoke()
|
val token = GetTokenLocalUseCase(AuthRepository).invoke()
|
||||||
destination = if (code == null) {
|
destination = if (token == null) {
|
||||||
AuthScreenDestination
|
AuthScreenDestination
|
||||||
} else {
|
} else {
|
||||||
MainScreenDestination
|
MainScreenDestination
|
||||||
}
|
}
|
||||||
|
// destination = MainScreenDestination
|
||||||
}
|
}
|
||||||
if (destination != null) {
|
if (destination != null) {
|
||||||
NavHost(
|
NavHost(
|
||||||
@ -55,6 +58,9 @@ fun AppNavHost(
|
|||||||
composable<BookScreenDestination> {
|
composable<BookScreenDestination> {
|
||||||
BookScreen(navController = navController)
|
BookScreen(navController = navController)
|
||||||
}
|
}
|
||||||
|
composable<MeetingsScreenDestination> {
|
||||||
|
MeetingsScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
sealed interface AuthIntent {
|
sealed interface AuthIntent {
|
||||||
data class Send(val text: String): AuthIntent
|
data class Send(val login: String, val password: String): AuthIntent
|
||||||
data class TextInput(val text: String): AuthIntent
|
data class TextInput(val login: String, val password: String): AuthIntent
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SecureTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -24,12 +26,15 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
import ru.myitschool.work.R
|
import ru.myitschool.work.R
|
||||||
import ru.myitschool.work.core.TestIds
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.ui.custom.component.CustomButon
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthScreen(
|
fun AuthScreen(
|
||||||
@ -47,7 +52,6 @@ fun AuthScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@ -70,34 +74,56 @@ fun AuthScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//@Composable
|
||||||
|
//fun SecureScreen(enabled: Boolean = true) {
|
||||||
|
//
|
||||||
|
//}
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content(
|
||||||
viewModel: AuthViewModel,
|
viewModel: AuthViewModel,
|
||||||
state: AuthState.Data
|
state: AuthState.Data
|
||||||
) {
|
) {
|
||||||
var inputText by remember { mutableStateOf("") }
|
var inputTextLogin by remember { mutableStateOf("") }
|
||||||
|
var inputTextPassword by remember { mutableStateOf("") }
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
modifier = Modifier.testTag(TestIds.Auth.LOGIN_INPUT).fillMaxWidth(),
|
||||||
value = inputText,
|
value = inputTextLogin,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
inputText = it
|
inputTextLogin = it
|
||||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
viewModel.onIntent(AuthIntent.TextInput(inputTextLogin, inputTextPassword))
|
||||||
},
|
},
|
||||||
label = { Text(stringResource(R.string.auth_label)) }
|
label = { Text(stringResource(R.string.auth_login)) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
Button(
|
TextField(
|
||||||
|
modifier = Modifier.testTag(TestIds.Auth.PASSWORD_INPUT).fillMaxWidth(),
|
||||||
|
value = inputTextPassword,
|
||||||
|
onValueChange = {
|
||||||
|
inputTextPassword = it
|
||||||
|
viewModel.onIntent(AuthIntent.TextInput(inputTextLogin, inputTextPassword))
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(R.string.auth_password)) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
CustomButon(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
viewModel.onIntent(AuthIntent.Send(inputTextLogin, inputTextPassword))
|
||||||
},
|
},
|
||||||
enabled = state.isEnabledSend
|
enabled = state.isEnabledSend
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
Text(stringResource(R.string.auth_sign_in))
|
||||||
}
|
}
|
||||||
if (state.error != null) {
|
// if (state.error != null) {
|
||||||
|
// Text(
|
||||||
|
// modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||||
|
// text = state.error,
|
||||||
|
// style = MaterialTheme.typography.bodyMedium,
|
||||||
|
// color = Color.Red,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
if (state.error != null && !state.error.contains("401") && !state.error.contains("Network")) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||||
text = state.error,
|
text = state.error,
|
||||||
@ -105,4 +131,19 @@ private fun Content(
|
|||||||
color = Color.Red,
|
color = Color.Red,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (state.error.toString().contains("401")) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||||
|
text = "Неверный логин или пароль",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Red,
|
||||||
|
)
|
||||||
|
} else if (state.error.toString().contains("Network")) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||||
|
text = "Отсутствует интернет-соединение",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = Color.Red,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@ -10,13 +11,15 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
import ru.myitschool.work.domain.auth.CheckLoginFormatUseCase
|
||||||
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase
|
import ru.myitschool.work.domain.auth.CheckPasswordFormatUseCase
|
||||||
|
import ru.myitschool.work.domain.auth.GetTokenNetworkUseCase
|
||||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel : ViewModel() {
|
||||||
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() }
|
private val checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() }
|
||||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() }
|
||||||
|
private val getTokenNetworkUseCase by lazy { GetTokenNetworkUseCase(AuthRepository) }
|
||||||
private val _uiState = MutableStateFlow<AuthState>(
|
private val _uiState = MutableStateFlow<AuthState>(
|
||||||
AuthState.Data(
|
AuthState.Data(
|
||||||
isEnabledSend = false,
|
isEnabledSend = false,
|
||||||
@ -27,12 +30,12 @@ class AuthViewModel : ViewModel() {
|
|||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
||||||
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
||||||
|
|
||||||
fun onIntent(intent: AuthIntent) {
|
fun onIntent(intent: AuthIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is AuthIntent.Send -> {
|
is AuthIntent.Send -> {
|
||||||
|
Log.e("onIntent", intent.login)
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
getTokenNetworkUseCase.invoke(intent.login, intent.password).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
|
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
|
||||||
},
|
},
|
||||||
@ -49,7 +52,7 @@ class AuthViewModel : ViewModel() {
|
|||||||
is AuthIntent.TextInput -> {
|
is AuthIntent.TextInput -> {
|
||||||
updateStateIfData { oldState ->
|
updateStateIfData { oldState ->
|
||||||
oldState.copy(
|
oldState.copy(
|
||||||
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text),
|
isEnabledSend = checkLoginFormatUseCase.invoke(intent.login) && checkPasswordFormatUseCase.invoke(intent.password),
|
||||||
error = null
|
error = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -141,6 +141,22 @@ private fun ErrorState(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
when (state.error) {
|
||||||
|
"No auth" -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.no_accounts),
|
||||||
|
null,
|
||||||
|
Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"Not internet" -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.not_wifi),
|
||||||
|
null,
|
||||||
|
Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.testTag(TestIds.Book.ERROR),
|
modifier = Modifier.testTag(TestIds.Book.ERROR),
|
||||||
text = state.error,
|
text = state.error,
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Button
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@ -30,11 +29,16 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.imageResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
@ -42,6 +46,8 @@ import coil3.compose.AsyncImage
|
|||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import ru.myitschool.work.R
|
import ru.myitschool.work.R
|
||||||
import ru.myitschool.work.core.TestIds
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.ui.custom.component.CustomButon
|
||||||
|
import ru.myitschool.work.ui.custom.component.RoundCustomButton
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
@ -112,14 +118,30 @@ private fun ErrorState(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
when (state.error) {
|
||||||
|
"No auth" -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.no_accounts),
|
||||||
|
null,
|
||||||
|
Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"Not internet" -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.not_wifi),
|
||||||
|
null,
|
||||||
|
Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
||||||
text = state.error,
|
text = state.error,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = Color.Black,
|
color = Color.Red
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
Button(
|
RoundCustomButton(
|
||||||
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON).fillMaxWidth(),
|
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON).fillMaxWidth(),
|
||||||
onClick = {
|
onClick = {
|
||||||
println("!!!!!!!! refresh on click error")
|
println("!!!!!!!! refresh on click error")
|
||||||
@ -230,3 +252,9 @@ private fun ContentState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun Show() {
|
||||||
|
// MainScreen()
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package ru.myitschool.work.ui.screen.main
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
@ -64,6 +65,7 @@ class MainViewModel : ViewModel() {
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
getMainDataUseCase.invoke().fold(
|
getMainDataUseCase.invoke().fold(
|
||||||
onSuccess = { data ->
|
onSuccess = { data ->
|
||||||
|
Log.d("DataMain", "${data.name}, ${data.book}")
|
||||||
MainState.Data(
|
MainState.Data(
|
||||||
name = data.name,
|
name = data.name,
|
||||||
photoUrl = data.photoUrl,
|
photoUrl = data.photoUrl,
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.meetings
|
||||||
|
|
||||||
|
sealed interface MeetingIntent {
|
||||||
|
/**
|
||||||
|
* Обнавление страницы
|
||||||
|
*/
|
||||||
|
data object Refresh: MeetingIntent
|
||||||
|
/**
|
||||||
|
* Выход
|
||||||
|
*/
|
||||||
|
data object Logout: MeetingIntent
|
||||||
|
/**
|
||||||
|
* Бронирование
|
||||||
|
*/
|
||||||
|
data class Add(
|
||||||
|
val date: String,
|
||||||
|
val placeId: String
|
||||||
|
): MeetingIntent
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.meetings
|
||||||
|
|
||||||
|
object MeetingResult {
|
||||||
|
const val REFRESH_STATUS = "refresh_status"
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.meetings
|
||||||
|
|
||||||
|
import ru.myitschool.work.ui.nav.AppDestination
|
||||||
|
|
||||||
|
sealed interface MeetingsAction {
|
||||||
|
class Open(
|
||||||
|
val destination: AppDestination,
|
||||||
|
val clearBackStack: Boolean = false
|
||||||
|
): MeetingsAction
|
||||||
|
}
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.meetings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
import ru.myitschool.work.core.TestIds
|
||||||
|
import ru.myitschool.work.ui.custom.component.RoundCustomButton
|
||||||
|
import ru.myitschool.work.ui.screen.book.BookAction
|
||||||
|
import ru.myitschool.work.ui.screen.book.BookViewModel
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainIntent
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainResult
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MeetingsScreen(
|
||||||
|
viewModel: MeetingsViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
when (val currentState = state) {
|
||||||
|
is MeetingsState.Data -> MeetingsData(currentState)
|
||||||
|
is MeetingsState.Empty -> MeetingsEmpty(currentState)
|
||||||
|
is MeetingsState.Error -> MeetingsError(viewModel, currentState)
|
||||||
|
is MeetingsState.Loading -> MeetingsLoading(currentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MeetingsData(
|
||||||
|
state: MeetingsState.Data
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MeetingsEmpty(
|
||||||
|
state: MeetingsState.Empty
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
||||||
|
text = "Нет бронирований",
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MeetingsError(
|
||||||
|
viewModel: MeetingsViewModel,
|
||||||
|
state: MeetingsState.Error
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(all = 24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
when (state.error) {
|
||||||
|
"No auth" -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.no_accounts),
|
||||||
|
null,
|
||||||
|
Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"Not internet" -> {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.not_wifi),
|
||||||
|
null,
|
||||||
|
Modifier.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
||||||
|
text = state.error,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = Color.Red
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
RoundCustomButton(
|
||||||
|
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON).fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
viewModel.refresh()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.main_refresh))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MeetingsLoading(
|
||||||
|
state: MeetingsState.Loading
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.meetings
|
||||||
|
|
||||||
|
import kotlinx.collections.immutable.PersistentList
|
||||||
|
|
||||||
|
sealed interface MeetingsState {
|
||||||
|
data object Loading: MeetingsState
|
||||||
|
data object Empty: MeetingsState
|
||||||
|
data class Error(
|
||||||
|
val error: String
|
||||||
|
): MeetingsState
|
||||||
|
data class Data(
|
||||||
|
val name: String,
|
||||||
|
val books: PersistentList<Book>,
|
||||||
|
): MeetingsState {
|
||||||
|
data class Book(
|
||||||
|
val date: String,
|
||||||
|
val time: String
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.meetings
|
||||||
|
|
||||||
|
import android.provider.Settings.System.DATE_FORMAT
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
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.BookRepository
|
||||||
|
import ru.myitschool.work.data.repo.MeetingsRepository
|
||||||
|
import ru.myitschool.work.domain.auth.LogoutUseCase
|
||||||
|
import ru.myitschool.work.domain.main.GetMainDataUseCase
|
||||||
|
import ru.myitschool.work.domain.meetings.GetRoomUseCase
|
||||||
|
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainAction
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainState
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
class MeetingsViewModel: ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow<MeetingsState>(MeetingsState.Loading)
|
||||||
|
val uiState: StateFlow<MeetingsState> = _uiState.asStateFlow()
|
||||||
|
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||||
|
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
||||||
|
private val logoutUseCase by lazy {
|
||||||
|
LogoutUseCase(AuthRepository)
|
||||||
|
}
|
||||||
|
private val getMeetingsDataUseCase by lazy {
|
||||||
|
GetRoomUseCase(MeetingsRepository)
|
||||||
|
}
|
||||||
|
init {
|
||||||
|
|
||||||
|
}
|
||||||
|
// fun onIntent(intent: MeetingIntent) {
|
||||||
|
// when(intent) {
|
||||||
|
// is MeetingIntent.Logout -> {
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// logoutUseCase.invoke()
|
||||||
|
// _actionFlow.emit(/*TODO*/)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// is MeetingIntent.Refresh -> {
|
||||||
|
// /*TODO("обновляться автоматически с заданной периодичностью")*/
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// fun refresh() {
|
||||||
|
// viewModelScope.launch {
|
||||||
|
// _uiState.update { MeetingsState.Loading }
|
||||||
|
// _uiState.update {
|
||||||
|
// getMeetingsDataUseCase.invoke().fold(
|
||||||
|
// onFailure = { error ->
|
||||||
|
// MeetingsState.Error(
|
||||||
|
// error = error.message?.takeIf { it.isNotBlank() } ?: "Unknown error"
|
||||||
|
// )
|
||||||
|
// },
|
||||||
|
// onSuccess = { data ->
|
||||||
|
// MeetingsState.Data(
|
||||||
|
// name = data.name,
|
||||||
|
// books = data.book
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -1,11 +1,20 @@
|
|||||||
package ru.myitschool.work.ui.theme
|
package ru.myitschool.work.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
val Purple80 = Color(0xFFD0BCFF)
|
val day_primary = Color(0xFFffa500)
|
||||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
val day_secondary = Color(0xFFffd700)
|
||||||
val Pink80 = Color(0xFFEFB8C8)
|
val day_tertiary = Color(0xFFffbe00)
|
||||||
|
val day_text = Color(0xFF003366)
|
||||||
|
val day_background = Color(0xFFf0f0f0)
|
||||||
|
|
||||||
val Purple40 = Color(0xFF6650a4)
|
val night_primary = Color(0xFFff8c00)
|
||||||
val PurpleGrey40 = Color(0xFF625b71)
|
val night_secondary = Color(0xFFff7300)
|
||||||
val Pink40 = Color(0xFF7D5260)
|
val night_tertiary = Color(0xFFff5a00)
|
||||||
|
val night_text = Color(0xFFffe4b5)
|
||||||
|
val night_background = Color(0xFF121212)
|
||||||
|
|
||||||
|
|
||||||
|
val orange_gradient = Brush.linearGradient(listOf(day_primary, night_primary))
|
||||||
|
val gray_gradient = Brush.linearGradient(listOf(Color(0xFFc4c7cf), Color(0xFF828897)))
|
||||||
|
|||||||
26
app/src/main/java/ru/myitschool/work/ui/theme/Font.kt
Normal file
26
app/src/main/java/ru/myitschool/work/ui/theme/Font.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package ru.myitschool.work.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
|
||||||
|
val subtitle = FontFamily(
|
||||||
|
Font(
|
||||||
|
R.font.merriweather_120pt_medium,
|
||||||
|
weight = FontWeight.Medium,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val title = FontFamily(
|
||||||
|
Font(
|
||||||
|
R.font.merriweather_120pt_bold,
|
||||||
|
weight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val normal = FontFamily(
|
||||||
|
Font(
|
||||||
|
R.font.roboto_serif_120pt_regular,
|
||||||
|
)
|
||||||
|
)
|
||||||
@ -11,15 +11,21 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
private val DarkColorScheme = darkColorScheme(
|
private val DarkColorScheme = darkColorScheme(
|
||||||
primary = Purple80,
|
primary = night_primary,
|
||||||
secondary = PurpleGrey80,
|
secondary = night_secondary,
|
||||||
tertiary = Pink80
|
tertiary = night_tertiary,
|
||||||
|
background = night_background,
|
||||||
|
onBackground = night_text,
|
||||||
|
onSurface = night_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val LightColorScheme = lightColorScheme(
|
private val LightColorScheme = lightColorScheme(
|
||||||
primary = Purple40,
|
primary = day_primary,
|
||||||
secondary = PurpleGrey40,
|
secondary = day_secondary,
|
||||||
tertiary = Pink40
|
tertiary = day_tertiary,
|
||||||
|
background = day_background,
|
||||||
|
onBackground = day_text,
|
||||||
|
onSurface = day_text
|
||||||
|
|
||||||
/* Other default colors to override
|
/* Other default colors to override
|
||||||
background = Color(0xFFFFFBFE),
|
background = Color(0xFFFFFBFE),
|
||||||
@ -36,7 +42,7 @@ private val LightColorScheme = lightColorScheme(
|
|||||||
fun WorkTheme(
|
fun WorkTheme(
|
||||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
// Dynamic color is available on Android 12+
|
// Dynamic color is available on Android 12+
|
||||||
dynamicColor: Boolean = true,
|
dynamicColor: Boolean = false,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
|
|||||||
@ -9,26 +9,24 @@ import androidx.compose.ui.unit.sp
|
|||||||
// Set of Material typography styles to start with
|
// Set of Material typography styles to start with
|
||||||
val Typography = Typography(
|
val Typography = Typography(
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = subtitle,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Medium,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
lineHeight = 24.sp,
|
lineHeight = 24.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
),
|
||||||
/* Other default text styles to override
|
|
||||||
titleLarge = TextStyle(
|
titleLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = title,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = 22.sp,
|
fontSize = 22.sp,
|
||||||
lineHeight = 28.sp,
|
lineHeight = 28.sp,
|
||||||
letterSpacing = 0.sp
|
letterSpacing = 0.sp
|
||||||
),
|
),
|
||||||
labelSmall = TextStyle(
|
labelSmall = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = normal,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 11.sp,
|
fontSize = 11.sp,
|
||||||
lineHeight = 16.sp,
|
lineHeight = 16.sp,
|
||||||
letterSpacing = 0.5.sp
|
letterSpacing = 0.5.sp
|
||||||
)
|
)
|
||||||
*/
|
|
||||||
)
|
)
|
||||||
BIN
app/src/main/res/drawable/no_accounts.png
Normal file
BIN
app/src/main/res/drawable/no_accounts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
52
app/src/main/res/drawable/not_auth.xml
Normal file
52
app/src/main/res/drawable/not_auth.xml
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 37 KiB |
BIN
app/src/main/res/drawable/not_wifi.png
Normal file
BIN
app/src/main/res/drawable/not_wifi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
app/src/main/res/font/merriweather_120pt_bold.ttf
Normal file
BIN
app/src/main/res/font/merriweather_120pt_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/merriweather_120pt_medium.ttf
Normal file
BIN
app/src/main/res/font/merriweather_120pt_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/roboto_serif_120pt_regular.ttf
Normal file
BIN
app/src/main/res/font/roboto_serif_120pt_regular.ttf
Normal file
Binary file not shown.
@ -1,8 +1,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Work</string>
|
<string name="app_name">Work</string>
|
||||||
<string name="title_activity_root">RootActivity</string>
|
<string name="title_activity_root">RootActivity</string>
|
||||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
<string name="auth_title">Привет! Введи свой логин и пароль для авторизации</string>
|
||||||
<string name="auth_label">Код</string>
|
<string name="auth_login">Логин</string>
|
||||||
|
<string name="auth_password">Пароль</string>
|
||||||
<string name="auth_sign_in">Войти</string>
|
<string name="auth_sign_in">Войти</string>
|
||||||
|
|
||||||
<string name="main_refresh">Обновить</string>
|
<string name="main_refresh">Обновить</string>
|
||||||
|
|||||||
8
local.properties
Normal file
8
local.properties
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
## This file must *NOT* be checked into Version Control Systems,
|
||||||
|
# as it contains information specific to your local configuration.
|
||||||
|
#
|
||||||
|
# Location of the SDK. This is only used by Gradle.
|
||||||
|
# For customization when using a Version Control System, please read the
|
||||||
|
# header note.
|
||||||
|
#Wed Feb 25 10:24:12 MSK 2026
|
||||||
|
sdk.dir=C\:\\Users\\Samsung\\AppData\\Local\\Android\\Sdk
|
||||||
Loading…
x
Reference in New Issue
Block a user