base functional added

This commit is contained in:
solovushka56
2025-12-11 01:51:17 +03:00
parent 945b9d347d
commit d8416283ae
65 changed files with 1954 additions and 72 deletions

View File

@@ -2,14 +2,24 @@ package ru.myitschool.work
import android.app.Application
import android.content.Context
import ru.myitschool.work.data.repo.AuthRepository
class App: Application() {
override fun onCreate() {
super.onCreate()
context = this
}
class App : Application() {
companion object {
lateinit var context: Context
@Volatile
private var instance: App? = null
fun getAppContext(): Context {
return instance?.applicationContext ?: throw IllegalStateException(
"app not initialized")
}
}
override fun onCreate() {
super.onCreate()
instance = this
AuthRepository.init(applicationContext)
}
}

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,86 @@
package ru.myitschool.work.data.repo
import android.content.Context
import android.content.Context.MODE_PRIVATE
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import ru.myitschool.work.data.source.NetworkDataSource
object AuthRepository {
private const val PREFS_NAME = "auth_prefs"
private const val KEY_CODE = "auth_code"
private const val KEY_NAME = "user_name"
private const val KEY_PHOTO = "user_photo"
private var _context: Context? = null
private var codeCache: String? = null
private var userCache: UserCache? = null
private val _isAuthorized = MutableStateFlow(false)
val isAuthorized: StateFlow<Boolean> = _isAuthorized.asStateFlow()
fun init(context: Context) {
if (_context == null) {
_context = context.applicationContext
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
}
}
}
private fun requireContext(): Context {
return _context ?: throw IllegalStateException(
"AuthRepository not inited"
)
}
private fun getPrefs() = requireContext().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()
}
}.onFailure { exception ->
println("Auth error: ${exception.message}")
}
}
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
fun clear() {
codeCache = null
userCache = null
_isAuthorized.value = false
getPrefs().edit()
.clear()
.apply()
}
}
data class UserCache(
val name: String,
val photo: String?
)

View File

@@ -0,0 +1,25 @@
package ru.myitschool.work.data.repo
import ru.myitschool.work.data.model.BookingInfoResponse
import ru.myitschool.work.data.source.AuthException
import ru.myitschool.work.data.source.NetworkDataSource
object BookRepository {
suspend fun getAvailableBookings(): Result<BookingInfoResponse> {
val code = AuthRepository.getCurrentCode()
return if (code != null) {
NetworkDataSource.getBookingInfo(code)
} else {
Result.failure(AuthException("user not authorized"))
}
}
suspend fun book(date: String, placeId: Int): Result<Unit> {
val code = AuthRepository.getCurrentCode()
return if (code != null) {
NetworkDataSource.book(code, date, placeId)
} else {
Result.failure(AuthException("user not authorized"))
}
}
}

View File

@@ -0,0 +1,16 @@
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
object MainRepository {
suspend fun getUserInfo(): Result<UserInfoResponse> {
val code = AuthRepository.getCurrentCode()
return if (code != null) {
NetworkDataSource.getInfo(code)
} else {
Result.failure(AuthException("user not pass auth"))
}
}
}

View File

@@ -1,42 +1,183 @@
package ru.myitschool.work.data.source
import io.ktor.client.HttpClient
import io.ktor.client.call.body
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.defaultRequest
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.kotlinx.json.json
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import ru.myitschool.work.core.Constants
import ru.myitschool.work.data.model.BookingInfoResponse
import ru.myitschool.work.data.model.UserInfoResponse
import java.net.ConnectException
import java.net.SocketTimeoutException
@Serializable
data class BookRequest(
val date: String,
val placeId: Int
)
object NetworkDataSource {
private val client by lazy {
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) {
json(
Json {
isLenient = true
ignoreUnknownKeys = true
explicitNulls = true
explicitNulls = false
encodeDefaults = true
}
)
}
defaultRequest {
contentType(ContentType.Application.Json)
}
}
}
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.AUTH_URL))
when (response.status) {
HttpStatusCode.OK -> true
else -> error(response.bodyAsText())
response.status == HttpStatusCode.OK
}.recoverCatching { throwable ->
when (throwable) {
is AuthException -> false
else -> throw throwable
}
}
}
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
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) {
HttpStatusCode.OK -> response.body<UserInfoResponse>()
else -> {
val errorMessage = response.body<String>().ifBlank {
"Ошибка: ${response.status}"
}
throw RuntimeException(errorMessage)
}
}
}
}
suspend fun getBookingInfo(code: String): Result<BookingInfoResponse> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.get(getUrl(code, Constants.BOOKING_URL))
when (response.status) {
HttpStatusCode.OK -> response.body<BookingInfoResponse>()
HttpStatusCode.NoContent -> BookingInfoResponse(emptyMap())
else -> {
val errorMsg = response.body<String>().ifBlank {
"Ошибка загрузки данных: ${response.status}"
}
throw RuntimeException(errorMsg)
}
}
}
}
suspend fun book(code: String, date: String, placeId: Int): Result<Unit> = withContext(Dispatchers.IO) {
return@withContext runCatching {
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
setBody(BookRequest(date, placeId))
}
when (response.status) {
HttpStatusCode.OK -> Unit
HttpStatusCode.Created -> Unit
HttpStatusCode.Conflict -> throw ConflictException("Место уже забронировано")
else -> {
val errorMsg = response.body<String>().ifBlank {
"Ошибка бронирования: ${response.status}"
}
throw RuntimeException(errorMsg)
}
}
}
}
private fun getUrl(code: String, targetUrl: String): String {
return "${Constants.HOST}/api/$code$targetUrl"
}
}
class NetworkException(message: String) : Exception(message)
class AuthException(message: String) : Exception(message)
class NotFoundException(message: String) : Exception(message)
class ConflictException(message: String) : Exception(message)
class ServerException(message: String) : Exception(message)
class BadRequestException(message: String) : Exception(message)

View File

@@ -1,10 +1,12 @@
package ru.myitschool.work.ui.screen.auth
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
@@ -16,12 +18,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@@ -38,31 +38,54 @@ fun AuthScreen(
navController: NavController
) {
val state by viewModel.uiState.collectAsState()
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
viewModel.actionFlow.collect {
navController.navigate(MainScreenDestination)
viewModel.actionFlow.collect { action ->
when (action) {
is AuthAction.NavigateToMain -> {
navController.navigate(MainScreenDestination) {
popUpTo(0)
}
}
}
}
}
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(all = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center
)
when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState)
is AuthState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineSmall,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 32.dp)
)
when (val currentState = state) {
is AuthState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.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))
}
)
}
}
}
}
@@ -70,28 +93,48 @@ fun AuthScreen(
@Composable
private fun Content(
viewModel: AuthViewModel,
state: AuthState.Data
state: AuthState.Data,
onTextChange: (String) -> Unit,
onSendClick: (String) -> Unit
) {
var inputText by remember { mutableStateOf("") }
Spacer(modifier = Modifier.size(16.dp))
TextField(
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
value = inputText,
onValueChange = {
inputText = it
viewModel.onIntent(AuthIntent.TextInput(it))
},
label = { Text(stringResource(R.string.auth_label)) }
)
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
onClick = {
viewModel.onIntent(AuthIntent.Send(inputText))
},
enabled = true
val isButtonEnabled = state.code.length == 4 &&
state.code.matches(Regex("^[a-zA-Z0-9]{4}$"))
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(R.string.auth_sign_in))
if (state.error != null) {
Text(
text = state.error,
color = Color.Red,
modifier = Modifier
.testTag(TestIds.Auth.ERROR)
.padding(bottom = 16.dp)
)
}
TextField(
modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(),
value = state.code,
onValueChange = onTextChange,
label = { Text(stringResource(R.string.auth_label)) },
singleLine = true,
isError = state.error != null
)
Spacer(modifier = Modifier.height(16.dp))
Button(
modifier = Modifier
.testTag(TestIds.Auth.SIGN_BUTTON)
.fillMaxWidth(),
onClick = { onSendClick(state.code) },
enabled = isButtonEnabled
) {
Text(stringResource(R.string.auth_sign_in))
}
}
}

