Compare commits

...

3 Commits

Author SHA1 Message Date
solovushka56
6f2ec1fefd bugfix 2025-12-12 14:56:24 +03:00
solovushka56
c6418793cb fix revert 2025-12-12 12:59:43 +03:00
solovushka56
2da3b58773 Revert "fix"
This reverts commit a04fb915ae.
2025-12-12 12:34:33 +03:00
43 changed files with 1379 additions and 1192 deletions

View File

@@ -2,29 +2,24 @@ package ru.myitschool.work
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import ru.myitschool.work.data.DataStoreManager import androidx.datastore.core.DataStore
import ru.myitschool.work.data.repo.AuthRepository import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import ru.myitschool.work.data.datastore.DataStoreManager
class App : Application() { val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "datastore")
companion object { class App: Application() {
@Volatile
private var instance: App? = null
fun getAppContext(): Context {
return instance?.applicationContext ?: throw IllegalStateException(
"app not initialized")
}
}
lateinit var dataStoreManager: DataStoreManager lateinit var dataStoreManager: DataStoreManager
private set
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
instance = this context = this
dataStoreManager = DataStoreManager(applicationContext) dataStoreManager = DataStoreManager(dataStore)
}
AuthRepository.getInstance(applicationContext) companion object {
lateinit var context: Context
} }
} }

View File

@@ -1,10 +1,7 @@
package ru.myitschool.work.core package ru.myitschool.work.core
object Constants { object Constants {
const val HOST = "http://10.0.2.2:8080" const val HOST = "http://10.0.2.2:8080"
// const val HOST = "http://127.0.0.1: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

@@ -1,36 +0,0 @@
package ru.myitschool.work.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")
class DataStoreManager(context: Context) {
private val dataStore = context.dataStore
private object Keys {
val USER_CODE = stringPreferencesKey("user_code")
}
suspend fun saveUserCode(code: String) {
dataStore.edit { prefs ->
prefs[Keys.USER_CODE] = code
}
}
suspend fun clearUserCode() {
dataStore.edit { prefs ->
prefs.remove(Keys.USER_CODE)
}
}
fun getUserCode(): Flow<String> = dataStore.data.map { prefs ->
prefs[Keys.USER_CODE] ?: ""
}
}

View File

@@ -0,0 +1,35 @@
package ru.myitschool.work.data.datastore
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class DataStoreManager(
private val dataStore: DataStore<Preferences>
) {
companion object {
private val USER_CODE_KEY = stringPreferencesKey("user_code")
}
suspend fun clearUserCode() {
dataStore.edit { preferences ->
preferences.remove(USER_CODE_KEY)
}
}
suspend fun saveUserCode(userCode: UserCode) {
dataStore.edit { preferences ->
preferences[USER_CODE_KEY] = userCode.code
}
}
fun getUserCode(): Flow<UserCode> = dataStore.data.map { preferences ->
UserCode(
code = preferences[USER_CODE_KEY] ?: ""
)
}
}

View File

@@ -0,0 +1,5 @@
package ru.myitschool.work.data.datastore
data class UserCode(
val code: String
)

View File

@@ -1,6 +0,0 @@
package ru.myitschool.work.data.dtos
class BookingDto {
}

View File

@@ -1,13 +0,0 @@
package ru.myitschool.work.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
typealias BookingInfoResponse = Map<String, List<PlaceInfo>>
@Serializable
data class PlaceInfo(
val id: Int,
val place: String
)

View File

@@ -1,41 +0,0 @@
package ru.myitschool.work.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UserInfoResponse(
@SerialName("name")
val name: String,
@SerialName("photoUrl")
val photoUrl: String?,
@SerialName("booking")
val bookingMap: Map<String, BookingInfo> = emptyMap()
) {
val bookings: List<BookingResponse>
get() = bookingMap.map { (date, info) ->
BookingResponse(
date = date,
place = info.place,
bookingId = info.id
)
}
}
@Serializable
data class BookingInfo(
@SerialName("id")
val id: Int,
@SerialName("place")
val place: String
)
@Serializable
data class BookingResponse(
val date: String,
val place: String,
val bookingId: Int
)

View File

@@ -1,104 +1,10 @@
package ru.myitschool.work.data.repo package ru.myitschool.work.data.repo
import android.content.Context
import android.content.Context.MODE_PRIVATE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import ru.myitschool.work.App
import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.data.source.NetworkDataSource
class AuthRepository private constructor(context: Context) { object AuthRepository {
companion object { suspend fun checkAndSave(code: String): Result<Boolean> {
@Volatile return NetworkDataSource.checkAuth(code)
private var INSTANCE: AuthRepository? = null
fun getInstance(context: Context): AuthRepository {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: AuthRepository(context.applicationContext).also { INSTANCE = it }
}
}
fun clearInstance() {
INSTANCE = null
}
}
private val PREFS_NAME = "auth_prefs"
private val KEY_CODE = "auth_code"
private val KEY_NAME = "user_name"
private val KEY_PHOTO = "user_photo"
private val context: Context = context.applicationContext
private var codeCache: String? = null
private var userCache: UserCache? = null
private val _isAuthorized = MutableStateFlow(false)
val isAuthorized: StateFlow<Boolean> = _isAuthorized.asStateFlow()
init {
val prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
codeCache = prefs.getString(KEY_CODE, null)
val name = prefs.getString(KEY_NAME, null)
val photo = prefs.getString(KEY_PHOTO, null)
if (codeCache != null && name != null) {
userCache = UserCache(name, photo)
_isAuthorized.value = true
} else {
CoroutineScope(Dispatchers.IO).launch {
clear()
}
}
}
private fun getPrefs() = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
suspend fun checkAndSave(text: String): Result<Boolean> {
return NetworkDataSource.checkAuth(text).onSuccess { success ->
if (success) {
codeCache = text
_isAuthorized.value = true
getPrefs().edit()
.putString(KEY_CODE, text)
.apply()
val app = context.applicationContext as App
app.dataStoreManager.saveUserCode(text)
}
}
}
fun getCurrentCode(): String? = codeCache
fun saveUserInfo(name: String, photo: String?) {
userCache = UserCache(name, photo)
getPrefs().edit()
.putString(KEY_NAME, name)
.putString(KEY_PHOTO, photo)
.apply()
}
fun getUserInfo(): UserCache? = userCache
suspend fun clear() {
codeCache = null
userCache = null
_isAuthorized.value = false
getPrefs().edit()
.clear()
.apply()
val app = context.applicationContext as App
app.dataStoreManager.clearUserCode()
} }
} }
data class UserCache(
val name: String,
val photo: String?
)

View File

@@ -1,26 +1,25 @@
package ru.myitschool.work.data.repo package ru.myitschool.work.data.repo
import ru.myitschool.work.data.model.PlaceInfo
import ru.myitschool.work.data.source.AuthException
import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.book.entities.BookingEntity
class BookRepository(private val authRepository: AuthRepository) { object ReserveRepo {
suspend fun getAvailableBookings(): Result<Map<String, List<PlaceInfo>>> { suspend fun loadAvailable(code: String): Result<BookingEntity> {
val code = authRepository.getCurrentCode() return NetworkDataSource.loadBooking(code)
return if (code != null) {
NetworkDataSource.getBookingInfo(code)
} else {
Result.failure(AuthException("user not authorized"))
}
} }
suspend fun book(date: String, placeId: Int): Result<Unit> { suspend fun reserve(
val code = authRepository.getCurrentCode() userCode: String,
return if (code != null) { day: String,
NetworkDataSource.book(code, date, placeId) placeId: Int,
} else { placeLabel: String
Result.failure(AuthException("user not authorized")) ): Result<Unit> {
} return NetworkDataSource.bookPlace(
userCode = userCode,
date = day,
placeId = placeId,
placeName = placeLabel
)
} }
} }

View File

@@ -1,17 +1,11 @@
package ru.myitschool.work.data.repo package ru.myitschool.work.data.repo
import ru.myitschool.work.data.model.UserInfoResponse
import ru.myitschool.work.data.source.AuthException
import ru.myitschool.work.data.source.NetworkDataSource import ru.myitschool.work.data.source.NetworkDataSource
import ru.myitschool.work.domain.main.entities.UserEntity
class MainRepository(private val authRepository: AuthRepository) { object ProfileRepo {
suspend fun getUserInfo(): Result<UserInfoResponse> { suspend fun loadProfile(code: String): Result<UserEntity> {
val code = authRepository.getCurrentCode() return NetworkDataSource.loadData(code)
return if (code != null) {
NetworkDataSource.getInfo(code)
} else {
Result.failure(AuthException("user not pass auth"))
}
} }
} }

View File

@@ -1,192 +1,129 @@
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
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.plugins.HttpRequestTimeoutException
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.http.ContentType import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.model.PlaceInfo import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.data.model.UserInfoResponse import ru.myitschool.work.domain.book.entities.PlaceInfo
import java.net.ConnectException import ru.myitschool.work.domain.main.entities.UserEntity
import java.net.SocketTimeoutException
@Serializable private const val testJson = """
data class BookRequest( {
val date: String, "name": "Иванов Петр Федорович",
val placeId: Int "photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",
) "booking": {
"2025-01-05": {"id":1,"place":"102"},
"2025-01-06": {"id":2,"place":"209.13"},
"2025-01-09": {"id":3,"place":"Зона 51. 50"}
}
}
"""
private const val testBookingJson = """
{
"2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
"2025-01-06": [{"id": 3, "place": "Зона 51. 50"}],
"2025-01-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
"2025-01-08": [{"id": 2, "place": "209.13"}]
}
"""
object NetworkDataSource { object NetworkDataSource {
private val client by lazy { private val client by lazy {
HttpClient(CIO) { HttpClient(CIO) {
engine {
requestTimeout = 10000
}
HttpResponseValidator {
validateResponse { response ->
val statusCode = response.status.value
when (statusCode) {
in 400..499 -> {
when (response.status) {
HttpStatusCode.Unauthorized -> {
throw AuthException("Неверный код авторизации")
}
HttpStatusCode.NotFound -> {
throw NotFoundException("Ресурс не найден")
}
HttpStatusCode.Conflict -> {
throw ConflictException("Место уже забронировано")
}
HttpStatusCode.BadRequest -> {
throw BadRequestException("Некорректный запрос")
}
else -> {
val exceptionMessage = response.body<String>()
throw ClientRequestException(
response,
exceptionMessage.ifBlank {
"Клиентская ошибка: $statusCode"
}
)
}
}
}
in 500..599 -> {
throw ServerException("Ошибка сервера: $statusCode")
}
}
}
handleResponseExceptionWithRequest { exception, _ ->
when (exception) {
is SocketTimeoutException -> {
throw NetworkException("Таймаут соединения")
}
is ConnectException -> {
throw NetworkException("Не удалось подключиться к серверу")
}
is HttpRequestTimeoutException -> {
throw NetworkException("Таймаут запроса")
}
}
}
}
install(ContentNegotiation) { install(ContentNegotiation) {
json( json(
Json { Json {
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
explicitNulls = false explicitNulls = true
encodeDefaults = true encodeDefaults = true
} }
) )
} }
}
}
defaultRequest { suspend fun bookPlace(
contentType(ContentType.Application.Json) userCode: String,
date: String,
placeId: Int,
placeName: String
): Result<Unit> = withContext(Dispatchers.IO) {
return@withContext runCatching {
// Log.i("aaa", "Booking: userCode=$userCode, date=$date, placeId=$placeId, placeName=$placeName")
// println("Booking: userCode=$userCode, date=$date, placeId=$placeId, placeName=$placeName")
val response = client.post(getUrl(userCode, Constants.BOOK_URL)) {
setBody(mapOf(
"date" to date,
"placeId" to placeId,
"placeName" to placeName
))
}
when (response.status) {
HttpStatusCode.OK -> Unit
else -> error(response.bodyAsText())
} }
} }
} }
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) { suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
// true // удалить при проверке
val response = client.get(getUrl(code, Constants.AUTH_URL)) val response = client.get(getUrl(code, Constants.AUTH_URL))
response.status == HttpStatusCode.OK response.status
}.recoverCatching { throwable ->
when (throwable) {
is AuthException -> false
else -> throw throwable
}
}
}
suspend fun getInfo(code: String): Result<UserInfoResponse> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) { when (response.status) {
HttpStatusCode.OK -> response.body<UserInfoResponse>() HttpStatusCode.OK -> true
else -> { else -> error(response.bodyAsText())
val errorMessage = response.body<String>().ifBlank {
"Ошибка: ${response.status}"
}
throw RuntimeException(errorMessage)
}
} }
} }
} }
suspend fun getBookingInfo(code: String): Result<Map<String, List<PlaceInfo>>> = withContext(Dispatchers.IO) { suspend fun loadData(code: String): Result<UserEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
// Json.decodeFromString<UserEntity>(testJson) // удалить при проверке
val response = client.get(getUrl(code, Constants.INFO_URL))
when (response.status) { when (response.status) {
HttpStatusCode.OK -> { HttpStatusCode.OK -> {
try { response.body<UserEntity>()
val body = response.body<Map<String, List<PlaceInfo>>>()
body
} catch (e: Exception) {
emptyMap()
}
}
HttpStatusCode.NoContent -> {
emptyMap()
}
else -> {
val errorMsg = response.body<String>().ifBlank {
"Ошибка загрузки данных: ${response.status}"
}
throw RuntimeException(errorMsg)
} }
else -> error(response.bodyAsText())
} }
} }
} }
suspend fun book(code: String, date: String, placeId: Int): Result<Unit> = withContext(Dispatchers.IO) { suspend fun loadBooking(code: String): Result<BookingEntity> = withContext(Dispatchers.IO) {
return@withContext runCatching { return@withContext runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
setBody(BookRequest(date, placeId))
}
// BookingEntity(Json.decodeFromString<Map<String, List<PlaceInfo>>>(testBookingJson)) // удалить при проверке
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) { when (response.status) {
HttpStatusCode.OK -> Unit HttpStatusCode.OK -> {
HttpStatusCode.Created -> Unit BookingEntity(response.body<Map<String, List<PlaceInfo>>>())
HttpStatusCode.Conflict -> throw ConflictException("Место уже забронировано")
else -> {
val errorMsg = response.body<String>().ifBlank {
"Ошибка бронирования: ${response.status}"
}
throw RuntimeException(errorMsg)
} }
else -> error(response.bodyAsText())
} }
} }
} }
private fun getUrl(code: String, targetUrl: String): String { private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
return "${Constants.HOST}/api/$code$targetUrl"
}
} }
class NetworkException(message: String) : Exception(message)
class AuthException(message: String) : Exception(message)
class NotFoundException(message: String) : Exception(message)
class ConflictException(message: String) : Exception(message)
class ServerException(message: String) : Exception(message)
class BadRequestException(message: String) : Exception(message)

