Compare commits
11 Commits
main
...
c6418793cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6418793cb | ||
|
|
2da3b58773 | ||
|
|
a04fb915ae | ||
|
|
fa68925205 | ||
|
|
011e169f96 | ||
|
|
cd70c1cd20 | ||
|
|
79650a94e8 | ||
|
|
2af2c36ab2 | ||
|
|
87bbfcc96c | ||
|
|
b74a74f3bd | ||
|
|
d8416283ae |
@@ -2,14 +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.repo.AuthRepository
|
||||||
|
|
||||||
class App: Application() {
|
class App : Application() {
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
context = this
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
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.getInstance(applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package ru.myitschool.work.data.dtos
|
||||||
|
|
||||||
|
class BookingDto {
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -1,16 +1,94 @@
|
|||||||
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.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import ru.myitschool.work.data.source.NetworkDataSource
|
import ru.myitschool.work.data.source.NetworkDataSource
|
||||||
|
|
||||||
object AuthRepository {
|
class AuthRepository private constructor(context: Context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
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 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 {
|
||||||
|
clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getPrefs() = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
|
||||||
|
|
||||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||||
if (success) {
|
if (success) {
|
||||||
codeCache = text
|
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?
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
class BookRepository(private val authRepository: AuthRepository) {
|
||||||
|
|
||||||
|
suspend fun getAvailableBookings(): Result<Map<String, List<PlaceInfo>>> {
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class MainRepository(private val authRepository: AuthRepository) {
|
||||||
|
|
||||||
|
suspend fun getUserInfo(): Result<UserInfoResponse> {
|
||||||
|
val code = authRepository.getCurrentCode()
|
||||||
|
return if (code != null) {
|
||||||
|
NetworkDataSource.getInfo(code)
|
||||||
|
} else {
|
||||||
|
Result.failure(AuthException("user not pass auth"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,192 @@
|
|||||||
package ru.myitschool.work.data.source
|
package ru.myitschool.work.data.source
|
||||||
|
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.client.HttpClient
|
||||||
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.client.engine.cio.CIO
|
||||||
|
import io.ktor.client.plugins.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.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.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.data.model.UserInfoResponse
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BookRequest(
|
||||||
|
val date: String,
|
||||||
|
val placeId: Int
|
||||||
|
)
|
||||||
|
|
||||||
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 = true
|
explicitNulls = false
|
||||||
encodeDefaults = true
|
encodeDefaults = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultRequest {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
||||||
when (response.status) {
|
response.status == HttpStatusCode.OK
|
||||||
HttpStatusCode.OK -> true
|
}.recoverCatching { throwable ->
|
||||||
else -> error(response.bodyAsText())
|
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<Map<String, List<PlaceInfo>>> = withContext(Dispatchers.IO) {
|
||||||
|
return@withContext runCatching {
|
||||||
|
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||||
|
|
||||||
|
when (response.status) {
|
||||||
|
HttpStatusCode.OK -> {
|
||||||
|
try {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -2,48 +2,62 @@ 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.ui.Alignment
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.screen.auth.AuthScreen
|
import ru.myitschool.work.ui.screen.auth.AuthScreen
|
||||||
|
import ru.myitschool.work.ui.screen.book.BookScreen
|
||||||
|
import ru.myitschool.work.ui.screen.main.MainScreen
|
||||||
|
|
||||||
@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 = AuthScreenDestination,
|
startDestination = if (isAuthorized) MainScreenDestination else AuthScreenDestination,
|
||||||
) {
|
) {
|
||||||
composable<AuthScreenDestination> {
|
composable<AuthScreenDestination> {
|
||||||
AuthScreen(navController = navController)
|
AuthScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable<MainScreenDestination> {
|
composable<MainScreenDestination> {
|
||||||
Box(
|
MainScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
composable<BookScreenDestination> {
|
composable<BookScreenDestination> {
|
||||||
Box(
|
BookScreen(navController = navController)
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(text = "Hello")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
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.material3.Button
|
||||||
@@ -16,12 +18,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.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
|
||||||
@@ -34,64 +35,107 @@ import ru.myitschool.work.ui.nav.MainScreenDestination
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AuthScreen(
|
fun AuthScreen(
|
||||||
viewModel: AuthViewModel = viewModel(),
|
viewModel: AuthViewModel = viewModel(factory = AuthViewModelFactory(LocalContext.current)),
|
||||||
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 {
|
viewModel.actionFlow.collect { action ->
|
||||||
navController.navigate(MainScreenDestination)
|
when (action) {
|
||||||
|
is AuthAction.NavigateToMain -> {
|
||||||
|
navController.navigate(MainScreenDestination) {
|
||||||
|
popUpTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(all = 24.dp),
|
.padding(all = 24.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.auth_title),
|
text = stringResource(R.string.auth_title),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(bottom = 32.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val currentState = state) {
|
when (val currentState = state) {
|
||||||
is AuthState.Data -> Content(viewModel, currentState)
|
|
||||||
is AuthState.Loading -> {
|
is AuthState.Loading -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(64.dp)
|
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))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Content(
|
private fun Content(
|
||||||
viewModel: AuthViewModel,
|
state: AuthState.Data,
|
||||||
state: AuthState.Data
|
onTextChange: (String) -> Unit,
|
||||||
|
onSendClick: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
var inputText by remember { mutableStateOf("") }
|
val isButtonEnabled = state.code.length == 4 &&
|
||||||
Spacer(modifier = Modifier.size(16.dp))
|
state.code.matches(Regex("^[a-zA-Z0-9]{4}$"))
|
||||||
TextField(
|
|
||||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
Column(
|
||||||
value = inputText,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onValueChange = {
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
inputText = it
|
) {
|
||||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
if (state.error != null) {
|
||||||
},
|
Text(
|
||||||
label = { Text(stringResource(R.string.auth_label)) }
|
text = state.error,
|
||||||
|
color = Color.Red,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag(TestIds.Auth.ERROR)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(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(
|
Button(
|
||||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
modifier = Modifier
|
||||||
onClick = {
|
.testTag(TestIds.Auth.SIGN_BUTTON)
|
||||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
.fillMaxWidth(),
|
||||||
},
|
onClick = { onSendClick(state.code) },
|
||||||
enabled = true
|
enabled = isButtonEnabled
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.auth_sign_in))
|
Text(stringResource(R.string.auth_sign_in))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
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
|
||||||
object Data: AuthState
|
data class Data(
|
||||||
|
val code: String = "",
|
||||||
|
val error: String? = null
|
||||||
|
) : AuthState
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package ru.myitschool.work.ui.screen.auth
|
package ru.myitschool.work.ui.screen.auth
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
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
|
||||||
@@ -13,31 +14,75 @@ import kotlinx.coroutines.launch
|
|||||||
import ru.myitschool.work.data.repo.AuthRepository
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||||
|
|
||||||
class AuthViewModel : ViewModel() {
|
|
||||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
class AuthViewModel(
|
||||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data)
|
private val authRepository: AuthRepository,
|
||||||
|
private val checkAndSaveAuthCodeUseCase: CheckAndSaveAuthCodeUseCase
|
||||||
|
) : ViewModel() {
|
||||||
|
private val _uiState = MutableStateFlow<AuthState>(AuthState.Data())
|
||||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
private val _actionFlow: MutableSharedFlow<Unit> = MutableSharedFlow()
|
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
||||||
val actionFlow: SharedFlow<Unit> = _actionFlow
|
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
||||||
|
|
||||||
fun onIntent(intent: AuthIntent) {
|
fun onIntent(intent: AuthIntent) {
|
||||||
when (intent) {
|
when (intent) {
|
||||||
is AuthIntent.Send -> {
|
is AuthIntent.Send -> {
|
||||||
viewModelScope.launch(Dispatchers.Default) {
|
if (validateCode(intent.text)) {
|
||||||
|
viewModelScope.launch {
|
||||||
_uiState.update { AuthState.Loading }
|
_uiState.update { AuthState.Loading }
|
||||||
checkAndSaveAuthCodeUseCase.invoke("9999").fold(
|
|
||||||
|
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
_actionFlow.emit(Unit)
|
_actionFlow.emit(AuthAction.NavigateToMain)
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
error.printStackTrace()
|
_uiState.update {
|
||||||
_actionFlow.emit(Unit)
|
AuthState.Data(
|
||||||
}
|
code = intent.text,
|
||||||
|
error = error.message ?: "error"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is AuthIntent.TextInput -> Unit
|
)
|
||||||
}
|
}
|
||||||
|
} 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,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
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun BookScreen(
|
||||||
|
viewModel: BookViewModel = viewModel(factory = BookViewModelFactory(LocalContext.current)),
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (val currentState = state) {
|
||||||
|
BookState.Loading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 -> {
|
||||||
|
if (currentState.error != null) {
|
||||||
|
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 = currentState.error,
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ERROR),
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
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 {
|
||||||
|
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(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
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())
|
||||||
|
) {
|
||||||
|
ScrollableTabRow(
|
||||||
|
selectedTabIndex = state.selectedDateIndex,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
edgePadding = 0.dp
|
||||||
|
) {
|
||||||
|
state.dates.forEachIndexed { index, dateItem ->
|
||||||
|
Tab(
|
||||||
|
selected = state.selectedDateIndex == index,
|
||||||
|
onClick = { onDateSelect(index) },
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.getIdDateItemByPosition(index))
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = dateItem.displayDate,
|
||||||
|
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
val selectedDate = state.dates.getOrNull(state.selectedDateIndex)
|
||||||
|
if (selectedDate != null) {
|
||||||
|
Column {
|
||||||
|
selectedDate.places.forEachIndexed { index, placeItem ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.selectable(
|
||||||
|
selected = state.selectedPlaceIndex == index,
|
||||||
|
onClick = { onPlaceSelect(index) }
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 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),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.book
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
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.model.PlaceInfo
|
||||||
|
import ru.myitschool.work.data.repo.AuthRepository
|
||||||
|
import ru.myitschool.work.data.repo.BookRepository
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class BookViewModel(private val bookRepo: BookRepository) : 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 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 {
|
||||||
|
BookState.Data(
|
||||||
|
dates = dateItems,
|
||||||
|
selectedDateIndex = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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,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
|
||||||
|
}
|
||||||
@@ -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(factory = MainViewModelFactory(LocalContext.current)),
|
||||||
|
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)
|
||||||
|
.padding(bottom = 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()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 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(bottom = 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState.bookings.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = "Мои бронирования:",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
itemsIndexed(currentState.bookings) { index, booking ->
|
||||||
|
BookingItem(
|
||||||
|
booking = booking,
|
||||||
|
position = index,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.testTag(TestIds.Main.getIdItemByPosition(index))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "У вас нет активных бронирований",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BookingItem(
|
||||||
|
booking: BookingItem,
|
||||||
|
position: Int,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package ru.myitschool.work.ui.screen.main
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
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
|
||||||
|
|
||||||
|
class MainViewModel(
|
||||||
|
private val authRepo: AuthRepository,
|
||||||
|
private val mainRepo: MainRepository
|
||||||
|
) : 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 -> {
|
||||||
|
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(
|
||||||
|
userName = "",
|
||||||
|
userPhotoUrl = null,
|
||||||
|
bookings = emptyList(),
|
||||||
|
error = error.message ?: "Ошибка загрузки данных"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainViewModelFactory(private val context: Context) : ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
|
||||||
|
val authRepository = AuthRepository.getInstance(context)
|
||||||
|
val mainRepository = MainRepository(authRepository)
|
||||||
|
return MainViewModel(authRepository, mainRepository) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface MainAction {
|
||||||
|
object NavigateToAuth : MainAction
|
||||||
|
object NavigateToBooking : MainAction
|
||||||
|
}
|
||||||
27
app/src/main/res/drawable/actions_icon.xml
Normal file
27
app/src/main/res/drawable/actions_icon.xml
Normal 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>
|
||||||
19
app/src/main/res/drawable/app_start_button.xml
Normal file
19
app/src/main/res/drawable/app_start_button.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/back.xml
Normal file
13
app/src/main/res/drawable/back.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/branch_opened.xml
Normal file
13
app/src/main/res/drawable/branch_opened.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/cpu.xml
Normal file
13
app/src/main/res/drawable/cpu.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/editor.xml
Normal file
13
app/src/main/res/drawable/editor.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/file.xml
Normal file
13
app/src/main/res/drawable/file.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/file_plus.xml
Normal file
13
app/src/main/res/drawable/file_plus.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/folder.xml
Normal file
13
app/src/main/res/drawable/folder.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/folder_filled.xml
Normal file
9
app/src/main/res/drawable/folder_filled.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/folder_plus.xml
Normal file
13
app/src/main/res/drawable/folder_plus.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/git_commit.xml
Normal file
13
app/src/main/res/drawable/git_commit.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/git_icon.xml
Normal file
9
app/src/main/res/drawable/git_icon.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/git_pull.xml
Normal file
13
app/src/main/res/drawable/git_pull.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/github.xml
Normal file
13
app/src/main/res/drawable/github.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/ic_arrow.xml
Normal file
13
app/src/main/res/drawable/ic_arrow.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/ic_arrow_common.xml
Normal file
13
app/src/main/res/drawable/ic_arrow_common.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_checkbox_no.xml
Normal file
9
app/src/main/res/drawable/ic_checkbox_no.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_checkbox_yes.xml
Normal file
9
app/src/main/res/drawable/ic_checkbox_yes.xml
Normal 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>
|
||||||
19
app/src/main/res/drawable/ic_file.xml
Normal file
19
app/src/main/res/drawable/ic_file.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_folder.xml
Normal file
9
app/src/main/res/drawable/ic_folder.xml
Normal 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>
|
||||||
16
app/src/main/res/drawable/ic_git_clone_mini.xml
Normal file
16
app/src/main/res/drawable/ic_git_clone_mini.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/ic_paste.xml
Normal file
13
app/src/main/res/drawable/ic_paste.xml
Normal 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>
|
||||||
19
app/src/main/res/drawable/ic_project_create_mini.xml
Normal file
19
app/src/main/res/drawable/ic_project_create_mini.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/ic_recreate.xml
Normal file
13
app/src/main/res/drawable/ic_recreate.xml
Normal 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>
|
||||||
17
app/src/main/res/drawable/ic_reset.xml
Normal file
17
app/src/main/res/drawable/ic_reset.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/ic_run_mini.xml
Normal file
13
app/src/main/res/drawable/ic_run_mini.xml
Normal 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>
|
||||||
12
app/src/main/res/drawable/ic_star.xml
Normal file
12
app/src/main/res/drawable/ic_star.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/icon.xml
Normal file
13
app/src/main/res/drawable/icon.xml
Normal 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>
|
||||||
20
app/src/main/res/drawable/img_icon_back.xml
Normal file
20
app/src/main/res/drawable/img_icon_back.xml
Normal 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>
|
||||||
16
app/src/main/res/drawable/img_icon_folder.xml
Normal file
16
app/src/main/res/drawable/img_icon_folder.xml
Normal 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>
|
||||||
24
app/src/main/res/drawable/img_icon_git.xml
Normal file
24
app/src/main/res/drawable/img_icon_git.xml
Normal 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>
|
||||||
16
app/src/main/res/drawable/img_icon_options.xml
Normal file
16
app/src/main/res/drawable/img_icon_options.xml
Normal 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>
|
||||||
20
app/src/main/res/drawable/img_icon_run.xml
Normal file
20
app/src/main/res/drawable/img_icon_run.xml
Normal 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>
|
||||||
20
app/src/main/res/drawable/img_icon_save.xml
Normal file
20
app/src/main/res/drawable/img_icon_save.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/play_icon.xml
Normal file
13
app/src/main/res/drawable/play_icon.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/search.xml
Normal file
13
app/src/main/res/drawable/search.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/search_1.xml
Normal file
13
app/src/main/res/drawable/search_1.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/settings_icon.xml
Normal file
13
app/src/main/res/drawable/settings_icon.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/settings_menu.xml
Normal file
13
app/src/main/res/drawable/settings_menu.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/trash.xml
Normal file
13
app/src/main/res/drawable/trash.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/tutorials.xml
Normal file
13
app/src/main/res/drawable/tutorials.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/tutorials_menu.xml
Normal file
13
app/src/main/res/drawable/tutorials_menu.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/undo.xml
Normal file
13
app/src/main/res/drawable/undo.xml
Normal 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>
|
||||||
13
app/src/main/res/drawable/x_icon.xml
Normal file
13
app/src/main/res/drawable/x_icon.xml
Normal 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>
|
||||||
@@ -4,4 +4,13 @@
|
|||||||
<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>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user