View File

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

View File

@@ -2,7 +2,6 @@ package ru.myitschool.work.ui.screen.auth
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
@@ -13,31 +12,62 @@ import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
private val authRepo = AuthRepository
private val checkAndSaveAuthCodeUseCase = CheckAndSaveAuthCodeUseCase(authRepo)
class AuthViewModel : ViewModel() {
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
val actionFlow: SharedFlow<Unit> = _actionFlow
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
val actionFlow: SharedFlow<AuthAction> = _actionFlow
fun onIntent(intent: AuthIntent) {
when (intent) {
is AuthIntent.Send -> {
viewModelScope.launch(Dispatchers.Default) {
_uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
onSuccess = {
_actionFlow.emit(Unit)
},
onFailure = { error ->
error.printStackTrace()
_actionFlow.emit(Unit)
}
if (validateCode(intent.text)) {
viewModelScope.launch {
_uiState.update { AuthState.Loading }
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
onSuccess = {
_actionFlow.emit(AuthAction.NavigateToMain)
},
onFailure = { error ->
_uiState.update {
AuthState.Data(
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
)
}
}
is AuthIntent.TextInput -> Unit
}
}
private fun validateCode(code: String): Boolean {
return code.length == 4 && code.matches(Regex("^[a-zA-Z0-9]{4}$"))
}
}
sealed interface AuthAction {
object NavigateToMain : AuthAction
}

View File

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

View File

@@ -0,0 +1,205 @@
package ru.myitschool.work.ui.screen.book
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
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.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import androidx.compose.material3.Icon
import androidx.compose.ui.res.painterResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookScreen(
viewModel: BookViewModel = viewModel(),
navController: NavController
) {
val state by viewModel.uiState.collectAsState()
LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action ->
when (action) {
BookAction.NavigateBack -> {
navController.popBackStack()
}
BookAction.NavigateBackWithRefresh -> {
navController.previousBackStackEntry?.savedStateHandle?.set(
"shouldRefresh", true)
navController.popBackStack()
}
}
}
}
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(
// TODO
painter = painterResource(id = R.drawable.back),
contentDescription = null
)
}
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val currentState = state) {
BookState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
BookState.Empty -> {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = stringResource(R.string.book_empty),
modifier = Modifier.testTag(TestIds.Book.EMPTY)
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.onIntent(BookIntent.Back) },
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON)
) {
Text(stringResource(R.string.book_back))
}
}
}
is BookState.Data -> {
if (currentState.error != null) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = currentState.error,
modifier = Modifier.testTag(TestIds.Book.ERROR)
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.onIntent(BookIntent.Refresh) },
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON)
) {
Text(stringResource(R.string.book_refresh))
}
}
} else {
Content(
state = currentState,
onDateSelect = { viewModel.onIntent(BookIntent.SelectDate(it)) },
onPlaceSelect = { viewModel.onIntent(BookIntent.SelectPlace(it)) },
onBookClick = { viewModel.onIntent(BookIntent.Book) }
)
}
}
}
}
}
}
@Composable
private fun Content(
state: BookState.Data,
onDateSelect: (Int) -> Unit,
onPlaceSelect: (Int) -> Unit,
onBookClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
ScrollableTabRow(
selectedTabIndex = state.selectedDateIndex,
modifier = Modifier.fillMaxWidth()
) {
state.dates.forEachIndexed { index, dateItem ->
Tab(
selected = state.selectedDateIndex == index,
onClick = { onDateSelect(index) },
modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index)),
text = {
Text(
text = dateItem.displayDate,
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE)
)
}
)
}
}
Spacer(modifier = Modifier.height(16.dp))
val selectedDate = state.dates[state.selectedDateIndex]
Column {
selectedDate.places.forEachIndexed { index, placeItem ->
Row(
modifier = Modifier
.fillMaxWidth()
.selectable(
selected = state.selectedPlaceIndex == index,
onClick = { onPlaceSelect(index) }
)
.padding(16.dp)
.testTag(TestIds.Book.getIdPlaceItemByPosition(index))
) {
RadioButton(
selected = state.selectedPlaceIndex == index,
onClick = { onPlaceSelect(index) },
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = placeItem.name,
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_TEXT)
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onBookClick,
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Book.BOOK_BUTTON),
enabled = state.selectedPlaceIndex != null
) {
Text(stringResource(R.string.book_book))
}
}
}

View File

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

View File

@@ -0,0 +1,182 @@
package ru.myitschool.work.ui.screen.book
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.data.repo.BookRepository
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private val bookRepo = BookRepository
class BookViewModel : ViewModel() {
private val _uiState = MutableStateFlow<BookState>(BookState.Loading)
val uiState: StateFlow<BookState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<BookAction> = MutableSharedFlow()
val actionFlow: SharedFlow<BookAction> = _actionFlow
init {
loadData()
}
fun onIntent(intent: BookIntent) {
when (intent) {
BookIntent.Refresh -> {
loadData()
}
BookIntent.Back -> {
viewModelScope.launch {
_actionFlow.emit(BookAction.NavigateBack)
}
}
BookIntent.Book -> {
bookSelected()
}
is BookIntent.SelectDate -> {
_uiState.update { state ->
if (state is BookState.Data) {
state.copy(
selectedDateIndex = intent.index,
selectedPlaceIndex = null
)
} else {
state
}
}
}
is BookIntent.SelectPlace -> {
_uiState.update { state ->
if (state 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 dateItems = response.dates.entries
.filter { (_, places) -> places.isNotEmpty() }
.sortedBy { (date, _) -> parseDate(date) }
.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 {
BookState.Data(
dates = dateItems,
selectedDateIndex = 0
)
}
}
},
onFailure = { error ->
_uiState.update {
BookState.Data(
error = error.message ?: "data load err"
)
}
}
)
}
}
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 ?: "book error"
)
}
}
)
} else {
_uiState.update {
state.copy(
error = "place !selected"
)
}
}
} else {
_uiState.update {
state.copy(
error = "select place for booking"
)
}
}
}
}
}
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
}

View File

@@ -0,0 +1,8 @@
package ru.myitschool.work.ui.screen.main
sealed interface MainIntent {
object Logout : MainIntent
object Refresh : MainIntent
object AddBooking : MainIntent
data class ItemClick(val position: Int) : MainIntent
}

View File