View File

@@ -1,33 +0,0 @@
// domain/main/LoadDataUseCase.kt
package ru.myitschool.work.domain.main
import ru.myitschool.work.data.model.UserInfoResponse
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.entities.BookingInfo
import ru.myitschool.work.domain.main.entities.UserEntity
class LoadDataUseCase(
private val repository: ru.myitschool.work.data.repo.MainRepository
) {
suspend operator fun invoke(userCode: String): Result<UserEntity> {
return repository.getUserInfo().map { userInfoResponse ->
mapToUserEntity(userInfoResponse)
}
}
private fun mapToUserEntity(response: UserInfoResponse): UserEntity {
val bookings = response.bookings.map { bookingResponse ->
BookingInfo(
date = bookingResponse.date,
place = bookingResponse.place,
id = bookingResponse.bookingId
)
}
return UserEntity(
name = response.name,
photoUrl = response.photoUrl,
booking = bookings
)
}
}

View File

@@ -0,0 +1,16 @@
package ru.myitschool.work.domain.book
import ru.myitschool.work.data.repo.BookRepository
class BookingUseCase(
private val repository: BookRepository
) {
suspend operator fun invoke(
userCode: String,
date: String,
placeId: Int,
placeName: String
): Result<Unit> {
return repository.bookPlace(userCode, date, placeId, placeName)
}
}

View File

@@ -0,0 +1,16 @@
package ru.myitschool.work.domain.book
import ru.myitschool.work.data.repo.BookRepository
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.domain.main.entities.UserEntity
class LoadBookingUseCase(
private val repository: BookRepository
) {
suspend operator fun invoke(
text: String
): Result<BookingEntity> {
return repository.loadBooking(text)
}
}

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.domain.book.entities
import kotlinx.serialization.Serializable
@Serializable
data class BookingEntity(
val bookings: Map<String, List<PlaceInfo>>
)

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.domain.book.entities
import kotlinx.serialization.Serializable
@Serializable
data class PlaceInfo(
val id: Int,
val place: String
)

View File

@@ -0,0 +1,14 @@
package ru.myitschool.work.domain.main
import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.entities.UserEntity
class LoadDataUseCase(
private val repository: MainRepository
) {
suspend operator fun invoke(
text: String
): Result<UserEntity> {
return repository.loadData(text)
}
}

View File

@@ -1,7 +1,9 @@
package ru.myitschool.work.domain.main.entities package ru.myitschool.work.domain.main.entities
import kotlinx.serialization.Serializable
@Serializable
data class BookingInfo( data class BookingInfo(
val date: String, val id: Int,
val place: String, val place: String
val id: Int
) )

View File

