forked from Olympic/NTO-2025-Android-TeamTask
Compare commits
3 Commits
a04fb915ae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f2ec1fefd | ||
|
|
c6418793cb | ||
|
|
2da3b58773 |
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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] ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package ru.myitschool.work.data.dtos
|
|
||||||
|
|
||||||
class BookingDto {
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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?
|
|
||||||
)
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.nav
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object SplashScreenDestination: AppDestination
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
sealed interface BookAction {
|
||||||
|
object Auth: BookAction
|
||||||
|
object Main: BookAction
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
sealed interface MainAction {
|
||||||
|
object Booking: MainAction
|
||||||
|
object Auth: MainAction
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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,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,
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user