@@ -0,0 +1,280 @@
package ru.myitschool.work.ui.screen.main
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil3.compose.rememberAsyncImagePainter
import coil3.request.ImageRequest
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
@Composable
fun MainScreen(
viewModel: MainViewModel = viewModel(),
navController: NavController
) {
val state by viewModel.uiState.collectAsState()
val shouldRefresh by navController.currentBackStackEntry
?.savedStateHandle
?.getStateFlow<Boolean>("shouldRefresh", false)
?.collectAsState() ?: remember { mutableStateOf(false) }
LaunchedEffect(shouldRefresh) {
if (shouldRefresh) {
viewModel.onIntent(MainIntent.Refresh)
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("shouldRefresh")
}
}
LaunchedEffect(Unit) {
viewModel.actionFlow.collect { action ->
when (action) {
is MainAction.NavigateToAuth -> {
navController.navigate(AuthScreenDestination) {
popUpTo(0)
}
}
is MainAction.NavigateToBooking -> {
navController.navigate(BookScreenDestination)
}
}
}
}
Box(
modifier = Modifier.fillMaxSize()
) {
when (val currentState = state) {
is MainState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
is MainState.Data -> {
if (currentState.error != null) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = currentState.error,
color = Color.Red,
modifier = Modifier.testTag(TestIds.Main.ERROR)
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.onIntent(MainIntent.Refresh) },
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON)
) {
Text(stringResource(R.string.main_refresh))
}
}
} else {
Column(
modifier = Modifier.fillMaxSize()
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (!currentState.userPhotoUrl.isNullOrEmpty()) {
Image(
painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current)
.data(currentState.userPhotoUrl)
.build()
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE)
)
} else {
Icon(
painter = painterResource(id = R.drawable.github),
contentDescription = null,
modifier = Modifier
.size(64.dp)
.testTag(TestIds.Main.PROFILE_IMAGE)
)
}
Spacer(modifier = Modifier.size(16.dp))
Column {
Text(
text = currentState.userName,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME)
)
if (currentState.bookings.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Забронировано мест: ${currentState.bookings.size}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
// Кнопки действий
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { viewModel.onIntent(MainIntent.Logout) },
modifier = Modifier
.weight(1f)
.testTag(TestIds.Main.LOGOUT_BUTTON)
) {
Text(stringResource(R.string.main_logout))
}
Button(
onClick = { viewModel.onIntent(MainIntent.Refresh) },
modifier = Modifier
.weight(1f)
.testTag(TestIds.Main.REFRESH_BUTTON)
) {
Text(stringResource(R.string.main_refresh))
}
Button(
onClick = { viewModel.onIntent(MainIntent.AddBooking) },
modifier = Modifier
.weight(1f)
.testTag(TestIds.Main.ADD_BUTTON)
) {
Text(stringResource(R.string.main_add_booking))
}
}
Spacer(modifier = Modifier.height(8.dp))
// Заголовок списка бронирований
if (currentState.bookings.isNotEmpty()) {
Text(
text = "Мои бронирования:",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
} else {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "У вас нет активных бронирований",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// Список бронирований
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
itemsIndexed(currentState.bookings) { index, booking ->
BookingItem(
booking = booking,
position = index,
modifier = Modifier.testTag(
TestIds.Main.getIdItemByPosition(index)
)
)
}
}
}
}
}
}
}
}
@Composable
private fun BookingItem(
booking: BookingItem,
position: Int,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = booking.place,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE)
)
Text(
text = booking.getFormattedDate(),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE)
)
}
}
}

View File

@@ -0,0 +1,24 @@
package ru.myitschool.work.ui.screen.main
import java.time.LocalDate
import java.time.format.DateTimeFormatter
sealed interface MainState {
object Loading : MainState
data class Data(
val userName: String = "",
val userPhotoUrl: String? = null,
val bookings: List<BookingItem> = emptyList(),
val error: String? = null
) : MainState
}
data class BookingItem(
val id: String,
val date: LocalDate,
val place: String
) {
fun getFormattedDate(): String {
return date.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))
}
}

View File

@@ -0,0 +1,101 @@
package ru.myitschool.work.ui.screen.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import ru.myitschool.work.data.repo.AuthRepository
import ru.myitschool.work.data.repo.MainRepository
private val authRepo = AuthRepository
private val mainRepo = MainRepository
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
val actionFlow: SharedFlow<MainAction> = _actionFlow
private val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
init {
loadData()
}
fun onIntent(intent: MainIntent) {
when (intent) {
MainIntent.Logout -> {
val currentState = _uiState.value
if (currentState is MainState.Data) {
authRepo.saveUserInfo(currentState.userName, currentState.userPhotoUrl)
}
authRepo.clear()
viewModelScope.launch {
_actionFlow.emit(MainAction.NavigateToAuth)
}
}
MainIntent.Refresh -> {
loadData()
}
MainIntent.AddBooking -> {
viewModelScope.launch {
_actionFlow.emit(MainAction.NavigateToBooking)
}
}
is MainIntent.ItemClick -> {
}
}
}
private fun loadData() {
viewModelScope.launch {
_uiState.update { MainState.Loading }
mainRepo.getUserInfo().fold(
onSuccess = { userInfo ->
val bookings = userInfo.bookings.mapNotNull { bookingResponse ->
try {
BookingItem(
id = bookingResponse.bookingId.toString(),
date = LocalDate.parse(bookingResponse.date, dateFormatter),
place = bookingResponse.place
)
} catch (e: Exception) {
null
}
}.sortedBy { it.date }
authRepo.saveUserInfo(userInfo.name, userInfo.photoUrl)
_uiState.update {
MainState.Data(
userName = userInfo.name,
userPhotoUrl = userInfo.photoUrl,
bookings = bookings
)
}
},
onFailure = { error ->
_uiState.update {
MainState.Data(
error = error.message ?: "data load err"
)
}
}
)
}
}
}
sealed interface MainAction {
object NavigateToAuth : MainAction
object NavigateToBooking : MainAction
}

View File