@@ -1,32 +1,26 @@
package ru.myitschool.work.domain.main.entities package ru.myitschool.work.domain.main.entities
import java.time.LocalDate import kotlinx.serialization.Serializable
import java.time.format.DateTimeFormatter import ru.myitschool.work.formatDate
@Serializable
data class UserEntity( data class UserEntity(
val name: String, val name: String,
val photoUrl: String?, val photoUrl: String,
val booking: List<BookingInfo> val booking: Map<String, BookingInfo>? = null
) { ) {
fun hasBookings(): Boolean = booking.isNotEmpty() fun getSortedBookings(): List<Pair<String, BookingInfo>> {
return booking?.entries
?.sortedBy { (date, _) -> date }
?.map { it.toPair() }
?: emptyList()
}
fun getSortedBookingsWithFormattedDate(): List<Triple<String, String, BookingInfo>>? { fun getSortedBookingsWithFormattedDate(): List<Triple<String, String, BookingInfo>> {
if (booking.isEmpty()) return null return getSortedBookings().map { (date, bookingInfo) ->
Triple(date, date.formatDate(), bookingInfo)
return booking.sortedBy { it.date }
.map { booking ->
val originalDate = booking.date
val formattedDate = formatDate(originalDate)
Triple(originalDate, formattedDate, booking)
} }
} }
private fun formatDate(dateStr: String): String { fun hasBookings(): Boolean = !booking.isNullOrEmpty()
return try {
val date = LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE)
date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
} catch (e: Exception) {
dateStr
}
}
} }

View File

@@ -0,0 +1,198 @@
package ru.myitschool.work.ui
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.myitschool.work.R
import ru.myitschool.work.ui.theme.Black
import ru.myitschool.work.ui.theme.Gray
import ru.myitschool.work.ui.theme.LightBlue
import ru.myitschool.work.ui.theme.LightGray
import ru.myitschool.work.ui.theme.Typography
import ru.myitschool.work.ui.theme.White
@Composable
fun BaseText24(
text: String,
modifier: Modifier = Modifier,
color: Color = Black,
textAlign: TextAlign = TextAlign.Left
) {
Text(
text = text,
fontSize = 24.sp,
style = Typography.bodyLarge,
modifier = modifier,
color = color,
textAlign = textAlign
)
}
@Composable
fun BaseButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Button(
onClick = onClick,
modifier = modifier,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = White,
disabledContainerColor = Color.Transparent,
disabledContentColor = White
)
) {
BaseText16(text = text, color = White)
}
}
@Composable
fun Logo() {
Image(
painter = painterResource(R.drawable.ic_git_clone_mini),
contentDescription = "Logo",
modifier = Modifier.padding(top = 40.dp, bottom = 60.dp)
)
}
@Composable
fun BaseText16(
text: String,
modifier: Modifier = Modifier,
color: Color = Black,
) {
Text(
text = text,
style = Typography.bodySmall,
fontSize = 16.sp,
color = color,
modifier = modifier
)
}
@Composable
fun BaseText12(
modifier: Modifier = Modifier,
text: String,
color: Color = Black,
) {
Text(
text = text,
style = Typography.bodySmall,
fontSize = 12.sp,
color = color,
modifier = modifier,
)
}
@Composable
fun BaseText14(
modifier: Modifier = Modifier,
text: String,
color: Color = Black,
) {
Text(
text = text,
style = Typography.bodySmall,
fontSize = 14.sp,
color = color,
modifier = modifier,
)
}
@Composable
fun BaseInputText(
placeholder: String= "",
modifier: Modifier = Modifier,
onValueChange: (String) -> Unit,
value: String
) {
TextField(
value = value,
onValueChange = onValueChange,
shape = RoundedCornerShape(16.dp),
placeholder = {
BaseText16(
text = placeholder,
color = Gray
)
},
textStyle = Typography.bodySmall.copy(fontSize = 16.sp),
colors = TextFieldDefaults.colors(
focusedContainerColor = LightBlue,
unfocusedContainerColor = LightBlue,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent
),
singleLine = true,
modifier = modifier
)
}
@Composable
fun BaseText20(
text: String,
color: Color = Color.Unspecified,
style: TextStyle = Typography.bodySmall,
modifier: Modifier = Modifier.padding(7.dp),
textAlign: TextAlign = TextAlign.Unspecified
) {
Text(
text = text,
style = style,
fontSize = 20.sp,
modifier = modifier,
color = color,
textAlign = textAlign
)
}
@Composable
fun BaseButton(
border: BorderStroke? = null,
enable: Boolean = true,
text: String,
btnColor: Color,
btnContentColor: Color,
onClick: () -> Unit,
icon: @Composable RowScope.() -> Unit = {},
modifier: Modifier = Modifier.fillMaxWidth()
) {
Button(
border = border,
enabled = enable,
onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = btnColor,
contentColor = btnContentColor,
disabledContainerColor = LightGray,
disabledContentColor = Gray
),
modifier = modifier,
shape = RoundedCornerShape(16.dp),
) {
icon()
BaseText20(text = text)
}
}

View File

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

View File

@@ -2,54 +2,39 @@ package ru.myitschool.work.ui.screen
import androidx.compose.animation.EnterTransition import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavHostController 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 ru.myitschool.work.data.repo.AuthRepository
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.SplashScreenDestination
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.splash.SplashScreen
@Composable @Composable
fun AppNavHost( fun AppNavHost(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
navController: NavHostController = rememberNavController() navController: NavHostController = rememberNavController()
) { ) {
val context = LocalContext.current
val authRepository = remember { AuthRepository.getInstance(context) }
val isAuthorized by authRepository.isAuthorized.collectAsState()
LaunchedEffect(isAuthorized) {
if (isAuthorized) {
navController.navigate(MainScreenDestination) {
popUpTo(0) { inclusive = false }
}
} else {
navController.navigate(AuthScreenDestination) {
popUpTo(0) { inclusive = false }
}
}
}
NavHost( NavHost(
modifier = modifier, modifier = modifier,
enterTransition = { EnterTransition.None }, enterTransition = { EnterTransition.None },
exitTransition = { ExitTransition.None }, exitTransition = { ExitTransition.None },
navController = navController, navController = navController,
startDestination = if (isAuthorized) MainScreenDestination else AuthScreenDestination, startDestination = SplashScreenDestination,
) { ) {
composable<SplashScreenDestination> {
SplashScreen(navController = navController)
}
composable<AuthScreenDestination> { composable<AuthScreenDestination> {
AuthScreen(navController = navController) AuthScreen(navController = navController)
} }

View File

@@ -1,28 +1,20 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
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.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -31,60 +23,55 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
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.BaseButton
import ru.myitschool.work.ui.BaseInputText
import ru.myitschool.work.ui.BaseText12
import ru.myitschool.work.ui.BaseText24
import ru.myitschool.work.ui.Logo
import ru.myitschool.work.ui.nav.MainScreenDestination import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.theme.Blue
import ru.myitschool.work.ui.theme.Red
import ru.myitschool.work.ui.theme.White
@Composable @Composable
fun AuthScreen( fun AuthScreen(
viewModel: AuthViewModel = viewModel(factory = AuthViewModelFactory(LocalContext.current)), viewModel: AuthViewModel = viewModel(),
navController: NavController navController: NavController,
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action -> viewModel.actionFlow.collect {
when (action) { navController.navigate(MainScreenDestination)
is AuthAction.NavigateToMain -> {
navController.navigate(MainScreenDestination) {
popUpTo(0)
}
}
}
} }
} }
Box( Box(
modifier = Modifier contentAlignment = Alignment.Center,
.fillMaxSize() modifier = Modifier.fillMaxSize()
.padding(all = 24.dp),
contentAlignment = Alignment.Center
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
verticalArrangement = Arrangement.Center .width(400.dp)
.fillMaxHeight()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text(
Logo()
BaseText24(
text = stringResource(R.string.auth_title), text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineSmall, textAlign = TextAlign.Center
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 32.dp)
) )
when (val currentState = state) { when (state) {
is AuthState.Data -> Content(viewModel)
is AuthState.Loading -> { is AuthState.Loading -> {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier.size(64.dp) modifier = Modifier
) .padding(top = 40.dp)
} .size(64.dp)
is AuthState.Data -> {
Content(
state = currentState,
// ?????? bug fix
onTextChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
onSendClick = {
keyboardController?.hide()
viewModel.onIntent(AuthIntent.Send(it))
}
) )
} }
} }
@@ -94,48 +81,50 @@ fun AuthScreen(
@Composable @Composable
private fun Content( private fun Content(
state: AuthState.Data, viewModel: AuthViewModel
onTextChange: (String) -> Unit,
onSendClick: (String) -> Unit
) { ) {
val isButtonEnabled = state.code.length == 4 &&
state.code.matches(Regex("^[a-zA-Z0-9]{4}$")) val isButtonEnabled by viewModel.isButtonEnabled.collectAsState()
val errorStateValue by viewModel.errorStateValue.collectAsState()
val textState by viewModel.textState.collectAsState()
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.padding(vertical = 20.dp)
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
if (state.error != null) {
Text(
text = state.error,
color = Color.Red,
modifier = Modifier
.testTag(TestIds.Auth.ERROR)
.padding(bottom = 16.dp)
)
}
TextField( BaseInputText(
value = textState,
placeholder = stringResource(R.string.auth_label),
modifier = Modifier modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT) .testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(), .fillMaxWidth(),
value = state.code, onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) }
onValueChange = onTextChange,
label = { Text(stringResource(R.string.auth_label)) },
singleLine = true,
isError = state.error != null
) )
Spacer(modifier = Modifier.height(16.dp)) if (errorStateValue != "") {
BaseText12(
text = errorStateValue,
color = Red,
modifier = Modifier
.testTag(TestIds.Auth.ERROR)
.padding(
start = 10.dp,
top = 5.dp,
bottom = 0.dp
)
.fillMaxWidth()
)
}
}
Button( BaseButton(
text = stringResource(R.string.auth_sign_in),
onClick = { viewModel.onIntent(AuthIntent.Send(textState)) },
btnColor = Blue,
enable = isButtonEnabled,
btnContentColor = White,
modifier = Modifier modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON) .testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(), .fillMaxWidth()
onClick = { onSendClick(state.code) }, )
enabled = isButtonEnabled
) {
Text(stringResource(R.string.auth_sign_in))
}
}
} }

