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 {
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()
implementation("androidx.datastore:datastore-preferences:1.1.7")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")

View File

@ -8,7 +8,6 @@ class App: Application() {
super.onCreate()
context = this
}
companion object {
lateinit var context: Context
}

View File

@ -1,7 +1,7 @@
package ru.myitschool.work.core
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 INFO_URL = "/info"
const val BOOKING_URL = "/booking"

View File

@ -4,7 +4,8 @@ object TestIds {
object Auth {
const val ERROR = "auth_error"
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 {
const val ERROR = "main_error"

View File

@ -1,6 +1,7 @@
package ru.myitschool.work.data.repo
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
@ -12,37 +13,47 @@ import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository {
private const val STORE = "AUTH-STORE"
private const val CODE_KEY = "CODE"
private var codeCache: String? = null
private const val TOKEN_KEY = "TOKEN"
private var tokenCache: String? = null
suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) {
codeCache = text
tokenCache = text
App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(CODE_KEY)
val prefKey = stringPreferencesKey(TOKEN_KEY)
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()
?.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() {
codeCache = null
tokenCache = null
App.context.userDataStore.edit { preferences ->
val prefKey = stringPreferencesKey(CODE_KEY)
val prefKey = stringPreferencesKey(TOKEN_KEY)
preferences.remove(prefKey)
}
}

View File

@ -1,5 +1,6 @@
package ru.myitschool.work.data.repo
import android.util.Log
import ru.myitschool.work.data.dto.BookRequestDto
import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.book.entities.BookRequestData
@ -10,8 +11,10 @@ class BookRepository(
private val authRepository: AuthRepository
) {
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 ->
Log.e("getInfoCode", "!!!!!!!!!!!!!! getInfo ERROR ${dto.booking}")
MainInfoEntity(
name = dto.name ?: error("Name is null"),
photoUrl = dto.photoUrl ?: error("Photo url is null"),
@ -26,7 +29,7 @@ class BookRepository(
}
suspend fun getBookingInfo(): Result<List<BookingData>> {
val code = authRepository.getCode() ?: return getNoAuthResult()
val code = authRepository.auth() ?: return getNoAuthResult()
return NetworkDataSource.getBooking(code).mapCatching { dto ->
dto?.map { (date, places) ->
BookingData(
@ -43,7 +46,7 @@ class BookRepository(
}
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)
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
import android.util.Log
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
@ -35,10 +36,32 @@ object NetworkDataSource {
}
}
}
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
suspend fun auth(login: String, password: String): Result<String> = withContext(Dispatchers.IO) {
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) {
HttpStatusCode.OK -> true
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 {
println("!!!!!!!!!!!!!! getInfo $code")
val response = client.get(getUrl(code, Constants.INFO_URL))
println("!!!!!!!!!!!!!! getInfo $token")
val response = client.get(getUrl(token, Constants.INFO_URL))
if (response.status == HttpStatusCode.OK) {
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
Log.d("1", "!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
response.body<UserDto>()
} else {
println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
Log.e("getInfo", "!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
error(response.bodyAsText())
}
}
}
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
suspend fun getBooking(token: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
val response = client.get(getUrl(token, Constants.BOOKING_URL))
if (response.status == HttpStatusCode.OK) {
response.body<Map<String, List<PlaceDto>>>()
} 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 {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
val response = client.post(getUrl(token, Constants.BOOK_URL)) {
contentType(ContentType.Application.Json)
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
class CheckAndSaveAuthCodeUseCase(
class CheckAndSaveAuthUseCase(
private val repository: AuthRepository
) {
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
class GetCodeUseCase(
class GetTokenLocalUseCase(
private val repository: AuthRepository
) {
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.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.delay
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.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
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.book.BookScreen
import ru.myitschool.work.ui.screen.main.MainScreen
import ru.myitschool.work.ui.screen.meetings.MeetingsScreen
@Composable
fun AppNavHost(
@ -31,12 +33,13 @@ fun AppNavHost(
) {
var destination by remember { mutableStateOf<AppDestination?>(null) }
LaunchedEffect(Unit) {
val code = GetCodeUseCase(AuthRepository).invoke()
destination = if (code == null) {
val token = GetTokenLocalUseCase(AuthRepository).invoke()
destination = if (token == null) {
AuthScreenDestination
} else {
MainScreenDestination
}
// destination = MainScreenDestination
}
if (destination != null) {
NavHost(
@ -55,6 +58,9 @@ fun AppNavHost(
composable<BookScreenDestination> {
BookScreen(navController = navController)
}
composable<MeetingsScreenDestination> {
MeetingsScreen()
}
}
}
}

View File

@ -1,6 +1,6 @@
package ru.myitschool.work.ui.screen.auth
sealed interface AuthIntent {
data class Send(val text: String): AuthIntent
data class TextInput(val text: String): AuthIntent
data class Send(val login: String, val password: String): AuthIntent
data class TextInput(val login: String, val password: String): AuthIntent
}

View File

@ -1,5 +1,6 @@
package ru.myitschool.work.ui.screen.auth
import android.util.Log
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SecureTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@ -24,12 +26,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import io.ktor.http.HttpStatusCode
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.custom.component.CustomButon
@Composable
fun AuthScreen(
@ -47,7 +52,6 @@ fun AuthScreen(
}
}
}
Column(
modifier = Modifier
.fillMaxSize()
@ -70,34 +74,56 @@ fun AuthScreen(
}
}
}
//@Composable
//fun SecureScreen(enabled: Boolean = true) {
//
//}
@Composable
private fun Content(
viewModel: AuthViewModel,
state: AuthState.Data
) {
var inputText by remember { mutableStateOf("") }
var inputTextLogin by remember { mutableStateOf("") }
var inputTextPassword by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp))
TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
value = inputText,
modifier = Modifier.testTag(TestIds.Auth.LOGIN_INPUT).fillMaxWidth(),
value = inputTextLogin,
onValueChange = {
inputText = it
viewModel.onIntent(AuthIntent.TextInput(it))
inputTextLogin = 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))
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(),
onClick = {
viewModel.onIntent(AuthIntent.Send(inputText))
viewModel.onIntent(AuthIntent.Send(inputTextLogin, inputTextPassword))
},
enabled = state.isEnabledSend
) {
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(
modifier = Modifier.testTag(TestIds.Auth.ERROR),
text = state.error,
@ -105,4 +131,19 @@ private fun Content(
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
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
@ -10,13 +11,15 @@ 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.domain.auth.CheckAndSaveAuthCodeUseCase
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase
import ru.myitschool.work.domain.auth.CheckLoginFormatUseCase
import ru.myitschool.work.domain.auth.CheckPasswordFormatUseCase
import ru.myitschool.work.domain.auth.GetTokenNetworkUseCase
import ru.myitschool.work.ui.nav.MainScreenDestination
class AuthViewModel : ViewModel() {
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() }
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
private val checkPasswordFormatUseCase by lazy { CheckPasswordFormatUseCase() }
private val checkLoginFormatUseCase by lazy { CheckLoginFormatUseCase() }
private val getTokenNetworkUseCase by lazy { GetTokenNetworkUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<AuthState>(
AuthState.Data(
isEnabledSend = false,
@ -27,12 +30,12 @@ class AuthViewModel : ViewModel() {
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
val actionFlow: SharedFlow<AuthAction> = _actionFlow
fun onIntent(intent: AuthIntent) {
when (intent) {
is AuthIntent.Send -> {
Log.e("onIntent", intent.login)
viewModelScope.launch {
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
getTokenNetworkUseCase.invoke(intent.login, intent.password).fold(
onSuccess = {
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
},
@ -49,7 +52,7 @@ class AuthViewModel : ViewModel() {
is AuthIntent.TextInput -> {
updateStateIfData { oldState ->
oldState.copy(
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text),
isEnabledSend = checkLoginFormatUseCase.invoke(intent.login) && checkPasswordFormatUseCase.invoke(intent.password),
error = null
)
}

View File

@ -141,6 +141,22 @@ private fun ErrorState(
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.Book.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.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
@ -30,11 +29,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
@ -42,6 +46,8 @@ import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.custom.component.CustomButon
import ru.myitschool.work.ui.custom.component.RoundCustomButton
@Composable
fun MainScreen(
@ -112,14 +118,30 @@ private fun ErrorState(
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.Black,
color = Color.Red
)
Spacer(modifier = Modifier.size(16.dp))
Button(
RoundCustomButton(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON).fillMaxWidth(),
onClick = {
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
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.collections.immutable.toPersistentList
@ -64,6 +65,7 @@ class MainViewModel : ViewModel() {
_uiState.update {
getMainDataUseCase.invoke().fold(
onSuccess = { data ->
Log.d("DataMain", "${data.name}, ${data.book}")
MainState.Data(
name = data.name,
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
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val day_primary = Color(0xFFffa500)
val day_secondary = Color(0xFFffd700)
val day_tertiary = Color(0xFFffbe00)
val day_text = Color(0xFF003366)
val day_background = Color(0xFFf0f0f0)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val night_primary = Color(0xFFff8c00)
val night_secondary = Color(0xFFff7300)
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
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
primary = night_primary,
secondary = night_secondary,
tertiary = night_tertiary,
background = night_background,
onBackground = night_text,
onSurface = night_text,
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
primary = day_primary,
secondary = day_secondary,
tertiary = day_tertiary,
background = day_background,
onBackground = day_text,
onSurface = day_text
/* Other default colors to override
background = Color(0xFFFFFBFE),
@ -36,7 +42,7 @@ private val LightColorScheme = lightColorScheme(
fun WorkTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {

View File

@ -9,26 +9,24 @@ import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontFamily = subtitle,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontFamily = title,
fontWeight = FontWeight.Bold,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontFamily = normal,
fontWeight = FontWeight.Normal,
fontSize = 11.sp,
lineHeight = 16.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>
<string name="app_name">Work</string>
<string name="title_activity_root">RootActivity</string>
<string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_label">Код</string>
<string name="auth_title">Привет! Введи свой логин и пароль для авторизации</string>
<string name="auth_login">Логин</string>
<string name="auth_password">Пароль</string>
<string name="auth_sign_in">Войти</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