@@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="34dp"
android:height="34dp"
android:viewportWidth="34"
android:viewportHeight="34">
<path
android:pathData="M17.321,18.625C18.08,18.625 18.695,18.01 18.695,17.252C18.695,16.493 18.08,15.878 17.321,15.878C16.563,15.878 15.948,16.493 15.948,17.252C15.948,18.01 16.563,18.625 17.321,18.625Z"
android:strokeLineJoin="round"
android:strokeWidth="3.38015"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
<path
android:pathData="M17.321,9.012C18.08,9.012 18.695,8.398 18.695,7.639C18.695,6.881 18.08,6.266 17.321,6.266C16.563,6.266 15.948,6.881 15.948,7.639C15.948,8.398 16.563,9.012 17.321,9.012Z"
android:strokeLineJoin="round"
android:strokeWidth="3.38015"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
<path
android:pathData="M17.321,28.237C18.08,28.237 18.695,27.622 18.695,26.864C18.695,26.105 18.08,25.491 17.321,25.491C16.563,25.491 15.948,26.105 15.948,26.864C15.948,27.622 16.563,28.237 17.321,28.237Z"
android:strokeLineJoin="round"
android:strokeWidth="3.38015"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="151dp"
android:height="72dp"
android:viewportWidth="151"
android:viewportHeight="72">
<path
android:pathData="M4,8C4,3.58 7.58,0 12,0H139C143.42,0 147,3.58 147,8V56C147,60.42 143.42,64 139,64H12C7.58,64 4,60.42 4,56V8Z"
android:fillColor="#4B70F5"/>
<path
android:pathData="M34.46,40.23C33.51,40.23 32.61,40.16 31.74,40C30.88,39.84 30.1,39.62 29.39,39.32L29.77,36.47C30.63,36.84 31.47,37.12 32.3,37.32C33.13,37.5 33.91,37.6 34.64,37.6C35.58,37.6 36.29,37.43 36.77,37.11C37.25,36.77 37.49,36.28 37.49,35.64C37.49,34.8 37.02,34.17 36.06,33.73L33.47,32.5C32.31,31.95 31.41,31.28 30.78,30.47C30.15,29.67 29.84,28.74 29.84,27.7C29.84,26.24 30.3,25.12 31.22,24.32C32.15,23.52 33.46,23.13 35.16,23.13C36.22,23.13 37.2,23.3 38.09,23.65C38.99,24 39.81,24.52 40.55,25.22L38.62,27.36C38.02,26.82 37.43,26.43 36.86,26.16C36.3,25.89 35.73,25.75 35.17,25.75C34.44,25.75 33.87,25.92 33.46,26.25C33.05,26.58 32.85,27.06 32.85,27.7C32.85,28.13 32.99,28.52 33.26,28.86C33.54,29.18 33.95,29.48 34.47,29.75L36.8,30.91C38,31.5 38.91,32.19 39.54,32.97C40.18,33.74 40.5,34.64 40.5,35.65C40.5,37.14 39.99,38.28 38.96,39.06C37.93,39.84 36.43,40.23 34.46,40.23ZM51.58,40.23C49.54,40.23 48.07,39.82 47.16,38.99C46.25,38.16 45.8,36.84 45.8,35.02V33.2H48.68V34.79C48.68,35.75 48.93,36.45 49.41,36.88C49.9,37.32 50.7,37.54 51.81,37.54C52.18,37.54 52.57,37.52 53,37.48C53.42,37.43 53.9,37.38 54.44,37.3L54.72,39.95C54.2,40.05 53.68,40.12 53.17,40.16C52.67,40.21 52.14,40.23 51.58,40.23ZM45.8,34.02V23.83H48.68V34.02H45.8ZM42.77,30.14V27.58H54.25V30.14H42.77ZM65.55,40.12L65.32,36.44L65.16,35.05V32.31C65.16,31.59 64.88,31.05 64.3,30.72C63.73,30.38 62.88,30.19 61.75,30.16L58.66,30.07L58.89,27.46L61.52,27.5C63.69,27.53 65.3,27.97 66.36,28.83C67.41,29.68 67.94,30.93 67.94,32.57V37.4L69.81,37.66V40L65.55,40.12ZM61.27,40.23C59.96,40.23 58.94,39.91 58.21,39.27C57.49,38.63 57.13,37.73 57.13,36.55C57.13,35.2 57.62,34.15 58.59,33.43C59.55,32.7 60.93,32.34 62.7,32.34C63.4,32.34 64.02,32.38 64.55,32.45C65.08,32.53 65.59,32.65 66.07,32.82L65.56,34.97C65.05,34.86 64.58,34.79 64.14,34.77C63.7,34.75 63.25,34.74 62.77,34.74C60.93,34.74 60,35.28 60,36.37C60,36.84 60.16,37.21 60.48,37.46C60.81,37.71 61.29,37.83 61.9,37.83C62.62,37.83 63.22,37.7 63.71,37.45C64.19,37.18 64.55,36.85 64.8,36.45C65.04,36.04 65.16,35.63 65.16,35.2V34.19L65.72,37.38H64.73L65.14,37C65.12,37.72 64.95,38.32 64.63,38.8C64.32,39.28 63.88,39.64 63.31,39.88C62.74,40.12 62.06,40.23 61.27,40.23ZM76.57,34.66L75.85,30.53H76.88C77.01,28.41 78.1,27.34 80.15,27.34C81.37,27.34 82.28,27.75 82.89,28.56C83.5,29.38 83.8,30.6 83.8,32.24H80.93C80.93,31.44 80.8,30.86 80.52,30.51C80.26,30.16 79.82,29.98 79.22,29.98C78.33,29.98 77.67,30.39 77.23,31.2C76.79,32.01 76.57,33.16 76.57,34.66ZM71.01,40V37.55H79.54V40H71.01ZM73.69,40V27.58H76.11L76.57,31.14V40H73.69ZM71.48,30.03V27.58H75.94L76.18,30.03H71.48ZM93.77,40.23C91.73,40.23 90.25,39.82 89.35,38.99C88.44,38.16 87.99,36.84 87.99,35.02V33.2H90.87V34.79C90.87,35.75 91.11,36.45 91.6,36.88C92.09,37.32 92.89,37.54 94,37.54C94.37,37.54 94.76,37.52 95.18,37.48C95.61,37.43 96.09,37.38 96.63,37.3L96.91,39.95C96.38,40.05 95.87,40.12 95.36,40.16C94.86,40.21 94.33,40.23 93.77,40.23ZM87.99,34.02V23.83H90.87V34.02H87.99ZM84.95,30.14V27.58H96.44V30.14H84.95Z"
android:fillColor="#F5F5F5"/>
<path
android:pathData="M110.33,26L119.67,32L110.33,38V26Z"
android:strokeLineJoin="round"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#F5F5F5"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="28dp"
android:viewportWidth="16"
android:viewportHeight="28">
<path
android:pathData="M14,26L2,14L14,2"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="10dp"
android:viewportWidth="16"
android:viewportHeight="10">
<path
android:pathData="M2,2L7.931,7.931L13.862,2"
android:strokeLineJoin="round"
android:strokeWidth="2.43316"
android:fillColor="#00000000"
android:strokeColor="#4B70F5"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M18,2V8M30,2V8M18,40V46M30,40V46M40,18H46M40,28H46M2,18H8M2,28H8M12,8H36C38.209,8 40,9.791 40,12V36C40,38.209 38.209,40 36,40H12C9.791,40 8,38.209 8,36V12C8,9.791 9.791,8 12,8ZM18,18H30V30H18V18Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="39dp"
android:viewportWidth="40"
android:viewportHeight="39">
<path
android:pathData="M20,36.243H38M29,3.243C29.796,2.447 30.875,2 32,2C32.557,2 33.109,2.11 33.624,2.323C34.138,2.536 34.606,2.849 35,3.243C35.394,3.637 35.707,4.104 35.92,4.619C36.133,5.134 36.243,5.685 36.243,6.243C36.243,6.8 36.133,7.351 35.92,7.866C35.707,8.381 35.394,8.849 35,9.243L10,34.243L2,36.243L4,28.243L29,3.243Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="44dp"
android:viewportWidth="36"
android:viewportHeight="44">
<path
android:pathData="M20,2H6C4.939,2 3.922,2.421 3.172,3.172C2.421,3.922 2,4.939 2,6V38C2,39.061 2.421,40.078 3.172,40.828C3.922,41.579 4.939,42 6,42H30C31.061,42 32.078,41.579 32.828,40.828C33.579,40.078 34,39.061 34,38V16M20,2L34,16M20,2V16H34"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="44dp"
android:viewportWidth="36"
android:viewportHeight="44">
<path
android:pathData="M22,2H6C4.939,2 3.922,2.421 3.172,3.172C2.421,3.922 2,4.939 2,6V38C2,39.061 2.421,40.078 3.172,40.828C3.922,41.579 4.939,42 6,42H30C31.061,42 32.078,41.579 32.828,40.828C33.579,40.078 34,39.061 34,38V14M22,2L34,14M22,2V14H34M18,34V22M12,28H24"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="40dp"
android:viewportWidth="44"
android:viewportHeight="40">
<path
android:pathData="M42,34C42,35.061 41.579,36.078 40.828,36.828C40.078,37.579 39.061,38 38,38H6C4.939,38 3.922,37.579 3.172,36.828C2.421,36.078 2,35.061 2,34V6C2,4.939 2.421,3.922 3.172,3.172C3.922,2.421 4.939,2 6,2H16L20,8H38C39.061,8 40.078,8.421 40.828,9.172C41.579,9.922 42,10.939 42,12V34Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="29dp"
android:height="23dp"
android:viewportWidth="29"
android:viewportHeight="23">
<path
android:pathData="M3.602,22.236C2.847,22.236 2.2,21.967 1.663,21.429C1.125,20.891 0.856,20.245 0.856,19.489V3.012C0.856,2.257 1.125,1.61 1.663,1.072C2.2,0.535 2.847,0.266 3.602,0.266H11.841L14.587,3.012H25.572C26.327,3.012 26.974,3.281 27.512,3.819C28.05,4.356 28.319,5.003 28.319,5.758V19.489C28.319,20.245 28.05,20.891 27.512,21.429C26.974,21.967 26.327,22.236 25.572,22.236H3.602Z"
android:fillColor="#3DC2EC"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="40dp"
android:viewportWidth="44"
android:viewportHeight="40">
<path
android:pathData="M22,18V30M16,24H28M42,34C42,35.061 41.579,36.078 40.828,36.828C40.078,37.579 39.061,38 38,38H6C4.939,38 3.922,37.579 3.172,36.828C2.421,36.078 2,35.061 2,34V6C2,4.939 2.421,3.922 3.172,3.172C3.922,2.421 4.939,2 6,2H16L20,8H38C39.061,8 40.078,8.421 40.828,9.172C41.579,9.922 42,10.939 42,12V34Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="20dp"
android:viewportWidth="48"
android:viewportHeight="20">
<path
android:pathData="M2,10H13.9M33.92,10H45.82M31.9,10C31.9,14.418 28.318,18 23.9,18C19.482,18 15.9,14.418 15.9,10C15.9,5.582 19.482,2 23.9,2C28.318,2 31.9,5.582 31.9,10Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="47dp"
android:height="47dp"
android:viewportWidth="47"
android:viewportHeight="47">
<path
android:pathData="M23.5,0.313C22.665,0.313 21.85,0.617 21.223,1.244L16.564,5.954C16.312,6.09 16.099,6.284 15.943,6.523L1.244,21.223C-0.011,22.471 -0.011,24.522 1.244,25.777L21.223,45.756C22.478,47.005 24.522,47.005 25.777,45.756L45.756,25.777C47.011,24.529 47.011,22.478 45.756,21.223L25.777,1.244C25.15,0.617 24.334,0.313 23.5,0.313ZM23.5,3.677L43.323,23.5L23.5,43.323L3.677,23.5L17.392,9.784L20.291,12.683C20.213,12.961 20.187,13.258 20.187,13.563C20.187,14.785 20.854,15.833 21.844,16.409V30.591C20.854,31.167 20.187,32.215 20.187,33.438C20.187,35.268 21.669,36.75 23.5,36.75C25.331,36.75 26.812,35.268 26.812,33.438C26.812,32.215 26.146,31.167 25.156,30.591V17.6L30.229,22.672C30.157,22.937 30.125,23.215 30.125,23.5C30.125,25.331 31.607,26.813 33.437,26.813C35.268,26.813 36.75,25.331 36.75,23.5C36.75,21.669 35.268,20.188 33.437,20.188C33.153,20.188 32.875,20.22 32.609,20.291L26.709,14.391C26.78,14.125 26.812,13.847 26.812,13.563C26.812,11.732 25.331,10.25 23.5,10.25C23.196,10.25 22.898,10.276 22.62,10.354L19.722,7.455L23.5,3.677Z"
android:fillColor="#3DC2EC"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M32,26C28.686,26 26,28.686 26,32C26,35.314 28.686,38 32,38C35.314,38 38,35.314 38,32C38,28.686 35.314,26 32,26ZM32,26V12C32,10.939 31.579,9.922 30.828,9.172C30.078,8.421 29.061,8 28,8H22M8,14C11.314,14 14,11.314 14,8C14,4.686 11.314,2 8,2C4.686,2 2,4.686 2,8C2,11.314 4.686,14 8,14ZM8,14V38"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="43dp"
android:height="47dp"
android:viewportWidth="43"
android:viewportHeight="47">
<path
android:pathData="M16,38.054C6,41.054 6,33.054 2,32.054M30,44.054V36.314C30.075,35.36 29.946,34.401 29.622,33.501C29.298,32.601 28.786,31.78 28.12,31.094C34.4,30.394 41,28.014 41,17.094C41,14.301 39.925,11.616 38,9.594C38.912,7.151 38.847,4.45 37.82,2.054C37.82,2.054 35.46,1.354 30,5.014C25.416,3.771 20.584,3.771 16,5.014C10.54,1.354 8.18,2.054 8.18,2.054C7.153,4.45 7.088,7.151 8,9.594C6.06,11.631 4.985,14.34 5,17.154C5,27.994 11.6,30.374 17.88,31.154C17.222,31.833 16.715,32.644 16.391,33.533C16.067,34.423 15.934,35.37 16,36.314V44.054"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="16dp"
android:viewportWidth="10"
android:viewportHeight="16">
<path
android:pathData="M2,13.862L7.931,7.931L2,2"
android:strokeLineJoin="round"
android:strokeWidth="2.43316"
android:fillColor="#00000000"
android:strokeColor="#4B70F5"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M10,24H38M38,24L24,10M38,24L24,38"
android:strokeLineJoin="round"
android:strokeWidth="5"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M7,13H17V11H7V13ZM5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V5C3,4.45 3.196,3.979 3.588,3.588C3.979,3.196 4.45,3 5,3H19C19.55,3 20.021,3.196 20.413,3.588C20.804,3.979 21,4.45 21,5V19C21,19.55 20.804,20.021 20.413,20.413C20.021,20.804 19.55,21 19,21H5Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.6,16.2L17.65,9.15L16.25,7.75L10.6,13.4L7.75,10.55L6.35,11.95L10.6,16.2ZM5,21C4.45,21 3.979,20.804 3.588,20.413C3.196,20.021 3,19.55 3,19V5C3,4.45 3.196,3.979 3.588,3.588C3.979,3.196 4.45,3 5,3H19C19.55,3 20.021,3.196 20.413,3.588C20.804,3.979 21,4.45 21,5V19C21,19.55 20.804,20.021 20.413,20.413C20.021,20.804 19.55,21 19,21H5Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="22dp"
android:viewportWidth="18"
android:viewportHeight="22">
<path
android:pathData="M9.875,2H3.75C3.286,2 2.841,2.184 2.513,2.513C2.184,2.841 2,3.286 2,3.75V17.75C2,18.214 2.184,18.659 2.513,18.987C2.841,19.316 3.286,19.5 3.75,19.5H14.25C14.714,19.5 15.159,19.316 15.487,18.987C15.816,18.659 16,18.214 16,17.75V8.125L9.875,2Z"
android:fillColor="#4B70F5"/>
<path
android:pathData="M9.875,2V8.125H16"
android:fillColor="#4B70F5"/>
<group>
<clip-path
android:pathData="M0,0h18v22h-18zM9.875,2H3.75C3.286,2 2.841,2.184 2.513,2.513C2.184,2.841 2,3.286 2,3.75V17.75C2,18.214 2.184,18.659 2.513,18.987C2.841,19.316 3.286,19.5 3.75,19.5H14.25C14.714,19.5 15.159,19.316 15.487,18.987C15.816,18.659 16,18.214 16,17.75V8.125L9.875,2ZM9.875,2V8.125H16"/>
<path
android:pathData="M9.875,2L11.112,0.763C10.784,0.434 10.339,0.25 9.875,0.25V2ZM3.75,2L3.75,0.25L3.75,2ZM2,3.75H0.25H2ZM2,17.75L0.25,17.75L2,17.75ZM16,8.125H17.75C17.75,7.661 17.566,7.216 17.237,6.888L16,8.125ZM9.875,8.125H8.125C8.125,9.092 8.908,9.875 9.875,9.875V8.125ZM9.875,0.25H3.75V3.75H9.875V0.25ZM3.75,0.25C2.822,0.25 1.931,0.619 1.275,1.275L3.75,3.75L3.75,3.75L3.75,0.25ZM1.275,1.275C0.619,1.931 0.25,2.822 0.25,3.75L3.75,3.75L3.75,3.75L1.275,1.275ZM0.25,3.75V17.75H3.75V3.75H0.25ZM0.25,17.75C0.25,18.678 0.619,19.569 1.275,20.225L3.75,17.75L3.75,17.75L0.25,17.75ZM1.275,20.225C1.931,20.881 2.822,21.25 3.75,21.25V17.75L3.75,17.75L1.275,20.225ZM3.75,21.25H14.25V17.75H3.75V21.25ZM14.25,21.25C15.178,21.25 16.069,20.881 16.725,20.225L14.25,17.75L14.25,17.75V21.25ZM16.725,20.225C17.381,19.569 17.75,18.678 17.75,17.75H14.25L14.25,17.75L16.725,20.225ZM17.75,17.75V8.125H14.25V17.75H17.75ZM17.237,6.888L11.112,0.763L8.638,3.237L14.763,9.362L17.237,6.888ZM8.125,2V8.125H11.625V2H8.125ZM9.875,9.875H16V6.375H9.875V9.875Z"
android:fillColor="#4B70F5"/>
</group>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="19dp"
android:height="17dp"
android:viewportWidth="19"
android:viewportHeight="17">
<path
android:pathData="M1.87,16.542C1.356,16.542 0.916,16.343 0.549,15.945C0.183,15.548 0,15.07 0,14.512V2.329C0,1.771 0.183,1.293 0.549,0.895C0.916,0.498 1.356,0.299 1.87,0.299H7.48L9.35,2.329H16.831C17.345,2.329 17.785,2.528 18.152,2.926C18.518,3.323 18.701,3.801 18.701,4.36V14.512C18.701,15.07 18.518,15.548 18.152,15.945C17.785,16.343 17.345,16.542 16.831,16.542H1.87Z"
android:fillColor="#3DC2EC"/>
</vector>

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="28dp"
android:viewportWidth="27"
android:viewportHeight="28">
<path
android:pathData="M9.974,23.29C3.564,25.145 3.564,20.199 1,19.581ZM18.949,27V22.215C18.997,21.625 18.914,21.032 18.706,20.476C18.499,19.919 18.17,19.412 17.744,18.987C21.769,18.555 26,17.083 26,10.332C26,8.605 25.311,6.945 24.077,5.695C24.661,4.184 24.62,2.515 23.962,1.033C23.962,1.033 22.449,0.6 18.949,2.863C16.01,2.095 12.913,2.095 9.974,2.863C6.474,0.6 4.962,1.033 4.962,1.033C4.303,2.515 4.262,4.184 4.846,5.695C3.603,6.954 2.913,8.63 2.923,10.369C2.923,17.071 7.154,18.542 11.179,19.024C10.758,19.445 10.432,19.946 10.225,20.496C10.017,21.045 9.932,21.631 9.974,22.215V27"
android:fillColor="#3DC2EC"/>
<path
android:pathData="M9.974,23.29C3.564,25.145 3.564,20.199 1,19.581M18.949,27V22.215C18.997,21.625 18.914,21.032 18.706,20.476C18.499,19.919 18.17,19.412 17.744,18.987C21.769,18.555 26,17.083 26,10.332C26,8.605 25.311,6.945 24.077,5.695C24.661,4.184 24.62,2.515 23.962,1.033C23.962,1.033 22.449,0.6 18.949,2.863C16.01,2.095 12.913,2.095 9.974,2.863C6.474,0.6 4.962,1.033 4.962,1.033C4.303,2.515 4.262,4.184 4.846,5.695C3.603,6.954 2.913,8.63 2.923,10.369C2.923,17.071 7.154,18.542 11.179,19.024C10.758,19.445 10.432,19.946 10.225,20.496C10.017,21.045 9.932,21.631 9.974,22.215V27"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M28,4H12C10.939,4 9.922,4.421 9.172,5.172C8.421,5.922 8,6.939 8,8V40C8,41.061 8.421,42.078 9.172,42.828C9.922,43.579 10.939,44 12,44H36C37.061,44 38.078,43.579 38.828,42.828C39.579,42.078 40,41.061 40,40V16M28,4L40,16M28,4V16H40M32,26H16M32,34H16M20,18H16"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="24dp"
android:viewportWidth="20"
android:viewportHeight="24">
<path
android:pathData="M11,2H4C3.47,2 2.961,2.211 2.586,2.586C2.211,2.961 2,3.47 2,4V20C2,20.53 2.211,21.039 2.586,21.414C2.961,21.789 3.47,22 4,22H16C16.53,22 17.039,21.789 17.414,21.414C17.789,21.039 18,20.53 18,20V9L11,2Z"
android:fillColor="#3DC2EC"/>
<path
android:pathData="M11,2V9H18"
android:fillColor="#3DC2EC"/>
<group>
<clip-path
android:pathData="M0,0h20v24h-20zM11,2H4C3.47,2 2.961,2.211 2.586,2.586C2.211,2.961 2,3.47 2,4V20C2,20.53 2.211,21.039 2.586,21.414C2.961,21.789 3.47,22 4,22H16C16.53,22 17.039,21.789 17.414,21.414C17.789,21.039 18,20.53 18,20V9L11,2ZM11,2V9H18"/>
<path
android:pathData="M11,2L12.237,0.763C11.909,0.434 11.464,0.25 11,0.25V2ZM4,2L4,0.25L4,2ZM2,4H0.25H2ZM2,20L0.25,20L2,20ZM18,9H19.75C19.75,8.536 19.566,8.091 19.237,7.763L18,9ZM11,9H9.25C9.25,9.967 10.033,10.75 11,10.75V9ZM11,2V0.25H4V2V3.75H11V2ZM4,2L4,0.25C3.005,0.25 2.052,0.645 1.348,1.348L2.586,2.586L3.823,3.823C3.87,3.776 3.934,3.75 4,3.75L4,2ZM2.586,2.586L1.348,1.348C0.645,2.052 0.25,3.005 0.25,4L2,4L3.75,4C3.75,3.934 3.776,3.87 3.823,3.823L2.586,2.586ZM2,4H0.25V20H2H3.75V4H2ZM2,20L0.25,20C0.25,20.995 0.645,21.948 1.348,22.652L2.586,21.414L3.823,20.177C3.776,20.13 3.75,20.066 3.75,20L2,20ZM2.586,21.414L1.348,22.652C2.052,23.355 3.005,23.75 4,23.75V22V20.25C3.934,20.25 3.87,20.224 3.823,20.177L2.586,21.414ZM4,22V23.75H16V22V20.25H4V22ZM16,22V23.75C16.995,23.75 17.948,23.355 18.652,22.652L17.414,21.414L16.177,20.177C16.13,20.224 16.066,20.25 16,20.25V22ZM17.414,21.414L18.652,22.652C19.355,21.948 19.75,20.995 19.75,20H18H16.25C16.25,20.066 16.224,20.13 16.177,20.177L17.414,21.414ZM18,20H19.75V9H18H16.25V20H18ZM18,9L19.237,7.763L12.237,0.763L11,2L9.763,3.237L16.763,10.237L18,9ZM11,2H9.25V9H11H12.75V2H11ZM11,9V10.75H18V9V7.25H11V9Z"
android:fillColor="#3DC2EC"/>
</group>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="33dp"
android:height="29dp"
android:viewportWidth="33"
android:viewportHeight="29">
<path
android:pathData="M31.602,3.265V11.504M31.602,11.504H23.363M31.602,11.504L25.23,5.517C23.754,4.041 21.929,2.962 19.923,2.382C17.918,1.802 15.798,1.74 13.762,2.2C11.726,2.661 9.84,3.63 8.28,5.017C6.719,6.404 5.536,8.163 4.839,10.131M1.393,25.235V16.997M1.393,16.997H9.632M1.393,16.997L7.764,22.983C9.24,24.46 11.066,25.539 13.071,26.119C15.076,26.699 17.196,26.761 19.232,26.301C21.268,25.84 23.154,24.871 24.715,23.484C26.275,22.097 27.459,20.338 28.155,18.37"
android:strokeLineJoin="round"
android:strokeWidth="3.38"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<group>
<clip-path
android:pathData="M0,0h48v48h-48z"/>
<path
android:pathData="M2,8V20M2,20H14M2,20L11.28,11.28C14.042,8.523 17.625,6.738 21.489,6.194C25.354,5.65 29.29,6.377 32.706,8.265C36.121,10.153 38.83,13.1 40.425,16.662C42.02,20.224 42.414,24.207 41.548,28.013C40.682,31.818 38.603,35.239 35.624,37.759C32.645,40.28 28.927,41.765 25.031,41.989C21.135,42.213 17.272,41.165 14.023,39.003C10.775,36.84 8.317,33.681 7.02,30"
android:strokeLineJoin="round"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="16dp"
android:viewportWidth="12"
android:viewportHeight="16">
<path
android:pathData="M1.334,2L10.667,8L1.334,14V2Z"
android:strokeLineJoin="round"
android:strokeWidth="2.5"
android:fillColor="#00000000"
android:strokeColor="#F5F5F5"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="52dp"
android:height="48dp"
android:viewportWidth="52"
android:viewportHeight="48">
<path
android:pathData="M27.152,2.727L33.286,16.525L48.853,17.863C49.098,17.882 49.333,17.971 49.526,18.119C49.72,18.266 49.863,18.465 49.939,18.691C50.015,18.917 50.02,19.16 49.953,19.389C49.886,19.617 49.751,19.821 49.563,19.976L37.78,29.864L41.255,44.526C41.291,44.68 41.295,44.839 41.267,44.995C41.239,45.151 41.18,45.3 41.092,45.433C41.004,45.567 40.89,45.682 40.756,45.773C40.621,45.863 40.47,45.928 40.31,45.962C39.979,46.029 39.633,45.967 39.349,45.789L25.977,38.104L12.569,45.827C12.429,45.909 12.274,45.963 12.112,45.986C11.95,46.01 11.784,46.003 11.625,45.965C11.466,45.926 11.317,45.859 11.185,45.764C11.054,45.671 10.943,45.552 10.859,45.417C10.775,45.284 10.719,45.137 10.694,44.983C10.669,44.83 10.676,44.673 10.715,44.522L14.193,29.86L2.425,19.976C2.178,19.765 2.027,19.468 2.003,19.151C1.98,18.833 2.087,18.519 2.301,18.277C2.536,18.044 2.857,17.909 3.195,17.901L18.703,16.562L24.837,2.727C24.936,2.511 25.098,2.327 25.303,2.198C25.509,2.069 25.749,2 25.994,2C26.24,2 26.48,2.069 26.685,2.198C26.891,2.327 27.053,2.511 27.152,2.727Z"
android:strokeLineJoin="bevel"
android:strokeWidth="4"
android:fillColor="#FFD401"
android:strokeColor="#FFD401"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="44dp"
android:viewportWidth="40"
android:viewportHeight="44">
<path
android:pathData="M2,10H6M6,10H38M6,10V38C6,39.061 6.421,40.078 7.172,40.828C7.922,41.579 8.939,42 10,42H30C31.061,42 32.078,41.579 32.828,40.828C33.579,40.078 34,39.061 34,38V10M12,10V6C12,4.939 12.421,3.922 13.172,3.172C13.922,2.421 14.939,2 16,2H24C25.061,2 26.078,2.421 26.828,3.172C27.579,3.922 28,4.939 28,6V10"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M11.2,0.788L44.155,0.788A10.985,10.985 0,0 1,55.14 11.773L55.14,44.728A10.985,10.985 0,0 1,44.155 55.713L11.2,55.713A10.985,10.985 0,0 1,0.215 44.728L0.215,11.773A10.985,10.985 0,0 1,11.2 0.788z"/>
<path
android:pathData="M11.2,0.788L44.155,0.788A10.985,10.985 0,0 1,55.14 11.773L55.14,44.728A10.985,10.985 0,0 1,44.155 55.713L11.2,55.713A10.985,10.985 0,0 1,0.215 44.728L0.215,11.773A10.985,10.985 0,0 1,11.2 0.788z"
android:fillColor="#2D246D"/>
<path
android:pathData="M31.797,36.489L23.558,28.25L31.797,20.012"
android:strokeLineJoin="round"
android:strokeWidth="3.38"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M11.11,0.788L44.065,0.788A10.985,10.985 0,0 1,55.05 11.773L55.05,44.728A10.985,10.985 0,0 1,44.065 55.713L11.11,55.713A10.985,10.985 0,0 1,0.125 44.728L0.125,11.773A10.985,10.985 0,0 1,11.11 0.788z"/>
<path
android:pathData="M11.11,0.788L44.065,0.788A10.985,10.985 0,0 1,55.05 11.773L55.05,44.728A10.985,10.985 0,0 1,44.065 55.713L11.11,55.713A10.985,10.985 0,0 1,0.125 44.728L0.125,11.773A10.985,10.985 0,0 1,11.11 0.788z"
android:fillColor="#2D246D"/>
<path
android:pathData="M16.602,39.236C15.847,39.236 15.2,38.967 14.663,38.429C14.125,37.891 13.856,37.245 13.856,36.489V20.012C13.856,19.257 14.125,18.61 14.663,18.072C15.2,17.535 15.847,17.266 16.602,17.266H24.841L27.587,20.012H38.572C39.327,20.012 39.974,20.281 40.512,20.819C41.05,21.356 41.319,22.003 41.319,22.758V36.489C41.319,37.245 41.05,37.891 40.512,38.429C39.974,38.967 39.327,39.236 38.572,39.236H16.602Z"
android:fillColor="#3DC2EC"/>
</group>
</vector>