View File

@@ -1,9 +1,6 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
sealed interface AuthState { sealed interface AuthState {
object Loading : AuthState object Loading: AuthState
data class Data( object Data: AuthState
val code: String = "",
val error: String? = null
) : AuthState
} }

View File

@@ -1,88 +1,62 @@
package ru.myitschool.work.ui.screen.auth package ru.myitschool.work.ui.screen.auth
import android.content.Context import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.*
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 kotlinx.coroutines.launch
import ru.myitschool.work.App
import ru.myitschool.work.data.datastore.UserCode
import ru.myitschool.work.data.repo.AuthRepository import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
class LoginViewModel(application: Application) : AndroidViewModel(application) {
class AuthViewModel( private val store by lazy { (getApplication() as App).dataStoreManager }
private val authRepository: AuthRepository,
private val checkAndSaveAuthCodeUseCase: CheckAndSaveAuthCodeUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow() private val _screenState = MutableStateFlow<LoginState>(LoginState.Ready)
val actionFlow: SharedFlow<AuthAction> = _actionFlow val screenState: StateFlow<LoginState> = _screenState.asStateFlow()
fun onIntent(intent: AuthIntent) { private val _navigate = MutableSharedFlow<Unit>()
val navigate: SharedFlow<Unit> = _navigate
private val _input = MutableStateFlow("")
val input: StateFlow<String> get() = _input
private val _error = MutableStateFlow("")
val error: StateFlow<String> get() = _error
val isValid = input.map { it.length == 4 && it.matches(Regex("[A-Za-z0-9]+")) }
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
fun perform(intent: LoginIntent) {
when (intent) { when (intent) {
is AuthIntent.Send -> {
if (validateCode(intent.text)) {
viewModelScope.launch {
_uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold( is LoginIntent.Type -> {
_input.value = intent.value
_error.value = ""
}
is LoginIntent.Submit -> {
authorize()
}
}
}
private fun authorize() {
viewModelScope.launch(Dispatchers.IO) {
_screenState.value = LoginState.Loading
AuthRepository.validateCode(_input.value).fold(
onSuccess = { onSuccess = {
_actionFlow.emit(AuthAction.NavigateToMain) store.saveUserCode(UserCode(_input.value))
_navigate.emit(Unit)
}, },
onFailure = { error -> onFailure = { err ->
_uiState.update { _screenState.value = LoginState.Ready
AuthState.Data( _error.value = err.message ?: "Ошибка"
code = intent.text,
error = error.message ?: "error"
)
}
} }
) )
} }
} else {
_uiState.update {
AuthState.Data(
code = intent.text,
error = "wrong"
)
}
}
}
is AuthIntent.TextInput -> {
_uiState.update {
AuthState.Data(
code = intent.text,
error = null
)
}
}
}
}
private fun validateCode(code: String): Boolean {
return code.length == 4 && code.matches(Regex("^[a-zA-Z0-9]{4}$"))
} }
} }
class AuthViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AuthViewModel::class.java)) {
val authRepository = AuthRepository.getInstance(context)
val useCase = CheckAndSaveAuthCodeUseCase(authRepository)
return AuthViewModel(authRepository, useCase) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
sealed interface AuthAction {
object NavigateToMain : AuthAction
}

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.ui.screen.book
sealed interface BookAction {
object Auth: BookAction
object Main: BookAction
}

View File

@@ -1,9 +1,12 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
sealed interface BookIntent { sealed interface BookIntent {
object Refresh : BookIntent object Back: BookIntent
object Back : BookIntent object LoadBooking: BookIntent
object Book : BookIntent object Book : BookIntent
data class SelectDate(val index: Int) : BookIntent data class SelectDate(val date: String) : BookIntent
data class SelectPlace(val index: Int) : BookIntent data class SelectPlace(
val placeId: Int,
val placeName: String
) : BookIntent
} }

View File

@@ -1,255 +1,415 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
import androidx.compose.foundation.layout.* import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.Image
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.background
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.border
import androidx.compose.material3.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.LaunchedEffect import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.collectAsState import androidx.compose.foundation.layout.FlowRow
import androidx.compose.runtime.getValue import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.Alignment import androidx.compose.foundation.layout.Row
import androidx.compose.ui.Modifier import androidx.compose.foundation.layout.Spacer
import androidx.compose.ui.platform.testTag import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.ui.res.stringResource import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.fillMaxWidth
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.compose.foundation.layout.height
import androidx.navigation.NavController import androidx.compose.foundation.layout.padding
import ru.myitschool.work.R import androidx.compose.foundation.layout.size
import ru.myitschool.work.core.TestIds import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon import androidx.compose.foundation.selection.selectable
import androidx.compose.ui.platform.LocalContext import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.res.painterResource import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.text.style.TextAlign import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.CircularProgressIndicator
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.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds.Book
import ru.myitschool.work.domain.book.entities.BookingEntity
import ru.myitschool.work.domain.book.entities.PlaceInfo
import ru.myitschool.work.formatBookingDate
import ru.myitschool.work.ui.BaseButton
import ru.myitschool.work.ui.BaseText16
import ru.myitschool.work.ui.BaseText24
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
import ru.myitschool.work.ui.theme.Black
import ru.myitschool.work.ui.theme.Blue
import ru.myitschool.work.ui.theme.Typography
import ru.myitschool.work.ui.theme.White
@OptIn(ExperimentalMaterial3Api::class) @Composable
@Composable fun BookScreen(
fun BookScreen( navController: NavController,
viewModel: BookViewModel = viewModel(factory = BookViewModelFactory(LocalContext.current)), viewModel: BookViewModel = viewModel(),
navController: NavController ) {
) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action -> viewModel.actionFlow.collect { action ->
when (action) { when(action) {
BookAction.NavigateBack -> { is BookAction.Auth -> navController.navigate(AuthScreenDestination)
navController.popBackStack()
} is BookAction.Main -> navController.navigate(MainScreenDestination)
BookAction.NavigateBackWithRefresh -> {
navController.previousBackStackEntry?.savedStateHandle?.set(
"shouldRefresh", true)
navController.popBackStack()
}
} }
} }
} }
when (val currentState = state) { when(state) {
BookState.Loading -> { is BookState.Loading -> {
Box( Box(
modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center,
contentAlignment = Alignment.Center modifier = Modifier.fillMaxSize()
) { ) {
CircularProgressIndicator() CircularProgressIndicator(
} modifier = Modifier.size(64.dp)
}
BookState.Empty -> {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.book_title)) },
navigationIcon = {
IconButton(
onClick = { viewModel.onIntent(BookIntent.Back) },
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON)
) {
Icon(
painter = painterResource(id = R.drawable.back),
contentDescription = null
) )
} }
} }
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.book_empty),
modifier = Modifier.testTag(TestIds.Book.EMPTY),
textAlign = TextAlign.Center
)
}
}
}
is BookState.Data -> { is BookState.Data -> {
if (currentState.error != null) { val dataState = state as BookState.Data
Scaffold( DataContent(
topBar = { viewModel = viewModel,
TopAppBar( bookingData = dataState.userBooking,
title = { Text(stringResource(R.string.book_title)) }, selectedDate = dataState.selectedDate,
navigationIcon = { selectedPlaceId = dataState.selectedPlaceId
IconButton( )
onClick = { viewModel.onIntent(BookIntent.Back) }, }
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON) is BookState.Error -> ErrorContent(viewModel)
is BookState.Empty -> EmptyContent(viewModel)
}
}
@Composable
fun EmptyContent(
viewModel: BookViewModel
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) { ) {
Icon(
painter = painterResource(id = R.drawable.back),
contentDescription = null
)
}
}
)
}
) { paddingValues ->
Column( Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center modifier = Modifier
.padding(15.dp)
.fillMaxHeight()
.width(320.dp)
) { ) {
Text(
text = currentState.error, Spacer(modifier = Modifier.height(80.dp))
modifier = Modifier.testTag(TestIds.Book.ERROR),
color = MaterialTheme.colorScheme.error, BaseText24(
text = stringResource(R.string.book_all_booked),
modifier = Modifier.testTag(Book.EMPTY),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Spacer(modifier = Modifier.height(16.dp))
Button( Spacer(modifier = Modifier.height(20.dp))
onClick = { viewModel.onIntent(BookIntent.Refresh) },
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON) BaseButton(
) { text = stringResource(R.string.book_back),
Text(stringResource(R.string.book_refresh)) modifier = Modifier
} .fillMaxWidth()
} .testTag(Book.BACK_BUTTON),
}
} else {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.book_title)) },
navigationIcon = {
IconButton(
onClick = { viewModel.onIntent(BookIntent.Back) }, onClick = { viewModel.onIntent(BookIntent.Back) },
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON) btnContentColor = White,
btnColor = Blue
)
}
}
}
@Composable
fun ErrorContent(
viewModel: BookViewModel
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) { ) {
Icon(
painter = painterResource(id = R.drawable.back),
contentDescription = null
)
}
}
)
}
) { paddingValues ->
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.fillMaxSize() .padding(15.dp)
.padding(paddingValues) .fillMaxHeight()
.padding(horizontal = 16.dp, vertical = 8.dp) .width(320.dp)
) { ) {
Content(
state = currentState,
onDateSelect = { viewModel.onIntent(BookIntent.SelectDate(it)) },
onPlaceSelect = { viewModel.onIntent(BookIntent.SelectPlace(it)) },
onBookClick = { viewModel.onIntent(BookIntent.Book) }
)
}
}
}
}
}
}
@Composable Spacer(modifier = Modifier.height(80.dp))
private fun Content(
state: BookState.Data, BaseText24(
onDateSelect: (Int) -> Unit, text = stringResource(R.string.book_error),
onPlaceSelect: (Int) -> Unit, modifier = Modifier.testTag(Book.ERROR),
onBookClick: () -> Unit textAlign = TextAlign.Center
) { )
Column(
Spacer(modifier = Modifier.height(20.dp))
BaseButton(
border = BorderStroke(1.dp, Blue),
text = stringResource(R.string.book_back),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxWidth()
.verticalScroll(rememberScrollState()) .testTag(Book.BACK_BUTTON),
) { onClick = { viewModel.onIntent(BookIntent.Back) },
ScrollableTabRow( btnContentColor = Blue,
selectedTabIndex = state.selectedDateIndex, btnColor = Color.Transparent
modifier = Modifier.fillMaxWidth(), )
edgePadding = 0.dp
) { Spacer(modifier = Modifier.height(15.dp))
state.dates.forEachIndexed { index, dateItem ->
Tab( BaseButton(
selected = state.selectedDateIndex == index, text = stringResource(R.string.main_update),
onClick = { onDateSelect(index) }, modifier = Modifier
modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)) .fillMaxWidth()
) { .testTag(Book.REFRESH_BUTTON),
Column( onClick = { viewModel.onIntent(BookIntent.LoadBooking) },
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp) btnContentColor = White,
) { btnColor = Blue
Text(
text = dateItem.displayDate,
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE)
) )
} }
} }
} }
}
Spacer(modifier = Modifier.height(16.dp)) @Composable
fun DataContent(
viewModel: BookViewModel,
bookingData: BookingEntity,
selectedDate: String,
selectedPlaceId: Int
) {
val availableDates = bookingData.bookings
.filter { it.value.isNotEmpty() }
.keys
.sorted()
val placesForSelectedDate = bookingData.bookings[selectedDate] ?: emptyList()
val selectedDate = state.dates.getOrNull(state.selectedDateIndex)
if (selectedDate != null) {
Column { Column {
selectedDate.places.forEachIndexed { index, placeItem -> Row(
modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
.background(Blue)
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
BaseText24(
text = stringResource(R.string.book_new_book),
color = White,
modifier = Modifier.padding(start = 15.dp)
)
BaseButton(
text = stringResource(R.string.book_back),
modifier = Modifier.testTag(Book.BACK_BUTTON),
onClick = { viewModel.onIntent(BookIntent.Back) }
)
}
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 20.dp, horizontal = 10.dp)
.clip(RoundedCornerShape(16.dp))
.background(White)
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
.padding(13.dp)
) {
Column {
Text(
text = stringResource(R.string.book_available_date),
style = Typography.bodyMedium,
fontSize = 16.sp,
)
BookDateList(
dates = availableDates,
selectedDate = selectedDate,
onDateSelected = { date ->
viewModel.onIntent(BookIntent.SelectDate(date))
}
)
Text(
text = stringResource(R.string.book_choose_place),
style = Typography.bodyMedium,
fontSize = 16.sp,
)
BookPlaceList(
places = placesForSelectedDate,
selectedPlaceId = selectedPlaceId,
onPlaceSelected = { placeId, placeName ->
viewModel.onIntent(BookIntent.SelectPlace(placeId, placeName))
}
)
}
BaseButton(
enable = selectedPlaceId != -1,
text = stringResource(R.string.booking_button),
btnColor = Blue,
btnContentColor = White,
onClick = { viewModel.onIntent(BookIntent.Book) },
modifier = Modifier
.testTag(Book.BOOK_BUTTON)
.padding(horizontal = 10.dp)
.fillMaxWidth(),
)
}
}
}
}
@Composable
fun BookPlaceList(
places: List<PlaceInfo>,
selectedPlaceId: Int,
onPlaceSelected: (Int, String) -> Unit
) {
Column(
modifier = Modifier.padding(vertical = 15.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (places.isEmpty()) {
Text(
text = "Нет доступных мест для выбранной даты",
color = Color.Gray,
style = Typography.bodyMedium,
modifier = Modifier.padding(vertical = 8.dp)
)
} else {
places.forEachIndexed { index, placeInfo ->
BookPlaceListElement(
placeInfo = placeInfo,
isSelected = placeInfo.id == selectedPlaceId,
onPlaceSelected = { onPlaceSelected(placeInfo.id, placeInfo.place) },
index = index
)
}
}
}
}
@Composable
fun BookPlaceListElement(
placeInfo: PlaceInfo,
isSelected: Boolean,
onPlaceSelected: () -> Unit,
index: Int
) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.selectable( .selectable(
selected = state.selectedPlaceIndex == index, selected = isSelected,
onClick = { onPlaceSelect(index) } onClick = onPlaceSelected
) )
.padding(vertical = 8.dp, horizontal = 16.dp) .testTag(Book.getIdPlaceItemByPosition(index))
.testTag(TestIds.Book.getIdPlaceItemByPosition(index)) .padding(vertical = 12.dp, horizontal = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) { ) {
RadioButton( BaseText16(
selected = state.selectedPlaceIndex == index, text = placeInfo.place,
onClick = { onPlaceSelect(index) }, modifier = Modifier.testTag(Book.ITEM_PLACE_TEXT)
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR)
) )
Spacer(modifier = Modifier.width(16.dp)) Box(
Text(
text = placeItem.name,
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onBookClick,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .size(24.dp)
.testTag(TestIds.Book.BOOK_BUTTON), .border(
enabled = state.selectedPlaceIndex != null width = 2.dp,
color = if (isSelected) Blue else Color.Gray,
shape = CircleShape
)
.background(
color = if (isSelected) Blue else Color.Transparent,
shape = CircleShape
)
.testTag(Book.ITEM_PLACE_SELECTOR)
) { ) {
Text(stringResource(R.string.book_book)) if (isSelected) {
Box(
modifier = Modifier
.size(12.dp)
.background(Color.White, CircleShape)
.align(Alignment.Center)
)
}
}
}
}
@Composable
fun BookDateList(
dates: List<String>,
selectedDate: String,
onDateSelected: (String) -> Unit
) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(7.dp),
modifier = Modifier.padding(vertical = 15.dp)
) {
dates.forEachIndexed { index, date ->
BookDateListElement(
date = date,
isSelected = date == selectedDate,
onClick = { onDateSelected(date) },
index = index
)
}
}
}
@Composable
fun BookDateListElement(
date: String,
isSelected: Boolean,
onClick: () -> Unit,
index: Int
) {
Button(
contentPadding = PaddingValues(0.dp),
modifier = Modifier
.testTag(Book.getIdDateItemByPosition(index))
.padding(0.dp),
border = BorderStroke(1.dp, if (isSelected) Blue else Black,),
onClick = onClick,
colors = ButtonColors(
contentColor = if (isSelected) White else Black,
containerColor = if (isSelected) Blue else Color.Transparent,
disabledContentColor = Black,
disabledContainerColor = Color.Transparent),
) {
val formattedDate = date.formatBookingDate()
BaseText16(
text = formattedDate,
modifier = Modifier.testTag(Book.ITEM_DATE),
color = if (isSelected) White else Black,
)
} }
} }
}

