main #4
@@ -36,7 +36,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
defaultComposeLibrary()
|
defaultComposeLibrary()
|
||||||
implementation("androidx.datastore:datastore-preferences:1.1.7")
|
implementation("androidx.datastore:datastore-preferences:1.2.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.4.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.9.6")
|
implementation("androidx.navigation:navigation-compose:2.9.6")
|
||||||
val coil = "3.3.0"
|
val coil = "3.3.0"
|
||||||
|
|||||||
@@ -2,11 +2,21 @@ package ru.myitschool.work
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
|
import ru.myitschool.work.data.datastore.DataStoreManager
|
||||||
|
|
||||||
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "datastore")
|
||||||
|
|
||||||
class App: Application() {
|
class App: Application() {
|
||||||
|
|
||||||
|
lateinit var dataStoreManager: DataStoreManager
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
context = this
|
context = this
|
||||||
|
dataStoreManager = DataStoreManager(dataStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -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] ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package ru.myitschool.work.data.datastore
|
||||||
|
|
||||||
|
data class UserCode(
|
||||||
|
val code: String
|
||||||
|
)
|
||||||
@@ -4,13 +4,8 @@ import ru.myitschool.work.data.source.NetworkDataSource
|
|||||||
|
|
||||||
object AuthRepository {
|
object AuthRepository {
|
||||||
|
|
||||||
private var codeCache: String? = null
|
|
||||||
|
|
||||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
return NetworkDataSource.checkAuth(text)
|
||||||
if (success) {
|
|
||||||
codeCache = text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
import ru.myitschool.work.domain.book.entities.BookingEntity
|
||||||
|
import ru.myitschool.work.domain.main.entities.UserEntity
|
||||||
|
|
||||||
|
object BookRepository {
|
||||||
|
|
||||||
|
suspend fun loadBooking(text: String): Result<BookingEntity> {
|
||||||
|
return NetworkDataSource.loadBooking(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun bookPlace(
|
||||||
|
userCode: String,
|
||||||
|
date: String,
|
||||||
|
placeId: Int,
|
||||||
|
placeName: String
|
||||||
|
): Result<Unit> {
|
||||||
|
return NetworkDataSource.bookPlace(userCode, date, placeId, placeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ru.myitschool.work.data.repo
|
||||||
|
|
||||||
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
import ru.myitschool.work.domain.main.entities.UserEntity
|
||||||
|
|
||||||
|
object MainRepository {
|
||||||
|
|
||||||
|
suspend fun loadData(text: String): Result<UserEntity> {
|
||||||
|
return NetworkDataSource.loadData(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
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.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.request.post
|
||||||
|
import io.ktor.client.request.setBody
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
@@ -11,6 +15,30 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
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.domain.book.entities.BookingEntity
|
||||||
|
import ru.myitschool.work.domain.book.entities.PlaceInfo
|
||||||
|
import ru.myitschool.work.domain.main.entities.UserEntity
|
||||||
|
|
||||||
|
private const val testJson = """
|
||||||
|
{
|
||||||
|
"name": "Иванов Петр Федорович",
|
||||||
|
"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 {
|
||||||
@@ -28,9 +56,38 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun bookPlace(
|
||||||
|
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
|
||||||
when (response.status) {
|
when (response.status) {
|
||||||
HttpStatusCode.OK -> true
|
HttpStatusCode.OK -> true
|
||||||
else -> error(response.bodyAsText())
|
else -> error(response.bodyAsText())
|
||||||
@@ -38,5 +95,35 @@ object NetworkDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun loadData(code: String): Result<UserEntity> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
|
||||||
|
// Json.decodeFromString<UserEntity>(testJson) // удалить при проверке
|
||||||
|
|
||||||
|
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
response.body<UserEntity>()
|
||||||
|
}
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadBooking(code: String): Result<BookingEntity> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
|
||||||
|
// BookingEntity(Json.decodeFromString<Map<String, List<PlaceInfo>>>(testBookingJson)) // удалить при проверке
|
||||||
|
|
||||||
|
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
BookingEntity(response.body<Map<String, List<PlaceInfo>>>())
|
||||||
|
}
|
||||||
|
else -> error(response.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>>
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.domain.main.entities
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookingInfo(
|
||||||
|
val id: Int,
|
||||||
|
val place: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package ru.myitschool.work.domain.main.entities
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import ru.myitschool.work.formatDate
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserEntity(
|
||||||
|
val name: String,
|
||||||
|
val photoUrl: String,
|
||||||
|
val booking: Map<String, BookingInfo>? = null
|
||||||
|
) {
|
||||||
|
fun getSortedBookings(): List<Pair<String, BookingInfo>> {
|
||||||
|
return booking?.entries
|
||||||
|
?.sortedBy { (date, _) -> date }
|
||||||
|
?.map { it.toPair() }
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSortedBookingsWithFormattedDate(): List<Triple<String, String, BookingInfo>> {
|
||||||
|
return getSortedBookings().map { (date, bookingInfo) ->
|
||||||
|
Triple(date, date.formatDate(), bookingInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasBookings(): Boolean = !booking.isNullOrEmpty()
|
||||||
|
}
|
||||||
198
app/src/main/java/ru/myitschool/work/ui/Composables.kt
Normal file
198
app/src/main/java/ru/myitschool/work/ui/Composables.kt
Normal 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 BaseNoBackgroundButton(
|
||||||
|
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.logo),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.nav
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SplashScreenDestination: AppDestination
|
||||||
@@ -14,7 +14,11 @@ import androidx.navigation.compose.rememberNavController
|
|||||||
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.main.MainScreen
|
||||||
|
import ru.myitschool.work.ui.screen.splash.SplashScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavHost(
|
fun AppNavHost(
|
||||||
@@ -26,24 +30,19 @@ fun AppNavHost(
|
|||||||
enterTransition = { EnterTransition.None },
|
enterTransition = { EnterTransition.None },
|
||||||
exitTransition = { ExitTransition.None },
|
exitTransition = { ExitTransition.None },
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = AuthScreenDestination,
|
startDestination = SplashScreenDestination,
|
||||||
) {
|
) {
|
||||||
|
composable<SplashScreenDestination> {
|
||||||
|
SplashScreen(navController = navController)
|
||||||
|
}
|
||||||
composable<AuthScreenDestination> {
|
composable<AuthScreenDestination> {
|
||||||
AuthScreen(navController = navController)
|
AuthScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable<MainScreenDestination> {
|
composable<MainScreenDestination> {
|
||||||
Box(
|
MainScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable<BookScreenDestination> {
|
composable<BookScreenDestination> {
|
||||||
Box(
|
BookScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +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.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.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.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
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.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
|
||||||
@@ -30,12 +23,20 @@ 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(),
|
viewModel: AuthViewModel = viewModel(),
|
||||||
navController: NavController
|
navController: NavController,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsState()
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
@@ -45,24 +46,34 @@ fun AuthScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
contentAlignment = Alignment.Center,
|
||||||
.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
.padding(all = 24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
text = stringResource(R.string.auth_title),
|
modifier = Modifier
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
.width(400.dp)
|
||||||
textAlign = TextAlign.Center
|
.fillMaxHeight()
|
||||||
)
|
.padding(20.dp),
|
||||||
when (val currentState = state) {
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
is AuthState.Data -> Content(viewModel, currentState)
|
) {
|
||||||
is AuthState.Loading -> {
|
|
||||||
CircularProgressIndicator(
|
Logo()
|
||||||
modifier = Modifier.size(64.dp)
|
|
||||||
)
|
BaseText24(
|
||||||
|
text = stringResource(R.string.auth_title),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is AuthState.Data -> Content(viewModel)
|
||||||
|
is AuthState.Loading -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 40.dp)
|
||||||
|
.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,28 +81,50 @@ fun AuthScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content(
|
||||||
viewModel: AuthViewModel,
|
viewModel: AuthViewModel
|
||||||
state: AuthState.Data
|
|
||||||
) {
|
) {
|
||||||
var inputText by remember { mutableStateOf("") }
|
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
val isButtonEnabled by viewModel.isButtonEnabled.collectAsState()
|
||||||
TextField(
|
val errorStateValue by viewModel.errorStateValue.collectAsState()
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
val textState by viewModel.textState.collectAsState()
|
||||||
value = inputText,
|
|
||||||
onValueChange = {
|
Column(
|
||||||
inputText = it
|
modifier = Modifier.padding(vertical = 20.dp)
|
||||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(R.string.auth_label)) }
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
|
||||||
onClick = {
|
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
|
||||||
},
|
|
||||||
enabled = true
|
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
|
||||||
|
BaseInputText(
|
||||||
|
value = textState,
|
||||||
|
placeholder = stringResource(R.string.auth_label),
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.CODE_INPUT)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errorStateValue != "") {
|
||||||
|
BaseText12(
|
||||||
|
text = errorStateValue,
|
||||||
|
color = Red,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.ERROR)
|
||||||
|
.padding(
|
||||||
|
start = 10.dp,
|
||||||
|
top = 5.dp,
|
||||||
|
bottom = 0.dp
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BaseButton(
|
||||||
|
text = stringResource(R.string.auth_sign_in),
|
||||||
|
onClick = { viewModel.onIntent(AuthIntent.Send(textState)) },
|
||||||
|
btnColor = Blue,
|
||||||
|
enable = isButtonEnabled,
|
||||||
|
btnContentColor = White,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
@@ -10,34 +11,52 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.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
|
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
class AuthViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val dataStoreManager by lazy {
|
||||||
|
(getApplication() as App).dataStoreManager
|
||||||
|
}
|
||||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
||||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
||||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
val actionFlow: SharedFlow<Unit> = _actionFlow
|
||||||
|
private val _errorStateValue = MutableStateFlow("")
|
||||||
|
val errorStateValue: StateFlow<String> = _errorStateValue.asStateFlow()
|
||||||
|
private val _isButtonEnabled = MutableStateFlow(false)
|
||||||
|
val isButtonEnabled: StateFlow<Boolean> = _isButtonEnabled.asStateFlow()
|
||||||
|
private val _textState = MutableStateFlow("")
|
||||||
|
val textState: StateFlow<String> = _textState.asStateFlow()
|
||||||
|
|
||||||
fun onIntent(intent: AuthIntent) {
|
fun onIntent(intent: AuthIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is AuthIntent.Send -> {
|
is AuthIntent.Send -> {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_uiState.update { AuthState.Loading }
|
_uiState.update { AuthState.Loading }
|
||||||
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
dataStoreManager.saveUserCode(UserCode(code = intent.text))
|
||||||
_actionFlow.emit(Unit)
|
_actionFlow.emit(Unit)
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
error.printStackTrace()
|
error.printStackTrace()
|
||||||
_actionFlow.emit(Unit)
|
_uiState.update { AuthState.Data }
|
||||||
|
_errorStateValue.value = error.message.toString() ?: "Неизвестная ошибка"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AuthIntent.TextInput -> Unit
|
is AuthIntent.TextInput -> {
|
||||||
|
_textState.value = intent.text
|
||||||
|
_errorStateValue.value = ""
|
||||||
|
_isButtonEnabled.value = if (intent.text.length == 4 && intent.text.matches(Regex("^[a-zA-Z0-9]*\$")))
|
||||||
|
true else false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookAction {
|
||||||
|
object Auth: BookAction
|
||||||
|
object Main: BookAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookIntent {
|
||||||
|
object Back: BookIntent
|
||||||
|
object LoadBooking: BookIntent
|
||||||
|
object Book : BookIntent
|
||||||
|
data class SelectDate(val date: String) : BookIntent
|
||||||
|
data class SelectPlace(
|
||||||
|
val placeId: Int,
|
||||||
|
val placeName: String
|
||||||
|
) : BookIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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
|
||||||
|
import ru.myitschool.work.core.TestIds.Book
|
||||||
|
import ru.myitschool.work.core.TestIds.Main
|
||||||
|
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.formatDate
|
||||||
|
import ru.myitschool.work.ui.BaseButton
|
||||||
|
import ru.myitschool.work.ui.BaseNoBackgroundButton
|
||||||
|
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.screen.main.MainIntent
|
||||||
|
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
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: BookViewModel = viewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.actionFlow.collect { action ->
|
||||||
|
when(action) {
|
||||||
|
is BookAction.Auth -> navController.navigate(AuthScreenDestination)
|
||||||
|
|
||||||
|
is BookAction.Main -> navController.navigate(MainScreenDestination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when(state) {
|
||||||
|
is BookState.Loading -> {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is BookState.Data -> {
|
||||||
|
val dataState = state as BookState.Data
|
||||||
|
DataContent(
|
||||||
|
viewModel = viewModel,
|
||||||
|
bookingData = dataState.userBooking,
|
||||||
|
selectedDate = dataState.selectedDate,
|
||||||
|
selectedPlaceId = dataState.selectedPlaceId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is BookState.Error -> ErrorContent(viewModel)
|
||||||
|
is BookState.Empty -> EmptyContent(viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyContent(
|
||||||
|
viewModel: BookViewModel
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(15.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(320.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
|
|
||||||
|
BaseText24(
|
||||||
|
text = stringResource(R.string.book_all_booked),
|
||||||
|
modifier = Modifier.testTag(Book.EMPTY),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
BaseButton(
|
||||||
|
text = stringResource(R.string.book_back),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(Book.BACK_BUTTON),
|
||||||
|
onClick = { viewModel.onIntent(BookIntent.Back) },
|
||||||
|
btnContentColor = White,
|
||||||
|
btnColor = Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ErrorContent(
|
||||||
|
viewModel: BookViewModel
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(15.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(320.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
|
|
||||||
|
BaseText24(
|
||||||
|
text = stringResource(R.string.book_error),
|
||||||
|
modifier = Modifier.testTag(Book.ERROR),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
BaseButton(
|
||||||
|
border = BorderStroke(1.dp, Blue),
|
||||||
|
text = stringResource(R.string.book_back),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(Book.BACK_BUTTON),
|
||||||
|
onClick = { viewModel.onIntent(BookIntent.Back) },
|
||||||
|
btnContentColor = Blue,
|
||||||
|
btnColor = Color.Transparent
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(15.dp))
|
||||||
|
|
||||||
|
BaseButton(
|
||||||
|
text = stringResource(R.string.main_update),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(Book.REFRESH_BUTTON),
|
||||||
|
onClick = { viewModel.onIntent(BookIntent.LoadBooking) },
|
||||||
|
btnContentColor = White,
|
||||||
|
btnColor = Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|
||||||
|
Column {
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
BaseNoBackgroundButton(
|
||||||
|
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(),
|
||||||
|
icon = { Image(
|
||||||
|
painter = painterResource(R.drawable.add_icon),
|
||||||
|
contentDescription = stringResource(R.string.add_icon_description)
|
||||||
|
) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.selectable(
|
||||||
|
selected = isSelected,
|
||||||
|
onClick = onPlaceSelected
|
||||||
|
)
|
||||||
|
.testTag(Book.getIdPlaceItemByPosition(index))
|
||||||
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
BaseText16(
|
||||||
|
text = placeInfo.place,
|
||||||
|
modifier = Modifier.testTag(Book.ITEM_PLACE_TEXT)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.border(
|
||||||
|
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)
|
||||||
|
) {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import ru.myitschool.work.domain.book.entities.BookingEntity
|
||||||
|
|
||||||
|
sealed interface BookState {
|
||||||
|
object Loading: BookState
|
||||||
|
data class Data(
|
||||||
|
val userBooking: BookingEntity,
|
||||||
|
val selectedDate: String = "",
|
||||||
|
val selectedPlaceId: Int = -1,
|
||||||
|
val selectedPlaceName: String = ""
|
||||||
|
): BookState
|
||||||
|
object Error: BookState
|
||||||
|
object Empty: BookState
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.data.repo.BookRepository
|
||||||
|
import ru.myitschool.work.data.repo.MainRepository
|
||||||
|
import ru.myitschool.work.domain.book.BookingUseCase
|
||||||
|
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(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)
|
||||||
|
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
|
||||||
|
private val _actionFlow: MutableSharedFlow<BookAction> = MutableSharedFlow()
|
||||||
|
val actionFlow: SharedFlow<BookAction> = _actionFlow
|
||||||
|
|
||||||
|
init {
|
||||||
|
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) {
|
||||||
|
when (intent) {
|
||||||
|
is BookIntent.LoadBooking -> loadBooking()
|
||||||
|
|
||||||
|
is BookIntent.Back -> {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
_actionFlow.emit(BookAction.Main)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BookIntent.Book -> bookSelectedPlace()
|
||||||
|
|
||||||
|
is BookIntent.SelectDate -> {
|
||||||
|
val currentState = _uiState.value
|
||||||
|
if (currentState is BookState.Data) {
|
||||||
|
val placesForDate =
|
||||||
|
currentState.userBooking.bookings[intent.date] ?: emptyList()
|
||||||
|
val newSelectedPlaceId = placesForDate.firstOrNull()?.id ?: -1
|
||||||
|
val newSelectedPlaceName = placesForDate.firstOrNull()?.place ?: ""
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
currentState.copy(
|
||||||
|
selectedDate = intent.date,
|
||||||
|
selectedPlaceId = newSelectedPlaceId,
|
||||||
|
selectedPlaceName = newSelectedPlaceName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is BookIntent.SelectPlace -> {
|
||||||
|
val currentState = _uiState.value
|
||||||
|
if (currentState is BookState.Data) {
|
||||||
|
_uiState.update {
|
||||||
|
currentState.copy(
|
||||||
|
selectedPlaceId = intent.placeId,
|
||||||
|
selectedPlaceName = intent.placeName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainAction {
|
||||||
|
object Booking: MainAction
|
||||||
|
object Auth: MainAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainIntent {
|
||||||
|
object Logout: MainIntent
|
||||||
|
object Booking: MainIntent
|
||||||
|
object LoadData: MainIntent
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
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.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
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 coil3.compose.rememberAsyncImagePainter
|
||||||
|
import ru.myitschool.work.R
|
||||||
|
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.BaseNoBackgroundButton
|
||||||
|
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.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
|
||||||
|
fun MainScreen(
|
||||||
|
navController: NavController,
|
||||||
|
viewModel: MainViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
|
||||||
|
val state by viewModel.uiState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.actionFlow.collect { action ->
|
||||||
|
when(action) {
|
||||||
|
is MainAction.Auth -> navController.navigate(AuthScreenDestination)
|
||||||
|
|
||||||
|
is MainAction.Booking -> navController.navigate(BookScreenDestination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when(state) {
|
||||||
|
is MainState.Loading -> {
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(64.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is MainState.Error -> {
|
||||||
|
ErrorContent(viewModel)
|
||||||
|
}
|
||||||
|
is MainState.Data -> {
|
||||||
|
DataContent(
|
||||||
|
viewModel,
|
||||||
|
userData = (state as MainState.Data).userData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ErrorContent(viewModel: MainViewModel){
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(15.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(320.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(80.dp))
|
||||||
|
|
||||||
|
BaseText24(
|
||||||
|
text = stringResource(R.string.data_error_message),
|
||||||
|
modifier = Modifier.testTag(Main.ERROR),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
BaseButton(
|
||||||
|
text = stringResource(R.string.main_update),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.testTag(Main.REFRESH_BUTTON),
|
||||||
|
onClick = { viewModel.onIntent(MainIntent.LoadData) },
|
||||||
|
btnContentColor = White,
|
||||||
|
btnColor = Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DataContent(
|
||||||
|
viewModel: MainViewModel,
|
||||||
|
userData: UserEntity
|
||||||
|
) {
|
||||||
|
Column (
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(LightGray)
|
||||||
|
.fillMaxSize()
|
||||||
|
.width(400.dp)
|
||||||
|
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(bottomStart = 16.dp, bottomEnd = 16.dp))
|
||||||
|
.background(Blue)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
BaseNoBackgroundButton(
|
||||||
|
text = stringResource(R.string.main_update),
|
||||||
|
onClick = { viewModel.onIntent(MainIntent.LoadData) },
|
||||||
|
modifier = Modifier.testTag(Main.REFRESH_BUTTON)
|
||||||
|
)
|
||||||
|
BaseNoBackgroundButton(
|
||||||
|
text = stringResource(R.string.main_log_out),
|
||||||
|
onClick = { viewModel.onIntent(MainIntent.Logout) },
|
||||||
|
modifier = Modifier.testTag(Main.LOGOUT_BUTTON)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = rememberAsyncImagePainter(
|
||||||
|
model = userData.photoUrl,
|
||||||
|
error = painterResource(R.drawable.avatar)
|
||||||
|
),
|
||||||
|
contentDescription = stringResource(R.string.main_avatar_description),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(999.dp))
|
||||||
|
.testTag(Main.PROFILE_IMAGE)
|
||||||
|
.width(150.dp)
|
||||||
|
.height(150.dp)
|
||||||
|
.padding(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
BaseText20(
|
||||||
|
text = userData.name,
|
||||||
|
color = White,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(Main.PROFILE_NAME)
|
||||||
|
.width(250.dp),
|
||||||
|
style = Typography.bodyLarge
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(20.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(White)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.main_booking_title),
|
||||||
|
style = Typography.bodyMedium,
|
||||||
|
color = Black,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
horizontal = 10.dp,
|
||||||
|
vertical = 20.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (userData.hasBookings()) {
|
||||||
|
SortedBookingList(userData = userData)
|
||||||
|
} else {
|
||||||
|
EmptyBookings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BaseButton(
|
||||||
|
text = stringResource(R.string.booking_button),
|
||||||
|
btnColor = Blue,
|
||||||
|
btnContentColor = White,
|
||||||
|
onClick = { viewModel.onIntent(MainIntent.Booking) },
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(Main.ADD_BUTTON)
|
||||||
|
.padding(horizontal = 10.dp, vertical = 15.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
icon = {Image(
|
||||||
|
painter = painterResource(R.drawable.add_icon),
|
||||||
|
contentDescription = stringResource(R.string.add_icon_description)
|
||||||
|
)}
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortedBookingList(userData: UserEntity) {
|
||||||
|
val sortedBookings = remember(userData.booking) {
|
||||||
|
userData.getSortedBookingsWithFormattedDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 20.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = sortedBookings
|
||||||
|
) { index, (originalDate, formattedDate, bookingInfo) ->
|
||||||
|
BookingItem(
|
||||||
|
originalDate = originalDate,
|
||||||
|
formattedDate = formattedDate,
|
||||||
|
bookingInfo = bookingInfo,
|
||||||
|
index = index
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookingItem(
|
||||||
|
originalDate: String,
|
||||||
|
formattedDate: String,
|
||||||
|
bookingInfo: BookingInfo,
|
||||||
|
index: Int
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(Main.getIdItemByPosition(index))
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 20.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
BaseText14(
|
||||||
|
text = bookingInfo.place,
|
||||||
|
modifier = Modifier.testTag(Main.ITEM_PLACE)
|
||||||
|
)
|
||||||
|
BaseText14(
|
||||||
|
text = formattedDate,
|
||||||
|
modifier = Modifier.testTag(Main.ITEM_DATE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyBookings() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
BaseText16(
|
||||||
|
text = stringResource(R.string.main_empty_booking)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import ru.myitschool.work.domain.main.entities.UserEntity
|
||||||
|
|
||||||
|
sealed interface MainState {
|
||||||
|
data class Data(val userData: UserEntity): MainState
|
||||||
|
object Loading: MainState
|
||||||
|
object Error: MainState
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import ru.myitschool.work.App
|
||||||
|
import ru.myitschool.work.data.repo.MainRepository
|
||||||
|
import ru.myitschool.work.domain.main.LoadDataUseCase
|
||||||
|
|
||||||
|
class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
|
private val dataStoreManager by lazy {
|
||||||
|
(getApplication() as App).dataStoreManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private val loadDataUseCase by lazy { LoadDataUseCase(MainRepository) }
|
||||||
|
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||||
|
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||||
|
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||||
|
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadData() {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
_uiState.update { MainState.Loading }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val userCode = dataStoreManager.getUserCode().first()
|
||||||
|
|
||||||
|
if (userCode.code.isEmpty()) {
|
||||||
|
_actionFlow.emit(MainAction.Auth)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDataUseCase.invoke(userCode.code).fold(
|
||||||
|
onSuccess = { data ->
|
||||||
|
_uiState.update { MainState.Data(data) }
|
||||||
|
},
|
||||||
|
onFailure = { error ->
|
||||||
|
error.printStackTrace()
|
||||||
|
_uiState.update { MainState.Error }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
error.printStackTrace()
|
||||||
|
_uiState.update { MainState.Error }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onIntent( intent: MainIntent) {
|
||||||
|
when(intent) {
|
||||||
|
is MainIntent.LoadData -> loadData()
|
||||||
|
|
||||||
|
is MainIntent.Booking -> {
|
||||||
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
|
_actionFlow.emit(MainAction.Booking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is MainIntent.Logout -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|
||||||
|
dataStoreManager.clearUserCode()
|
||||||
|
_actionFlow.emit(MainAction.Auth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -2,19 +2,34 @@ 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 MontserratFontFamily = FontFamily(
|
||||||
|
Font(R.font.montserrat_bold, FontWeight.Bold),
|
||||||
|
Font(R.font.montserrat_medium, FontWeight.Medium),
|
||||||
|
Font(R.font.montserrat_semibold, weight = FontWeight.SemiBold)
|
||||||
|
)
|
||||||
|
|
||||||
val Typography = Typography(
|
val Typography = Typography(
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontFamily = MontserratFontFamily,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontFamily = MontserratFontFamily,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
bodyLarge = TextStyle(
|
bodyLarge = TextStyle(
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = MontserratFontFamily,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Bold,
|
||||||
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,
|
||||||
|
|||||||
26
app/src/main/java/ru/myitschool/work/utils.kt
Normal file
26
app/src/main/java/ru/myitschool/work/utils.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/res/drawable/add_icon.xml
Normal file
11
app/src/main/res/drawable/add_icon.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="17dp"
|
||||||
|
android:height="17dp"
|
||||||
|
android:viewportWidth="17"
|
||||||
|
android:viewportHeight="17">
|
||||||
|
<path
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M8.296,0.5C8.503,0.5 8.656,0.565 8.786,0.695C8.916,0.826 8.981,0.977 8.98,1.183V7.61H15.405C15.615,7.61 15.767,7.676 15.896,7.805C16.026,7.934 16.09,8.086 16.09,8.294C16.089,8.503 16.024,8.657 15.894,8.788C15.767,8.917 15.616,8.981 15.407,8.98H8.98V15.405C8.98,15.615 8.914,15.767 8.785,15.896C8.656,16.026 8.504,16.09 8.296,16.09C8.087,16.089 7.934,16.024 7.805,15.894C7.676,15.766 7.61,15.615 7.61,15.405V8.98H1.185C0.975,8.98 0.823,8.915 0.695,8.786C0.566,8.656 0.5,8.503 0.5,8.294C0.5,8.086 0.565,7.935 0.694,7.806C0.825,7.675 0.978,7.61 1.185,7.61H7.61V1.185C7.61,0.975 7.676,0.824 7.805,0.695C7.934,0.566 8.087,0.501 8.296,0.5Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
</vector>
|
||||||
14
app/src/main/res/drawable/avatar.xml
Normal file
14
app/src/main/res/drawable/avatar.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="100dp"
|
||||||
|
android:height="100dp"
|
||||||
|
android:viewportWidth="100"
|
||||||
|
android:viewportHeight="100">
|
||||||
|
<path
|
||||||
|
android:pathData="M50,50m-48.5,0a48.5,48.5 0,1 1,97 0a48.5,48.5 0,1 1,-97 0"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:fillColor="#AEC3FF"
|
||||||
|
android:strokeColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M61.99,54.17C64.58,54.17 66.67,56.26 66.67,58.85V60.05C66.67,61.91 66.01,63.71 64.8,65.13C61.53,68.95 56.55,70.83 50,70.83C43.45,70.83 38.48,68.95 35.21,65.13C34,63.71 33.34,61.91 33.34,60.05V58.85C33.34,56.26 35.44,54.17 38.03,54.17H61.99ZM61.99,57.29H38.03C37.16,57.29 36.47,57.99 36.47,58.85V60.05C36.47,61.17 36.86,62.25 37.59,63.1C40.2,66.16 44.3,67.71 50,67.71C55.71,67.71 59.8,66.16 62.42,63.1C63.15,62.25 63.55,61.17 63.55,60.05V58.85C63.55,57.99 62.85,57.29 61.99,57.29ZM50,29.18C55.75,29.18 60.42,33.84 60.42,39.59C60.42,45.35 55.75,50.01 50,50.01C44.25,50.01 39.58,45.35 39.58,39.59C39.58,33.84 44.25,29.18 50,29.18ZM50,32.3C45.97,32.3 42.71,35.57 42.71,39.59C42.71,43.62 45.97,46.88 50,46.88C54.03,46.88 57.29,43.62 57.29,39.59C57.29,35.57 54.03,32.3 50,32.3Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
||||||
21
app/src/main/res/drawable/logo.xml
Normal file
21
app/src/main/res/drawable/logo.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="53dp"
|
||||||
|
android:height="99dp"
|
||||||
|
android:viewportWidth="53"
|
||||||
|
android:viewportHeight="99">
|
||||||
|
<path
|
||||||
|
android:pathData="M33.501,13.467V35.047H52.507C52.507,24.867 44.587,16.26 33.501,13.467Z"
|
||||||
|
android:fillColor="#004BFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0.267,61.92C0.267,72.08 8.187,80.713 19.274,83.507V61.92H0.267Z"
|
||||||
|
android:fillColor="#004BFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M52.781,61.92C52.507,46.767 36.027,41.173 28.594,39.6C25.087,38.84 18.814,36.933 19.007,33.713V13.44C8.621,16.053 -0.293,24.873 0.007,35.473C1.714,53.393 19.207,55.613 29.341,58.88C32.581,60.153 33.861,61.78 33.781,63.333V83.513C44.041,80.993 52.901,72.16 52.781,61.92Z"
|
||||||
|
android:fillColor="#004BFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M26.374,85.92C22.941,85.92 20.047,84.867 19.234,83.54L26.374,98.3L33.754,83.513C32.714,84.813 29.821,85.92 26.374,85.92Z"
|
||||||
|
android:fillColor="#004BFF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M26.661,13.467C29.821,13.467 32.467,12.407 32.467,10.8V2.62C32.467,1.32 29.801,0 26.661,0C23.221,0 20.601,1.053 20.601,2.667V10.813C20.574,12.14 23.221,13.467 26.661,13.467Z"
|
||||||
|
android:fillColor="#004BFF"/>
|
||||||
|
</vector>
|
||||||
BIN
app/src/main/res/font/montserrat_bold.ttf
Normal file
BIN
app/src/main/res/font/montserrat_bold.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/montserrat_medium.ttf
Normal file
BIN
app/src/main/res/font/montserrat_medium.ttf
Normal file
Binary file not shown.
BIN
app/src/main/res/font/montserrat_semibold.ttf
Normal file
BIN
app/src/main/res/font/montserrat_semibold.ttf
Normal file
Binary file not shown.
@@ -1,7 +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_update">Обновить</string>
|
||||||
|
<string name="main_log_out">Выйти</string>
|
||||||
|
<string name="main_avatar_description">Фото пользователя</string>
|
||||||
|
<string name="main_booking_title">Ваши забронированные места</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>
|
||||||
Reference in New Issue
Block a user