View File

@@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M11.93,0.788L44.887,0.788A10.986,10.986 0,0 1,55.872 11.774L55.872,44.73A10.986,10.986 0,0 1,44.887 55.715L11.93,55.715A10.986,10.986 0,0 1,0.945 44.73L0.945,11.774A10.986,10.986 0,0 1,11.93 0.788z"/>
<path
android:pathData="M11.93,0.788L44.887,0.788A10.986,10.986 0,0 1,55.872 11.774L55.872,44.73A10.986,10.986 0,0 1,44.887 55.715L11.93,55.715A10.986,10.986 0,0 1,0.945 44.73L0.945,11.774A10.986,10.986 0,0 1,11.93 0.788z"
android:fillColor="#2D246D"/>
<group>
<clip-path
android:pathData="M11.93,11.773h32.956v32.956h-32.956z"/>
<path
android:pathData="M24.289,37.864C17.423,39.924 17.423,34.431 14.677,33.744M33.901,41.984V36.669C33.953,36.014 33.864,35.356 33.642,34.738C33.419,34.12 33.068,33.557 32.611,33.085C36.922,32.605 41.454,30.971 41.454,23.473C41.453,21.556 40.716,19.712 39.394,18.324C40.02,16.646 39.976,14.792 39.271,13.147C39.271,13.147 37.65,12.666 33.901,15.179C30.754,14.326 27.436,14.326 24.289,15.179C20.54,12.666 18.92,13.147 18.92,13.147C18.215,14.792 18.17,16.646 18.796,18.324C17.465,19.722 16.726,21.583 16.736,23.514C16.736,30.957 21.268,32.591 25.58,33.126C25.128,33.593 24.78,34.15 24.557,34.761C24.335,35.371 24.243,36.021 24.289,36.669V41.984"
android:strokeLineJoin="round"
android:strokeWidth="3.38015"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</group>
</group>
</vector>