View File

@@ -1,24 +1,15 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
import ru.myitschool.work.domain.book.entities.BookingEntity
sealed interface BookState { sealed interface BookState {
object Loading : BookState object Loading: BookState
data class Data( data class Data(
val dates: List<DateItem> = emptyList(), val userBooking: BookingEntity,
val selectedDateIndex: Int = 0, val selectedDate: String = "",
val selectedPlaceIndex: Int? = null, val selectedPlaceId: Int = -1,
val error: String? = null val selectedPlaceName: String = ""
) : BookState ): BookState
object Empty : BookState object Error: BookState
object Empty: BookState
} }
data class DateItem(
val id: String,
val displayDate: String, // dd.MM
val rawDate: String, // for api
val places: List<PlaceItem>
)
data class PlaceItem(
val id: String,
val name: String
)

View File

@@ -1,196 +1,165 @@
package ru.myitschool.work.ui.screen.book package ru.myitschool.work.ui.screen.book
import android.content.Context import android.app.Application
import androidx.lifecycle.ViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.data.model.PlaceInfo import ru.myitschool.work.App
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.BookRepository import ru.myitschool.work.data.repo.BookRepository
import java.text.SimpleDateFormat import ru.myitschool.work.data.repo.MainRepository
import java.util.Date import ru.myitschool.work.domain.book.BookingUseCase
import java.util.Locale import ru.myitschool.work.domain.book.LoadBookingUseCase
import ru.myitschool.work.domain.main.LoadDataUseCase
import ru.myitschool.work.ui.screen.main.MainAction
import ru.myitschool.work.ui.screen.main.MainIntent
import ru.myitschool.work.ui.screen.main.MainState
import kotlin.text.isEmpty
class BookViewModel(private val bookRepo: BookRepository) : ViewModel() { class BookViewModel(application: Application) : AndroidViewModel(application) {
private val loadBookingUseCase by lazy { LoadBookingUseCase(BookRepository) }
private val bookingUseCase by lazy { BookingUseCase (BookRepository) }
private val dataStoreManager by lazy {
(getApplication() as App).dataStoreManager
}
private val _uiState = MutableStateFlow<BookState>(BookState.Loading) private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
val uiState: StateFlow<BookState> = _uiState.asStateFlow() val uiState: StateFlow<BookState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<BookAction> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<BookAction> = MutableSharedFlow()
val actionFlow: SharedFlow<BookAction> = _actionFlow val actionFlow: SharedFlow<BookAction> = _actionFlow
init { init {
loadData() loadBooking()
}
private fun bookSelectedPlace() {
viewModelScope.launch(Dispatchers.IO) {
try {
val userCode = dataStoreManager.getUserCode().first()
val currentState = _uiState.value
if (currentState is BookState.Data && currentState.selectedPlaceId != -1) {
bookingUseCase.invoke(
userCode = userCode.code,
date = currentState.selectedDate,
placeId = currentState.selectedPlaceId,
placeName = currentState.selectedPlaceName
).fold(
onSuccess = {
_actionFlow.emit(BookAction.Main)
},
onFailure = { error ->
error.printStackTrace()
_uiState.update { BookState.Error }
}
)
}
} catch (error: Exception) {
error.printStackTrace()
_uiState.update { BookState.Error }
}
}
}
private fun loadBooking() {
viewModelScope.launch(Dispatchers.IO) {
_uiState.update { BookState.Loading }
try {
val userCode = dataStoreManager.getUserCode().first()
if (userCode.code.isEmpty()) {
_actionFlow.emit(BookAction.Auth)
return@launch
}
loadBookingUseCase.invoke(userCode.code).fold(
onSuccess = { data ->
val availableDates = data.bookings
.filter { it.value.isNotEmpty() }
.keys
.sorted()
if (availableDates.isEmpty()) {
_uiState.update { BookState.Empty }
} else {
val selectedDate = availableDates.first()
val placesForSelectedDate = data.bookings[selectedDate] ?: emptyList()
val selectedPlaceId = placesForSelectedDate.firstOrNull()?.id ?: -1
val selectedPlaceName = placesForSelectedDate.firstOrNull()?.place ?: ""
_uiState.update {
BookState.Data(
userBooking = data,
selectedDate = selectedDate,
selectedPlaceId = selectedPlaceId,
selectedPlaceName = selectedPlaceName
)
}
}
},
onFailure = { error ->
error.printStackTrace()
_uiState.update { BookState.Error }
}
)
} catch (error: Exception) {
error.printStackTrace()
_uiState.update { BookState.Error }
}
}
} }
fun onIntent(intent: BookIntent) { fun onIntent(intent: BookIntent) {
when (intent) { when (intent) {
BookIntent.Refresh -> { is BookIntent.LoadBooking -> loadBooking()
loadData()
} is BookIntent.Back -> {
BookIntent.Back -> { viewModelScope.launch(Dispatchers.Default) {
viewModelScope.launch { _actionFlow.emit(BookAction.Main)
_actionFlow.emit(BookAction.NavigateBack)
} }
} }
BookIntent.Book -> {
bookSelected() is BookIntent.Book -> bookSelectedPlace()
}
is BookIntent.SelectDate -> { is BookIntent.SelectDate -> {
_uiState.update { state -> val currentState = _uiState.value
if (state is BookState.Data) { if (currentState is BookState.Data) {
state.copy( val placesForDate =
selectedDateIndex = intent.index, currentState.userBooking.bookings[intent.date] ?: emptyList()
selectedPlaceIndex = null val newSelectedPlaceId = placesForDate.firstOrNull()?.id ?: -1
val newSelectedPlaceName = placesForDate.firstOrNull()?.place ?: ""
_uiState.update {
currentState.copy(
selectedDate = intent.date,
selectedPlaceId = newSelectedPlaceId,
selectedPlaceName = newSelectedPlaceName
) )
} else {
state
} }
} }
} }
is BookIntent.SelectPlace -> { is BookIntent.SelectPlace -> {
_uiState.update { state -> val currentState = _uiState.value
if (state is BookState.Data) { if (currentState is BookState.Data) {
state.copy(selectedPlaceIndex = intent.index)
} else {
state
}
}
}
}
}
private fun loadData() {
viewModelScope.launch {
_uiState.update { BookState.Loading }
bookRepo.getAvailableBookings().fold(
onSuccess = { response ->
val datesMap = response ?: emptyMap()
val dateItems = datesMap.entries
.sortedBy { (date, _) -> parseDate(date) }
.filter { (_, places) -> places.isNotEmpty() }
.map { (dateString, places) ->
DateItem(
id = dateString,
displayDate = formatDateForDisplay(dateString),
rawDate = dateString,
places = places.map { placeInfo ->
PlaceItem(
id = placeInfo.id.toString(),
name = placeInfo.place
)
}
)
}
if (dateItems.isEmpty()) {
_uiState.update { BookState.Empty }
} else {
_uiState.update { _uiState.update {
BookState.Data( currentState.copy(
dates = dateItems, selectedPlaceId = intent.placeId,
selectedDateIndex = 0 selectedPlaceName = intent.placeName
)
}
}
},
onFailure = { error ->
_uiState.update {
BookState.Data(
error = error.message ?: "Ошибка загрузки данных"
)
}
}
)
}
}
private fun bookSelected() {
viewModelScope.launch {
val state = _uiState.value
if (state is BookState.Data) {
val selectedDate = state.dates.getOrNull(state.selectedDateIndex)
val selectedPlaceIndex = state.selectedPlaceIndex
if (selectedDate != null && selectedPlaceIndex != null) {
val selectedPlace = selectedDate.places.getOrNull(selectedPlaceIndex)
if (selectedPlace != null) {
_uiState.update { BookState.Loading }
bookRepo.book(
date = selectedDate.rawDate,
placeId = selectedPlace.id.toInt()
).fold(
onSuccess = {
_actionFlow.emit(BookAction.NavigateBackWithRefresh)
},
onFailure = { error ->
_uiState.update {
state.copy(
error = error.message ?: "Ошибка бронирования"
)
}
}
)
} else {
_uiState.update {
state.copy(
error = "Место не выбрано"
)
}
}
} else {
_uiState.update {
state.copy(
error = "Выберите место для бронирования"
) )
} }
} }
} }
} }
} }
private fun parseDate(dateString: String): Long {
return try {
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
format.parse(dateString)?.time ?: 0L
} catch (e: Exception) {
0L
}
}
private fun formatDateForDisplay(dateString: String): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val outputFormat = SimpleDateFormat("dd.MM", Locale.getDefault())
val date = inputFormat.parse(dateString)
date?.let { outputFormat.format(it) } ?: dateString
} catch (e: Exception) {
dateString
}
}
}
sealed interface BookAction {
object NavigateBack : BookAction
object NavigateBackWithRefresh : BookAction
}
class BookViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BookViewModel::class.java)) {
val authRepository = AuthRepository.getInstance(context)
val bookRepository = BookRepository(authRepository)
return BookViewModel(bookRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
} }

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainAction {
object Booking: MainAction
object Auth: MainAction
}

