checkpoint 0 #5

Open
student-e-klyukin wants to merge 15 commits from WindWin-org/NTO-2026-Android-TeamTask-Template:main into main
44 changed files with 730 additions and 96 deletions

2
README.md Normal file
View 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

View File

@ -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")

View File

@ -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
} }

View File

@ -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"

View File

@ -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"

View File

@ -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)
} }
} }

View File

@ -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)
} }

View File

@ -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
}
}

View File

@ -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"
} }

View File

@ -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(

View File

@ -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())
}
}
}

View File

@ -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())
}
}
}

View File

@ -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
}
}
}

View File

@ -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()
} }
} }

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -0,0 +1,6 @@
package ru.myitschool.work.ui.nav
import kotlinx.serialization.Serializable
@Serializable
data object MeetingsScreenDestination: AppDestination

View File

@ -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()
}
} }
} }
} }

View File

@ -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
} }

View File

@ -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,
)
}
} }

View File

@ -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
) )
} }

View File

@ -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,

View File

@ -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")
@ -229,4 +251,10 @@ private fun ContentState(
) )
} }
} }
}
@Preview
@Composable
fun Show() {
// MainScreen()
} }

View File

@ -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,

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package ru.myitschool.work.ui.screen.meetings
object MeetingResult {
const val REFRESH_STATUS = "refresh_status"
}

View File

@ -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
}

View File

@ -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)
)
}
}

View File

@ -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
)
}
}

View File

@ -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
// )
// }
// )
// }
// }
// }
}

View File

@ -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)))

View 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,
)
)

View File

@ -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 {

View File

@ -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
) )
*/
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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
View 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