View File

@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M11.843,0.788L44.8,0.788A10.986,10.986 0,0 1,55.785 11.774L55.785,44.73A10.986,10.986 0,0 1,44.8 55.715L11.843,55.715A10.986,10.986 0,0 1,0.858 44.73L0.858,11.774A10.986,10.986 0,0 1,11.843 0.788z"/>
<path
android:pathData="M11.843,0.788L44.8,0.788A10.986,10.986 0,0 1,55.785 11.774L55.785,44.73A10.986,10.986 0,0 1,44.8 55.715L11.843,55.715A10.986,10.986 0,0 1,0.858 44.73L0.858,11.774A10.986,10.986 0,0 1,11.843 0.788z"
android:fillColor="#2D246D"/>
<path
android:pathData="M28.322,39.237C27.566,39.237 26.92,38.968 26.382,38.43C25.844,37.893 25.575,37.246 25.575,36.491C25.575,35.735 25.844,35.089 26.382,34.551C26.92,34.013 27.566,33.744 28.322,33.744C29.077,33.744 29.723,34.013 30.261,34.551C30.799,35.089 31.068,35.735 31.068,36.491C31.068,37.246 30.799,37.893 30.261,38.43C29.723,38.968 29.077,39.237 28.322,39.237ZM28.322,30.998C27.566,30.998 26.92,30.729 26.382,30.191C25.844,29.653 25.575,29.007 25.575,28.252C25.575,27.496 25.844,26.85 26.382,26.312C26.92,25.774 27.566,25.505 28.322,25.505C29.077,25.505 29.723,25.774 30.261,26.312C30.799,26.85 31.068,27.496 31.068,28.252C31.068,29.007 30.799,29.653 30.261,30.191C29.723,30.729 29.077,30.998 28.322,30.998ZM28.322,22.759C27.566,22.759 26.92,22.49 26.382,21.952C25.844,21.414 25.575,20.768 25.575,20.013C25.575,19.257 25.844,18.611 26.382,18.073C26.92,17.535 27.566,17.266 28.322,17.266C29.077,17.266 29.723,17.535 30.261,18.073C30.799,18.611 31.068,19.257 31.068,20.013C31.068,20.768 30.799,21.414 30.261,21.952C29.723,22.49 29.077,22.759 28.322,22.759Z"
android:fillColor="#3DC2EC"/>
</group>
</vector>