View File

@@ -1,8 +1,7 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
sealed interface MainIntent { sealed interface MainIntent {
object LoadData : MainIntent object Logout: MainIntent
object Booking : MainIntent object Booking: MainIntent
object Logout : MainIntent object LoadData: MainIntent
} }

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@@ -16,10 +17,7 @@ import androidx.compose.foundation.layout.width
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.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -29,38 +27,45 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment 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.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
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.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController import androidx.navigation.NavController
import coil3.compose.rememberAsyncImagePainter import coil3.compose.rememberAsyncImagePainter
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.Main
import ru.myitschool.work.domain.main.entities.BookingInfo
import ru.myitschool.work.domain.main.entities.UserEntity
import ru.myitschool.work.ui.BaseButton
import ru.myitschool.work.ui.BaseText14
import ru.myitschool.work.ui.BaseText16
import ru.myitschool.work.ui.BaseText20
import ru.myitschool.work.ui.BaseText24
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.theme.Black
import ru.myitschool.work.ui.theme.Blue
import ru.myitschool.work.ui.theme.LightGray
import ru.myitschool.work.ui.theme.Typography
import ru.myitschool.work.ui.theme.White
@Composable @Composable
fun MainScreen( fun MainScreen(
navController: NavController, navController: NavController,
viewModel: MainViewModel = viewModel() viewModel: MainViewModel = viewModel()
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action -> viewModel.actionFlow.collect { action ->
when(action) { when(action) {
is MainAction.Auth -> { is MainAction.Auth -> navController.navigate(AuthScreenDestination)
navController.navigate(AuthScreenDestination) {
popUpTo(0)
}
}
is MainAction.Booking -> navController.navigate(BookScreenDestination) is MainAction.Booking -> navController.navigate(BookScreenDestination)
} }
} }
@@ -68,6 +73,7 @@ fun MainScreen(
when(state) { when(state) {
is MainState.Loading -> { is MainState.Loading -> {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -91,8 +97,6 @@ fun MainScreen(
@Composable @Composable
fun ErrorContent(viewModel: MainViewModel){ fun ErrorContent(viewModel: MainViewModel){
val configuration = LocalConfiguration.current
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@@ -101,32 +105,29 @@ fun ErrorContent(viewModel: MainViewModel){
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.padding(15.dp) .padding(15.dp)
.fillMaxSize() .fillMaxHeight()
.width(320.dp)
) { ) {
Spacer(modifier = Modifier.height(80.dp)) Spacer(modifier = Modifier.height(80.dp))
Text( BaseText24(
text = stringResource(R.string.data_error_message), text = stringResource(R.string.data_error_message),
modifier = Modifier.testTag(TestIds.Main.ERROR), modifier = Modifier.testTag(Main.ERROR),
textAlign = TextAlign.Center, textAlign = TextAlign.Center
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.error
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Button( BaseButton(
onClick = { viewModel.onIntent(MainIntent.LoadData) }, text = stringResource(R.string.main_update),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(TestIds.Main.REFRESH_BUTTON), .testTag(Main.REFRESH_BUTTON),
colors = ButtonDefaults.buttonColors( onClick = { viewModel.onIntent(MainIntent.LoadData) },
containerColor = MaterialTheme.colorScheme.primary, btnContentColor = White,
contentColor = MaterialTheme.colorScheme.onPrimary btnColor = Blue
) )
) {
Text(stringResource(R.string.main_refresh))
}
} }
} }
} }
@@ -134,143 +135,127 @@ fun ErrorContent(viewModel: MainViewModel){
@Composable @Composable
fun DataContent( fun DataContent(
viewModel: MainViewModel, viewModel: MainViewModel,
userData: ru.myitschool.work.domain.main.entities.UserEntity userData: UserEntity
) { ) {
val configuration = LocalConfiguration.current
Column ( Column (
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.background(LightGray)
.fillMaxSize() .fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant) .width(400.dp)
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp)) .clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
.background(MaterialTheme.colorScheme.primary) .background(Blue)
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(10.dp)
) { ) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Button( BaseButton(
text = stringResource(R.string.main_update),
onClick = { viewModel.onIntent(MainIntent.LoadData) }, onClick = { viewModel.onIntent(MainIntent.LoadData) },
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON), modifier = Modifier.testTag(Main.REFRESH_BUTTON)
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) )
) { BaseButton(
Text(stringResource(R.string.main_refresh)) text = stringResource(R.string.main_log_out),
}
Button(
onClick = { viewModel.onIntent(MainIntent.Logout) }, onClick = { viewModel.onIntent(MainIntent.Logout) },
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON), modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) )
) {
Text(stringResource(R.string.main_logout))
}
} }
Image( Image(
painter = rememberAsyncImagePainter( painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current) model = userData.photoUrl,
.data(userData.photoUrl) error = painterResource(R.drawable.github)
.build()
), ),
contentDescription = stringResource(R.string.main_avatar_description), contentDescription = stringResource(R.string.main_avatar_description),
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(999.dp)) .clip(RoundedCornerShape(999.dp))
.testTag(TestIds.Main.PROFILE_IMAGE) .testTag(Main.PROFILE_IMAGE)
.size(150.dp) .width(150.dp)
.padding(20.dp), .height(150.dp)
contentScale = ContentScale.Crop .padding(20.dp)
) )
Text( BaseText20(
text = userData.name, text = userData.name,
color = MaterialTheme.colorScheme.onPrimary, color = White,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.testTag(TestIds.Main.PROFILE_NAME) .testTag(Main.PROFILE_NAME)
.padding(horizontal = 20.dp), .width(250.dp),
style = MaterialTheme.typography.headlineSmall style = Typography.bodyLarge
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
} }
Column( Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(20.dp) .padding(20.dp)
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(MaterialTheme.colorScheme.surface) .background(White)
.padding(16.dp) ) {
Column(
modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = "Мои бронирования:", text = stringResource(R.string.main_booking_title),
style = MaterialTheme.typography.titleMedium, style = Typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface, color = Black,
modifier = Modifier.padding(bottom = 16.dp) fontSize = 16.sp,
modifier = Modifier.padding(
horizontal = 10.dp,
vertical = 20.dp
)
) )
if (userData.hasBookings()) { if (userData.hasBookings()) {
SortedBookingList(userData = userData) SortedBookingList(userData = userData)
} else { } else {
EmptyBookings() EmptyBookings()
} }
}
Spacer(modifier = Modifier.weight(1f)) BaseButton(
text = stringResource(R.string.booking_button),
Button( btnColor = Blue,
btnContentColor = White,
onClick = { viewModel.onIntent(MainIntent.Booking) }, onClick = { viewModel.onIntent(MainIntent.Booking) },
modifier = Modifier modifier = Modifier
.testTag(TestIds.Main.ADD_BUTTON) .testTag(Main.ADD_BUTTON)
.padding(horizontal = 10.dp, vertical = 15.dp)
.fillMaxWidth(), .fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary
) )
) {
Text(stringResource(R.string.main_add_booking))
}
} }
} }
} }
@Composable @Composable
fun SortedBookingList(userData: ru.myitschool.work.domain.main.entities.UserEntity) { fun SortedBookingList(userData: UserEntity) {
val sortedBookings = remember(userData.booking) { val sortedBookings = remember(userData.booking) {
userData.getSortedBookingsWithFormattedDate()?.sortedBy { (originalDate, _, _) -> userData.getSortedBookingsWithFormattedDate()
originalDate
}
} }
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 20.dp)
) { ) {
itemsIndexed( itemsIndexed(
items = sortedBookings ?: emptyList() items = sortedBookings
) { index, (originalDate, formattedDate, bookingInfo) -> ) { index, (originalDate, formattedDate, bookingInfo) ->
Box(
modifier = Modifier.testTag(TestIds.Main.getIdItemByPosition(index))
) {
BookingItem( BookingItem(
originalDate = originalDate, originalDate = originalDate,
formattedDate = formattedDate, formattedDate = formattedDate,
bookingInfo = bookingInfo, bookingInfo = bookingInfo,
index = index index = index
) )
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
} }
@@ -280,24 +265,23 @@ fun SortedBookingList(userData: ru.myitschool.work.domain.main.entities.UserEnti
fun BookingItem( fun BookingItem(
originalDate: String, originalDate: String,
formattedDate: String, formattedDate: String,
bookingInfo: ru.myitschool.work.domain.main.entities.BookingInfo, bookingInfo: BookingInfo,
index: Int index: Int
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.testTag(Main.getIdItemByPosition(index))
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp), .padding(vertical = 20.dp),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( BaseText14(
text = bookingInfo.place, text = bookingInfo.place,
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE), modifier = Modifier.testTag(Main.ITEM_PLACE)
style = MaterialTheme.typography.bodyMedium
) )
Text( BaseText14(
text = formattedDate, text = formattedDate,
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE), modifier = Modifier.testTag(Main.ITEM_DATE)
style = MaterialTheme.typography.bodyMedium
) )
} }
} }
@@ -310,10 +294,8 @@ fun EmptyBookings() {
.padding(16.dp), .padding(16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( BaseText16(
text = "У вас нет активных бронирований", text = stringResource(R.string.main_empty_booking)
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }

View File

@@ -1,20 +1,9 @@
package ru.myitschool.work.ui.screen.main package ru.myitschool.work.ui.screen.main
import java.time.LocalDate import ru.myitschool.work.domain.main.entities.UserEntity
import java.time.format.DateTimeFormatter
sealed interface MainState { sealed interface MainState {
object Loading : MainState data class Data(val userData: UserEntity): MainState
data class Data(val userData: ru.myitschool.work.domain.main.entities.UserEntity) : MainState object Loading: MainState
object Error : MainState object Error: MainState
}
data class BookingItem(
val id: String,
val date: LocalDate,
val place: String
) {
fun getFormattedDate(): String {
return date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
}
} }

View File

@@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.myitschool.work.App import ru.myitschool.work.App
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.MainRepository import ru.myitschool.work.data.repo.MainRepository
import ru.myitschool.work.domain.main.LoadDataUseCase import ru.myitschool.work.domain.main.LoadDataUseCase
@@ -23,21 +22,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
(getApplication() as App).dataStoreManager (getApplication() as App).dataStoreManager
} }
private val authRepository by lazy { private val loadDataUseCase by lazy { LoadDataUseCase(MainRepository) }
AuthRepository.getInstance(getApplication())
}
private val mainRepository by lazy {
MainRepository(authRepository)
}
private val loadDataUseCase by lazy {
LoadDataUseCase(mainRepository)
}
private val _uiState = MutableStateFlow<MainState>(MainState.Loading) private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow() val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow() private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
val actionFlow: SharedFlow<MainAction> = _actionFlow val actionFlow: SharedFlow<MainAction> = _actionFlow
@@ -52,12 +39,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
try { try {
val userCode = dataStoreManager.getUserCode().first() val userCode = dataStoreManager.getUserCode().first()
if (userCode.isEmpty()) { if (userCode.code.isEmpty()) {
_actionFlow.emit(MainAction.Auth) _actionFlow.emit(MainAction.Auth)
return@launch return@launch
} }
loadDataUseCase.invoke(userCode).fold( loadDataUseCase.invoke(userCode.code).fold(
onSuccess = { data -> onSuccess = { data ->
_uiState.update { MainState.Data(data) } _uiState.update { MainState.Data(data) }
}, },
@@ -73,25 +60,23 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
} }
fun onIntent(intent: MainIntent) { fun onIntent( intent: MainIntent) {
when(intent) { when(intent) {
is MainIntent.LoadData -> loadData() is MainIntent.LoadData -> loadData()
is MainIntent.Booking -> { is MainIntent.Booking -> {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
_actionFlow.emit(MainAction.Booking) _actionFlow.emit(MainAction.Booking)
} }
} }
is MainIntent.Logout -> { is MainIntent.Logout -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
authRepository.clear()
dataStoreManager.clearUserCode()
_actionFlow.emit(MainAction.Auth) _actionFlow.emit(MainAction.Auth)
} }
} }
} }
} }
} }
sealed interface MainAction {
object Auth : MainAction
object Booking : MainAction
}

View File

@@ -0,0 +1,49 @@
package ru.myitschool.work.ui.screen.splash
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.MainScreenDestination
@Composable
fun SplashScreen(
navController: NavController,
viewModel: SplashViewModel = viewModel()
) {
val splashState by viewModel.splashState.collectAsState()
LaunchedEffect(splashState) {
when (splashState) {
is SplashState.Authenticated -> {
navController.navigate(MainScreenDestination)
}
is SplashState.UnAuthenticated -> {
navController.navigate(AuthScreenDestination)
}
is SplashState.Error -> {
navController.navigate(AuthScreenDestination)
}
SplashState.Loading -> {
}
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
}
}

View File

@@ -0,0 +1,10 @@
package ru.myitschool.work.ui.screen.splash
import android.os.Message
sealed interface SplashState {
object Loading: SplashState
object Authenticated: SplashState
object UnAuthenticated: SplashState
class Error(message: String): SplashState
}

View File

@@ -0,0 +1,44 @@
package ru.myitschool.work.ui.screen.splash
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import ru.myitschool.work.App
class SplashViewModel(application: Application) : AndroidViewModel(application) {
private val dataStoreManager by lazy {
(getApplication() as App).dataStoreManager
}
private val _splashState = MutableStateFlow<SplashState>(SplashState.Loading)
val splashState: StateFlow<SplashState> = _splashState.asStateFlow()
init {
checkAuthStatus()
}
private fun checkAuthStatus() {
viewModelScope.launch {
try {
val userCode = dataStoreManager.getUserCode().first()
val isAuthenticated = if (userCode.code.isEmpty()) false else true
_splashState.value = if (isAuthenticated) {
SplashState.Authenticated
} else {
SplashState.UnAuthenticated
}
} catch (e: Exception) {
_splashState.value = SplashState.Error(e.message ?: "Unknown error")
}
}
}
}

View File

@@ -9,3 +9,17 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71) val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260) val Pink40 = Color(0xFF7D5260)
val Blue = Color(0xFF004BFF)
val Gray = Color(0xFF777777)
val LightBlue = Color(0xFFF2EFFF)
val White = Color(0xFFFFFFFF)
val Red = Color(0xFFFF4D4D)
val LightGray = Color(0xFFF2F1F7)
val Black = Color(0xFF000000)