View File

@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="55dp"
android:height="56dp"
android:viewportWidth="55"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M11.02,0.788L43.975,0.788A10.985,10.985 0,0 1,54.96 11.773L54.96,44.728A10.985,10.985 0,0 1,43.975 55.713L11.02,55.713A10.985,10.985 0,0 1,0.035 44.728L0.035,11.773A10.985,10.985 0,0 1,11.02 0.788z"/>
<path
android:pathData="M11.02,0.788L43.975,0.788A10.985,10.985 0,0 1,54.96 11.773L54.96,44.728A10.985,10.985 0,0 1,43.975 55.713L11.02,55.713A10.985,10.985 0,0 1,0.035 44.728L0.035,11.773A10.985,10.985 0,0 1,11.02 0.788z"
android:fillColor="#2D246D"/>
<path
android:pathData="M17.885,15.892L37.109,28.25L17.885,40.608V15.892Z"
android:strokeLineJoin="round"
android:strokeWidth="3.38"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<group>
<clip-path
android:pathData="M11.843,0.788L44.8,0.788A10.986,10.986 0,0 1,55.785 11.774L55.785,44.73A10.986,10.986 0,0 1,44.8 55.715L11.843,55.715A10.986,10.986 0,0 1,0.858 44.73L0.858,11.774A10.986,10.986 0,0 1,11.843 0.788z"/>
<path
android:pathData="M11.843,0.788L44.8,0.788A10.986,10.986 0,0 1,55.785 11.774L55.785,44.73A10.986,10.986 0,0 1,44.8 55.715L11.843,55.715A10.986,10.986 0,0 1,0.858 44.73L0.858,11.774A10.986,10.986 0,0 1,11.843 0.788z"
android:fillColor="#2D246D"/>
<path
android:pathData="M35.188,40.61V29.625H21.456V40.61M21.456,15.893V22.759H32.441M37.934,40.61H18.709C17.981,40.61 17.282,40.321 16.767,39.806C16.252,39.291 15.963,38.592 15.963,37.864V18.639C15.963,17.911 16.252,17.212 16.767,16.698C17.282,16.182 17.981,15.893 18.709,15.893H33.814L40.68,22.759V37.864C40.68,38.592 40.391,39.291 39.876,39.806C39.361,40.321 38.662,40.61 37.934,40.61Z"
android:strokeLineJoin="round"
android:strokeWidth="3.38015"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="23dp"
android:height="28dp"
android:viewportWidth="23"
android:viewportHeight="28">
<path
android:pathData="M3.042,3.5L19.958,14L3.042,24.5V3.5Z"
android:strokeLineJoin="round"
android:strokeWidth="6"
android:fillColor="#3DC2EC"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M38,38L29.3,29.3M34,18C34,26.837 26.837,34 18,34C9.163,34 2,26.837 2,18C2,9.163 9.163,2 18,2C26.837,2 34,9.163 34,18Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M38,38L29.3,29.3M34,18C34,26.837 26.837,34 18,34C9.163,34 2,26.837 2,18C2,9.163 9.163,2 18,2C26.837,2 34,9.163 34,18Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="42dp"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:pathData="M25.403,10.607C25.036,10.981 24.831,11.484 24.831,12.007C24.831,12.531 25.036,13.033 25.403,13.407L28.603,16.607C28.976,16.974 29.479,17.179 30.003,17.179C30.526,17.179 31.029,16.974 31.403,16.607L38.943,9.067C39.948,11.29 40.253,13.766 39.816,16.166C39.378,18.565 38.22,20.775 36.495,22.5C34.77,24.225 32.561,25.383 30.161,25.82C27.761,26.257 25.285,25.953 23.063,24.947L9.243,38.767C8.447,39.563 7.368,40.01 6.243,40.01C5.117,40.01 4.038,39.563 3.243,38.767C2.447,37.972 2,36.893 2,35.767C2,34.642 2.447,33.563 3.243,32.767L17.063,18.947C16.057,16.725 15.752,14.249 16.19,11.849C16.627,9.449 17.785,7.24 19.51,5.515C21.235,3.79 23.444,2.632 25.844,2.194C28.244,1.757 30.72,2.062 32.943,3.067L25.403,10.607Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="31dp"
android:height="31dp"
android:viewportWidth="31"
android:viewportHeight="31">
<path
android:pathData="M18.067,7.909C17.816,8.166 17.675,8.511 17.675,8.871C17.675,9.23 17.816,9.575 18.067,9.832L20.264,12.029C20.521,12.28 20.866,12.421 21.226,12.421C21.585,12.421 21.93,12.28 22.187,12.029L27.363,6.852C28.054,8.378 28.263,10.078 27.963,11.726C27.663,13.373 26.867,14.89 25.683,16.074C24.499,17.259 22.982,18.054 21.334,18.354C19.687,18.654 17.987,18.445 16.461,17.755L6.973,27.243C6.426,27.789 5.685,28.096 4.913,28.096C4.14,28.096 3.399,27.789 2.853,27.243C2.307,26.697 2,25.956 2,25.183C2,24.411 2.307,23.67 2.853,23.124L12.341,13.635C11.651,12.11 11.442,10.41 11.742,8.762C12.042,7.114 12.837,5.597 14.022,4.413C15.206,3.229 16.723,2.434 18.371,2.133C20.018,1.833 21.718,2.042 23.244,2.733L18.067,7.909Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="44dp"
android:viewportWidth="40"
android:viewportHeight="44">
<path
android:pathData="M2,10H6M6,10H38M6,10V38C6,39.061 6.421,40.078 7.172,40.828C7.922,41.579 8.939,42 10,42H30C31.061,42 32.078,41.579 32.828,40.828C33.579,40.078 34,39.061 34,38V10M12,10V6C12,4.939 12.421,3.922 13.172,3.172C13.922,2.421 14.939,2 16,2H24C25.061,2 26.078,2.421 26.828,3.172C27.579,3.922 28,4.939 28,6V10"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="48dp"
android:viewportWidth="32"
android:viewportHeight="48">
<path
android:pathData="M8.42,27.78L6,46L16,40L26,46L23.58,27.76M30,16C30,23.732 23.732,30 16,30C8.268,30 2,23.732 2,16C2,8.268 8.268,2 16,2C23.732,2 30,8.268 30,16Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="35dp"
android:viewportWidth="24"
android:viewportHeight="35">
<path
android:pathData="M7.156,19.7L5.494,32.209L12.36,28.089L19.226,32.209L17.564,19.686M21.972,11.612C21.972,16.92 17.668,21.224 12.36,21.224C7.051,21.224 2.748,16.92 2.748,11.612C2.748,6.303 7.051,2 12.36,2C17.668,2 21.972,6.303 21.972,11.612Z"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:pathData="M12,22L2,12M2,12L12,2M2,12L26,12C28.122,12 30.157,12.843 31.657,14.343C33.157,15.843 34,17.878 34,20L34,34"
android:strokeLineJoin="round"
android:strokeWidth="4"
android:fillColor="#00000000"
android:strokeColor="#3DC2EC"
android:strokeLineCap="round"/>
</vector>

View File

@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="30dp"
android:height="30dp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:pathData="M27,3L3,27M3,3L27,27"
android:strokeLineJoin="round"
android:strokeWidth="6"
android:fillColor="#00000000"
android:strokeColor="#CC425E"
android:strokeLineCap="round"/>
</vector>

View File

@@ -4,4 +4,13 @@
<string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_label">Код</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>
</resources>