View File

@@ -2,19 +2,27 @@ package ru.myitschool.work.ui.theme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import ru.myitschool.work.R
// Set of Material typography styles to start with // Set of Material typography styles to start with
val Typography = Typography( val Typography = Typography(
bodySmall = TextStyle(
fontWeight = FontWeight.Medium,
),
bodyMedium = TextStyle(
fontWeight = FontWeight.SemiBold,
),
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold,
fontWeight = FontWeight.Normal, ),
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override /* Other default text styles to override
titleLarge = TextStyle( titleLarge = TextStyle(
fontFamily = FontFamily.Default, fontFamily = FontFamily.Default,

View File

@@ -0,0 +1,26 @@
package ru.myitschool.work
import java.text.SimpleDateFormat
import java.util.Locale
fun String.formatDate(): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val outputFormat = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
val date = inputFormat.parse(this)
outputFormat.format(date)
} catch (e: Exception) {
this
}
}
fun String.formatBookingDate(): String {
return try {
val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val outputFormat = SimpleDateFormat("dd.MM", Locale.getDefault())
val date = inputFormat.parse(this)
outputFormat.format(date)
} catch (e: Exception) {
this
}
}

View File

@@ -1,22 +1,21 @@
<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_label">Код</string>
<string name="auth_sign_in">Войти</string> <string name="auth_sign_in">Войти</string>
<string name="main_title">Мои бронирования</string>
<string name="main_logout">Выйти</string>
<string name="main_refresh">Обновить</string>
<string name="main_add_booking">Забронировать</string>
<string name="book_title">Бронирование</string>
<string name="book_back">Назад</string>
<string name="book_book">Забронировать</string>
<string name="book_refresh">Повторить</string>
<string name="book_empty">Всё забронировано</string>
<string name="data_error_message">Ошибка загрузки данных</string>
<string name="main_update">Обновить</string> <string name="main_update">Обновить</string>
<string name="main_booking_title">Мои бронирования</string> <string name="main_log_out">Выйти</string>
<string name="main_empty_booking">У вас нет активных бронирований</string> <string name="main_avatar_description">Фото пользователя</string>
<string name="main_avatar_description">Аватар пользователя</string> <string name="main_booking_title">Ваши забронированные места</string>
<string name="add_icon_description">Добавить</string> <string name="booking_button">Бронировать</string>
<string name="add_icon_description">Иконка добавления</string>
<string name="data_error_message">Ошибка загрузки данных</string>
<string name="main_empty_booking">Нет бронирований</string>
<string name="book_new_book">Новая встреча</string>
<string name="book_back">Назад</string>
<string name="book_available_date">Доступные даты</string>
<string name="book_choose_place">Выберите место встречи</string>
<string name="book_all_booked">Всё забронировано</string>
<string name="book_error">Ошибка сервера</string>
</resources> </resources>