Initial commit
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -4,6 +4,3 @@
|
||||
[submodule "buildSrc"]
|
||||
path = buildSrc
|
||||
url = https://git.sicampus.ru/core/dependecies.git
|
||||
[submodule "test-lib"]
|
||||
path = testLib
|
||||
url = https://git.sicampus.ru/core/test-lib
|
||||
|
||||
@@ -47,8 +47,4 @@ dependencies {
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$ktor")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
|
||||
|
||||
androidTestImplementation(project(path = ":testLib"))
|
||||
androidTestImplementation("io.github.kakaocup:compose:1.0.0")
|
||||
androidTestImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
|
||||
}
|
||||
@@ -1,474 +0,0 @@
|
||||
@file:OptIn(ExperimentalTestApi::class)
|
||||
|
||||
package ru.myitschool.work
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
import ru.myitschool.work.screens.AuthScreen
|
||||
import ru.myitschool.work.screens.BookScreen
|
||||
import ru.myitschool.work.screens.MainScreen
|
||||
import ru.myitschool.work.screens.getDateText
|
||||
import ru.myitschool.work.screens.getPlaceText
|
||||
import ru.myitschool.work.ui.root.RootActivity
|
||||
import ru.myitschool.work.utils.MockWebServerRule
|
||||
import ru.myitschool.work.utils.Response
|
||||
import ru.samsung.test.core.core.BaseTest
|
||||
import ru.samsung.test.core.utils.warmUpCompose
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
class Tests : BaseTest<RootActivity>(
|
||||
clazz = RootActivity::class.java,
|
||||
isEnabledCompose = true,
|
||||
) {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<RootActivity>()
|
||||
|
||||
@get:Rule
|
||||
val serverRule = MockWebServerRule(8090)
|
||||
|
||||
@Test
|
||||
fun aПроверка_экрана_авторизации_валидация() = runWithInit(1) {
|
||||
onComposeScreen<AuthScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
errorText.assertIsNotDisplayed()
|
||||
signButton.assertIsDisplayed()
|
||||
codeInput.assertIsDisplayed()
|
||||
codeInput.assertTextEquals("Код", includeEditableText = false)
|
||||
}
|
||||
|
||||
step("Проверка состояния при пустом поле ввода") {
|
||||
signButton.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
step("Проверка состояния при вводе неверной комбинации 1") {
|
||||
codeInput.performTextReplacement("1234567890")
|
||||
signButton.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
step("Проверка состояния при вводе верной комбинации 1") {
|
||||
codeInput.performTextReplacement("1230")
|
||||
signButton.assertIsEnabled()
|
||||
}
|
||||
|
||||
step("Проверка состояния при вводе неверной комбинации 2") {
|
||||
codeInput.performTextReplacement("прив")
|
||||
signButton.assertIsNotEnabled()
|
||||
}
|
||||
|
||||
step("Проверка состояния при вводе верной комбинации 2") {
|
||||
codeInput.performTextReplacement("aAzZ")
|
||||
signButton.assertIsEnabled()
|
||||
}
|
||||
|
||||
step("Проверка состояния при вводе неверной комбинации 3") {
|
||||
codeInput.performTextReplacement("1пLK")
|
||||
signButton.assertIsNotEnabled()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bПроверка_экрана_авторизации_статусы() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abcd/auth" to Response(statusCode = 401),
|
||||
"/api/1234/auth" to Response(statusCode = 400),
|
||||
"/api/4321/auth" to Response(statusCode = 500),
|
||||
"/api/abc1/auth" to Response(statusCode = 200),
|
||||
"/api/abc1/info" to Response(statusCode = 400),
|
||||
)
|
||||
onComposeScreen<AuthScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
errorText.assertIsNotDisplayed()
|
||||
signButton.assertIsDisplayed()
|
||||
codeInput.assertIsDisplayed()
|
||||
}
|
||||
|
||||
listOf(
|
||||
"abcd",
|
||||
"1234",
|
||||
"4321"
|
||||
).forEachIndexed { index, code ->
|
||||
warmUpCompose(composeTestRule)
|
||||
step("Проверка статуса ошибки $index") {
|
||||
codeInput.performTextReplacement(code)
|
||||
flakySafely(timeoutMs = 500L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
}
|
||||
signButton.performClick()
|
||||
flakySafely(timeoutMs = 500L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
step("Проверка верного статуса") {
|
||||
codeInput.performTextReplacement("abc1")
|
||||
errorText.assertIsNotDisplayed()
|
||||
signButton.assertIsEnabled()
|
||||
signButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка перехода на главный экран") {
|
||||
errorText.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cПроверка_главного_экрана() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abc1/info" to Response(assetFile = "profile.json"),
|
||||
"/api/abc1/info" to Response(assetFile = "profile2.json"),
|
||||
)
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
addButton.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
logoutButton.assertIsDisplayed()
|
||||
profileNameText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
step("Проверка контента на экране") {
|
||||
profileNameText.assertTextEquals("Иван А")
|
||||
getItemByPosition(0).invoke {
|
||||
getDateText().assertTextEquals("05.01.2025")
|
||||
getPlaceText().assertTextEquals("102")
|
||||
}
|
||||
getItemByPosition(1).invoke {
|
||||
getDateText().assertTextEquals("06.01.2025")
|
||||
getPlaceText().assertTextEquals("209.13")
|
||||
}
|
||||
getItemByPosition(2).invoke {
|
||||
getDateText().assertTextEquals("09.01.2025")
|
||||
getPlaceText().assertTextEquals("Зона 51. 50")
|
||||
}
|
||||
}
|
||||
|
||||
step("Обновляем контент и проверяем повторно") {
|
||||
refreshButton.performClick()
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
profileNameText.assertTextEquals("Вова Б")
|
||||
}
|
||||
getItemByPosition(0).invoke {
|
||||
getDateText().assertTextEquals("01.01.2001")
|
||||
getPlaceText().assertTextEquals("1")
|
||||
}
|
||||
getItemByPosition(1).invoke {
|
||||
getDateText().assertTextEquals("01.01.2002")
|
||||
getPlaceText().assertTextEquals("2")
|
||||
}
|
||||
getItemByPosition(2).invoke {
|
||||
getDateText().assertTextEquals("01.01.2003")
|
||||
getPlaceText().assertTextEquals("3")
|
||||
}
|
||||
getItemByPosition(3).assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dПроверка_кнопки_выхода() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abc1/info" to Response(assetFile = "profile.json"),
|
||||
)
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
addButton.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
logoutButton.assertIsDisplayed()
|
||||
profileNameText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
step("Нажимаем на кнопку выхода") {
|
||||
logoutButton.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
onComposeScreen<AuthScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
errorText.assertIsNotDisplayed()
|
||||
signButton.assertIsDisplayed()
|
||||
codeInput.assertIsDisplayed()
|
||||
codeInput.assertTextEquals("Код", includeEditableText = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun eПроверка_навигации_брони() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abc1/auth" to Response(statusCode = 200),
|
||||
"/api/abc1/info" to Response(assetFile = "profile.json"),
|
||||
"/api/abc1/booking" to Response(statusCode = 400),
|
||||
)
|
||||
|
||||
onComposeScreen<AuthScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
errorText.assertIsNotDisplayed()
|
||||
signButton.assertIsDisplayed()
|
||||
codeInput.assertIsDisplayed()
|
||||
}
|
||||
|
||||
step("Вход") {
|
||||
codeInput.performTextReplacement("abc1")
|
||||
errorText.assertIsNotDisplayed()
|
||||
signButton.assertIsEnabled()
|
||||
signButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
addButton.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
logoutButton.assertIsDisplayed()
|
||||
profileNameText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
step("Нажимаем на кнопку добавления") {
|
||||
addButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<BookScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
}
|
||||
pressBack()
|
||||
}
|
||||
}
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка контента на экране (обратная навигация)") {
|
||||
profileNameText.assertTextEquals("Иван А")
|
||||
getItemByPosition(0).invoke {
|
||||
getDateText().assertTextEquals("05.01.2025")
|
||||
getPlaceText().assertTextEquals("102")
|
||||
}
|
||||
getItemByPosition(1).invoke {
|
||||
getDateText().assertTextEquals("06.01.2025")
|
||||
getPlaceText().assertTextEquals("209.13")
|
||||
}
|
||||
getItemByPosition(2).invoke {
|
||||
getDateText().assertTextEquals("09.01.2025")
|
||||
getPlaceText().assertTextEquals("Зона 51. 50")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fПроверка_контента_брони() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abc1/info" to Response(assetFile = "profile.json"),
|
||||
"/api/abc1/booking" to Response(statusCode = 400),
|
||||
"/api/abc1/booking" to Response(assetFile = "booking.json"),
|
||||
)
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
addButton.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
logoutButton.assertIsDisplayed()
|
||||
profileNameText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
step("Нажимаем на кнопку добавления") {
|
||||
addButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<BookScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
}
|
||||
refreshButton.performClick()
|
||||
}
|
||||
step("Проверка контента") {
|
||||
getNodeDateByPosition(0).invoke {
|
||||
assertTextEquals("05.01")
|
||||
assertIsSelected()
|
||||
}
|
||||
getNodeDateByPosition(1).assertTextEquals("06.01")
|
||||
getNodeDateByPosition(2).assertTextEquals("07.02")
|
||||
getNodeDateByPosition(3).assertTextEquals("08.03")
|
||||
getNodePlaceByPosition(0).invoke {
|
||||
assertTextEquals("102")
|
||||
assertIsSelected()
|
||||
}
|
||||
getNodePlaceByPosition(1).assertTextEquals("666")
|
||||
getNodePlaceByPosition(2).assertIsNotDisplayed()
|
||||
}
|
||||
step("Проверка выбора переговорок") {
|
||||
getNodePlaceByPosition(1).invoke {
|
||||
performClick()
|
||||
assertIsSelected()
|
||||
}
|
||||
getNodePlaceByPosition(0).assertIsNotSelected()
|
||||
}
|
||||
step("Проверка переключения дат") {
|
||||
getNodeDateByPosition(2).invoke {
|
||||
performClick()
|
||||
assertIsSelected()
|
||||
}
|
||||
getNodePlaceByPosition(0).invoke {
|
||||
assertTextEquals("102")
|
||||
assertIsSelected()
|
||||
}
|
||||
getNodePlaceByPosition(1).assertTextEquals("209.13")
|
||||
getNodePlaceByPosition(2).assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gПроверка_возможности_брони() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abc1/info" to Response(assetFile = "profile.json"),
|
||||
"/api/abc1/booking" to Response(assetFile = "booking.json"),
|
||||
"/api/abc1/book" to Response(statusCode = 201),
|
||||
"/api/abc1/info" to Response(assetFile = "profile2.json"),
|
||||
)
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
addButton.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
logoutButton.assertIsDisplayed()
|
||||
profileNameText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
step("Нажимаем на кнопку добавления") {
|
||||
addButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<BookScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
backButton.assertIsDisplayed()
|
||||
bookButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Оформление брони") {
|
||||
bookButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверяем данные с главного экрана") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
profileNameText.assertTextEquals("Вова Б")
|
||||
}
|
||||
getItemByPosition(0).invoke {
|
||||
getDateText().assertTextEquals("01.01.2001")
|
||||
getPlaceText().assertTextEquals("1")
|
||||
}
|
||||
getItemByPosition(1).invoke {
|
||||
getDateText().assertTextEquals("01.01.2002")
|
||||
getPlaceText().assertTextEquals("2")
|
||||
}
|
||||
getItemByPosition(2).invoke {
|
||||
getDateText().assertTextEquals("01.01.2003")
|
||||
getPlaceText().assertTextEquals("3")
|
||||
}
|
||||
getItemByPosition(3).assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun hПроверка_пустых_слотов() = runWithInit(1) {
|
||||
serverRule.mockResponses(
|
||||
"/api/abc1/info" to Response(assetFile = "profile.json"),
|
||||
"/api/abc1/booking" to Response(assetFile = "booking2.json"),
|
||||
"/api/abc1/booking" to Response(assetFile = "booking3.json")
|
||||
)
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
errorText.assertIsNotDisplayed()
|
||||
addButton.assertIsDisplayed()
|
||||
refreshButton.assertIsDisplayed()
|
||||
logoutButton.assertIsDisplayed()
|
||||
profileNameText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
step("Нажимаем на кнопку добавления") {
|
||||
addButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<BookScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
backButton.assertIsDisplayed()
|
||||
bookButton.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Проверка пустого состояния") {
|
||||
emptyText.assertIsNotDisplayed()
|
||||
getNodeDateByPosition(0).assertIsDisplayed()
|
||||
getNodeDateByPosition(1).assertIsDisplayed()
|
||||
getNodeDateByPosition(2).assertIsDisplayed()
|
||||
getNodeDateByPosition(3).assertIsNotDisplayed()
|
||||
}
|
||||
step("Возврат на главную") {
|
||||
backButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<MainScreen>(composeTestRule) {
|
||||
step("Нажимаем на кнопку добавления") {
|
||||
addButton.performClick()
|
||||
}
|
||||
}
|
||||
onComposeScreen<BookScreen>(composeTestRule) {
|
||||
step("Проверка наличия всех элементов на экране") {
|
||||
flakySafely(timeoutMs = 5000L, intervalMs = 100L) {
|
||||
warmUpCompose(composeTestRule)
|
||||
backButton.assertIsDisplayed()
|
||||
bookButton.assertIsNotDisplayed()
|
||||
}
|
||||
}
|
||||
step("Проверка пустого состояния") {
|
||||
emptyText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package ru.myitschool.work.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.kaspersky.components.composesupport.core.KNode
|
||||
import ru.myitschool.work.core.TestIds
|
||||
|
||||
class AuthScreen(
|
||||
semanticsProvider: SemanticsNodeInteractionsProvider
|
||||
) : UiBaseScreen<AuthScreen>(semanticsProvider) {
|
||||
val errorText = child<KNode> {
|
||||
hasTestTag(TestIds.Auth.ERROR)
|
||||
}
|
||||
val signButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Auth.SIGN_BUTTON)
|
||||
}
|
||||
val codeInput: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Auth.CODE_INPUT)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package ru.myitschool.work.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.kaspersky.components.composesupport.core.KNode
|
||||
import ru.myitschool.work.core.TestIds
|
||||
|
||||
class BookScreen(
|
||||
semanticsProvider: SemanticsNodeInteractionsProvider
|
||||
) : UiBaseScreen<BookScreen>(semanticsProvider) {
|
||||
val errorText = child<KNode> {
|
||||
hasTestTag(TestIds.Book.ERROR)
|
||||
}
|
||||
val emptyText: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Book.EMPTY)
|
||||
}
|
||||
val refreshButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Book.REFRESH_BUTTON)
|
||||
}
|
||||
val backButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Book.BACK_BUTTON)
|
||||
}
|
||||
val bookButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Book.BOOK_BUTTON)
|
||||
}
|
||||
|
||||
fun getNodeDateByPosition(position: Int) = child<KNode> {
|
||||
hasTestTag(TestIds.Book.getIdDateItemByPosition(position))
|
||||
}
|
||||
|
||||
fun getNodePlaceByPosition(position: Int) = child<KNode> {
|
||||
hasTestTag(TestIds.Book.getIdPlaceItemByPosition(position))
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package ru.myitschool.work.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.kaspersky.components.composesupport.core.KNode
|
||||
import io.github.kakaocup.compose.node.core.BaseNode
|
||||
import ru.myitschool.work.core.TestIds
|
||||
|
||||
class MainScreen(
|
||||
semanticsProvider: SemanticsNodeInteractionsProvider
|
||||
) : UiBaseScreen<MainScreen>(semanticsProvider) {
|
||||
val errorText = child<KNode> {
|
||||
hasTestTag(TestIds.Main.ERROR)
|
||||
}
|
||||
val addButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Main.ADD_BUTTON)
|
||||
}
|
||||
val refreshButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Main.REFRESH_BUTTON)
|
||||
}
|
||||
val logoutButton: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Main.LOGOUT_BUTTON)
|
||||
}
|
||||
val profileNameText: KNode = child<KNode> {
|
||||
hasTestTag(TestIds.Main.PROFILE_NAME)
|
||||
}
|
||||
|
||||
fun getItemByPosition(position: Int) = child<KNode> {
|
||||
hasTestTag(TestIds.Main.getIdItemByPosition(position))
|
||||
}
|
||||
}
|
||||
fun KNode.getDateText() = child<KNode> { hasTestTag(TestIds.Main.ITEM_DATE) }
|
||||
|
||||
fun KNode.getPlaceText() = child<KNode> { hasTestTag(TestIds.Main.ITEM_PLACE) }
|
||||
@@ -1,10 +0,0 @@
|
||||
package ru.myitschool.work.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
|
||||
open class UiBaseScreen<out T : ComposeScreen<T>>(
|
||||
semanticsProvider: SemanticsNodeInteractionsProvider
|
||||
) : ComposeScreen<T>(
|
||||
semanticsProvider = semanticsProvider
|
||||
)
|
||||
@@ -1,27 +0,0 @@
|
||||
package ru.myitschool.work.utils
|
||||
|
||||
import android.app.LocaleManager
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.rules.ExternalResource
|
||||
import java.util.Locale
|
||||
|
||||
class LocaleRule(
|
||||
private val locale: Locale,
|
||||
) : ExternalResource() {
|
||||
|
||||
override fun before() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.getSystemService(LocaleManager::class.java)
|
||||
.applicationLocales = LocaleList(locale)
|
||||
} else {
|
||||
AppCompatDelegate.setApplicationLocales(
|
||||
LocaleListCompat.create(locale)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package ru.myitschool.work.utils
|
||||
|
||||
import okhttp3.mockwebserver.Dispatcher
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
import java.net.HttpURLConnection
|
||||
import java.util.LinkedList
|
||||
import java.util.Queue
|
||||
|
||||
class MockWebServerRule(private val port: Int) : TestRule {
|
||||
val server: MockWebServer get() = _server
|
||||
|
||||
private lateinit var _server: MockWebServer
|
||||
private val dispatcher = object : Dispatcher() {
|
||||
private val responses = mutableMapOf<String, Queue<Response>>()
|
||||
var requestListener: ((RecordedRequest) -> Unit)? = null
|
||||
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
requestListener?.invoke(request)
|
||||
val response = responses[request.path]?.poll()
|
||||
?: return MockResponse().apply {
|
||||
setResponseCode(HttpURLConnection.HTTP_NOT_FOUND)
|
||||
}
|
||||
return MockResponse().apply {
|
||||
setResponseCode(response.statusCode)
|
||||
if (response.assetFile != null) {
|
||||
val resource = this.javaClass.classLoader
|
||||
?.getResourceAsStream(response.assetFile)
|
||||
?: throw IllegalStateException("File not found")
|
||||
setBody(String(resource.readBytes()))
|
||||
response.contentType?.let { setHeader("Content-Type", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addMockResponse(requestUrl: String, response: Response) {
|
||||
val queue = responses.getOrPut(requestUrl) { LinkedList() }
|
||||
queue.add(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
_server = MockWebServer()
|
||||
_server.dispatcher = dispatcher
|
||||
_server.start(port = port)
|
||||
try {
|
||||
base.evaluate()
|
||||
} finally {
|
||||
try {
|
||||
_server.dispatcher.shutdown()
|
||||
_server.shutdown()
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setRecorderListener(listener: (RecordedRequest) -> Unit) {
|
||||
dispatcher.requestListener = listener
|
||||
}
|
||||
|
||||
fun removeRecorderListener() {
|
||||
dispatcher.requestListener = null
|
||||
}
|
||||
|
||||
fun mockResponses(vararg pairs: Pair<String, Response>) {
|
||||
pairs.forEach { (request, response) ->
|
||||
dispatcher.addMockResponse(request, response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Response(
|
||||
val assetFile: String? = null,
|
||||
val contentType: String? = "application/json",
|
||||
val statusCode: Int = HttpURLConnection.HTTP_OK,
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"2025-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "666"}],
|
||||
"2025-01-06": [{"id": 3, "place": "Зона 51. 50"}],
|
||||
"2025-02-07": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
|
||||
"2025-03-08": [{"id": 2, "place": "209.13"}]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"2000-01-08": [{"id": 1, "place": "102"},{"id": 2, "place": "666"}],
|
||||
"2000-01-07": [{"id": 3, "place": "Зона 51. 50"}],
|
||||
"2000-01-05": [{"id": 1, "place": "102"},{"id": 2, "place": "209.13"}],
|
||||
"2000-01-01": []
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "Иван А",
|
||||
"photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",
|
||||
"booking": {
|
||||
"2025-01-05": {"id": 1, "place": "102"},
|
||||
"2025-01-06": {"id": 2, "place": "209.13"},
|
||||
"2025-01-09": {"id": 3, "place": "Зона 51. 50"}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "Вова Б",
|
||||
"photoUrl": "https://funnyducks.ru/upload/iblock/0cd/0cdeb7ec3ed6fddda0f90fccee05557d.jpg",
|
||||
"booking": {
|
||||
"2003-01-01": {"id": 4, "place": "3"},
|
||||
"2002-01-01": {"id": 5, "place": "2"},
|
||||
"2001-01-01": {"id": 6, "place": "1"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.data.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class BookRequestDto(
|
||||
@SerialName("date")
|
||||
val date: String,
|
||||
@SerialName("placeId")
|
||||
val placeId: String,
|
||||
)
|
||||
12
app/src/main/java/ru/myitschool/work/data/dto/PlaceDto.kt
Normal file
12
app/src/main/java/ru/myitschool/work/data/dto/PlaceDto.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.data.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PlaceDto(
|
||||
@SerialName("id")
|
||||
val id: String?,
|
||||
@SerialName("place")
|
||||
val place: String?,
|
||||
)
|
||||
15
app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt
Normal file
15
app/src/main/java/ru/myitschool/work/data/dto/UserDto.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package ru.myitschool.work.data.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserDto(
|
||||
@SerialName("name")
|
||||
val name: String?,
|
||||
@SerialName("photoUrl")
|
||||
val photoUrl: String?,
|
||||
@SerialName("booking")
|
||||
val booking: Map<String, PlaceDto>?
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import ru.myitschool.work.App
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
|
||||
object AuthRepository {
|
||||
private const val STORE = "AUTH-STORE"
|
||||
private const val CODE_KEY = "CODE"
|
||||
|
||||
private var codeCache: String? = null
|
||||
|
||||
suspend fun checkAndSave(text: String): Result<Boolean> {
|
||||
return NetworkDataSource.checkAuth(text).onSuccess { success ->
|
||||
if (success) {
|
||||
codeCache = text
|
||||
App.context.userDataStore.edit { preferences ->
|
||||
val prefKey = stringPreferencesKey(CODE_KEY)
|
||||
preferences[prefKey] = text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCode(): String? {
|
||||
if (codeCache == null) {
|
||||
codeCache = App.context.userDataStore.data
|
||||
.firstOrNull()
|
||||
?.let { preferences ->
|
||||
preferences[stringPreferencesKey(CODE_KEY)]
|
||||
}
|
||||
}
|
||||
return codeCache
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
codeCache = null
|
||||
App.context.userDataStore.edit { preferences ->
|
||||
val prefKey = stringPreferencesKey(CODE_KEY)
|
||||
preferences.remove(prefKey)
|
||||
}
|
||||
}
|
||||
|
||||
private val Context.userDataStore: DataStore<Preferences> by preferencesDataStore(name = STORE)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package ru.myitschool.work.data.repo
|
||||
|
||||
import ru.myitschool.work.data.dto.BookRequestDto
|
||||
import ru.myitschool.work.data.source.NetworkDataSource
|
||||
import ru.myitschool.work.domain.book.entities.BookRequestData
|
||||
import ru.myitschool.work.domain.book.entities.BookingData
|
||||
import ru.myitschool.work.domain.main.entities.MainInfoEntity
|
||||
|
||||
class BookRepository(
|
||||
private val authRepository: AuthRepository
|
||||
) {
|
||||
suspend fun getInfo(): Result<MainInfoEntity> {
|
||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getInfo(code).mapCatching { dto ->
|
||||
MainInfoEntity(
|
||||
name = dto.name ?: error("Name is null"),
|
||||
photoUrl = dto.photoUrl ?: error("Photo url is null"),
|
||||
book = dto.booking?.mapNotNull { (date, place) ->
|
||||
MainInfoEntity.Book(
|
||||
date = date,
|
||||
place = place.place ?: return@mapNotNull null
|
||||
)
|
||||
} ?: listOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBookingInfo(): Result<List<BookingData>> {
|
||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
||||
return NetworkDataSource.getBooking(code).mapCatching { dto ->
|
||||
dto?.map { (date, places) ->
|
||||
BookingData(
|
||||
date = date,
|
||||
places = places.mapNotNull { place ->
|
||||
BookingData.Place(
|
||||
id = place.id ?: return@mapNotNull null,
|
||||
name = place.place ?: return@mapNotNull null
|
||||
)
|
||||
}
|
||||
)
|
||||
} ?: error("map is null")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendBook(data: BookRequestData): Result<Boolean> {
|
||||
val code = authRepository.getCode() ?: return getNoAuthResult()
|
||||
val dto = BookRequestDto(data.date, data.placeId)
|
||||
return NetworkDataSource.addBook(code, dto)
|
||||
}
|
||||
private fun <T> getNoAuthResult() = Result.failure<T>(
|
||||
IllegalStateException("No auth")
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package ru.myitschool.work.data.source
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import ru.myitschool.work.core.Constants
|
||||
import ru.myitschool.work.data.dto.PlaceDto
|
||||
import ru.myitschool.work.data.dto.BookRequestDto
|
||||
import ru.myitschool.work.data.dto.UserDto
|
||||
|
||||
object NetworkDataSource {
|
||||
private val client by lazy {
|
||||
HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkAuth(code: String): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl(code, Constants.AUTH_URL))
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getInfo(code: String): Result<UserDto> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
println("!!!!!!!!!!!!!! getInfo $code")
|
||||
val response = client.get(getUrl(code, Constants.INFO_URL))
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
println("!!!!!!!!!!!!!! getInfo OK ${response.bodyAsText()}")
|
||||
response.body<UserDto>()
|
||||
} else {
|
||||
println("!!!!!!!!!!!!!! getInfo ERROR ${response.bodyAsText()}")
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBooking(code: String): Result<Map<String, List<PlaceDto>>?> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.get(getUrl(code, Constants.BOOKING_URL))
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
response.body<Map<String, List<PlaceDto>>>()
|
||||
} else {
|
||||
error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addBook(code: String, data: BookRequestDto): Result<Boolean> = withContext(Dispatchers.IO) {
|
||||
return@withContext runCatching {
|
||||
val response = client.post(getUrl(code, Constants.BOOK_URL)) {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(data)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.Created -> true
|
||||
HttpStatusCode.Conflict -> false
|
||||
else -> error(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUrl(code: String, targetUrl: String) = "${Constants.HOST}/api/$code$targetUrl"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class CheckAndSaveAuthCodeUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
text: String
|
||||
): Result<Unit> {
|
||||
return repository.checkAndSave(text).mapCatching { success ->
|
||||
if (!success) error("Code is incorrect")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
class CheckCodeFormatUseCase {
|
||||
operator fun invoke(
|
||||
text: String
|
||||
): Boolean {
|
||||
return text.length == 4 && text.all { char ->
|
||||
char.isLetterOrDigit() &&
|
||||
((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char.isDigit())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class GetCodeUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke(): String? {
|
||||
return repository.getCode()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.auth
|
||||
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
|
||||
class LogoutUseCase(
|
||||
private val repository: AuthRepository
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
repository.logout()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ru.myitschool.work.domain.book
|
||||
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
import ru.myitschool.work.domain.book.entities.BookingData
|
||||
import java.time.LocalDate
|
||||
|
||||
class GetBookingDataUseCase(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Result<List<BookingData>> {
|
||||
return repository.getBookingInfo().map { data ->
|
||||
data
|
||||
.sortedBy { book ->
|
||||
LocalDate.parse(book.date)
|
||||
}
|
||||
.filter { it.places.isNotEmpty() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package ru.myitschool.work.domain.book
|
||||
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
import ru.myitschool.work.domain.book.entities.BookRequestData
|
||||
|
||||
class SendBookRequestUseCase(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
suspend operator fun invoke(data: BookRequestData): Result<Unit> {
|
||||
return repository.sendBook(data).mapCatching { success ->
|
||||
if (!success) error("Book error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.domain.book.entities
|
||||
|
||||
data class BookRequestData(
|
||||
val date: String,
|
||||
val placeId: String
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.domain.book.entities
|
||||
|
||||
data class BookingData(
|
||||
val date: String,
|
||||
val places: List<Place>
|
||||
) {
|
||||
data class Place(
|
||||
val id: String,
|
||||
val name: String
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package ru.myitschool.work.domain.main
|
||||
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
import ru.myitschool.work.domain.main.entities.MainInfoEntity
|
||||
import java.time.LocalDate
|
||||
|
||||
class GetMainDataUseCase(
|
||||
private val repository: BookRepository
|
||||
) {
|
||||
suspend operator fun invoke(): Result<MainInfoEntity> {
|
||||
return repository.getInfo().map { main ->
|
||||
main.copy(
|
||||
book = main.book.sortedBy { book ->
|
||||
LocalDate.parse(book.date)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.myitschool.work.domain.main.entities
|
||||
|
||||
data class MainInfoEntity(
|
||||
val name: String,
|
||||
val photoUrl: String,
|
||||
val book: List<Book>
|
||||
) {
|
||||
data class Book(
|
||||
val date: String,
|
||||
val place: String,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
sealed interface AppDestination
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object AuthScreenDestination: AppDestination
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object BookScreenDestination: AppDestination
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.nav
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object MainScreenDestination: AppDestination
|
||||
30
app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt
Normal file
30
app/src/main/java/ru/myitschool/work/ui/root/RootActivity.kt
Normal file
@@ -0,0 +1,30 @@
|
||||
package ru.myitschool.work.ui.root
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.ui.Modifier
|
||||
import ru.myitschool.work.ui.screen.AppNavHost
|
||||
import ru.myitschool.work.ui.theme.WorkTheme
|
||||
|
||||
class RootActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
WorkTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
AppNavHost(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package ru.myitschool.work.ui.screen
|
||||
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import kotlinx.coroutines.delay
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.GetCodeUseCase
|
||||
import ru.myitschool.work.ui.nav.AppDestination
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
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
|
||||
fun AppNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController = rememberNavController()
|
||||
) {
|
||||
var destination by remember { mutableStateOf<AppDestination?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
val code = GetCodeUseCase(AuthRepository).invoke()
|
||||
destination = if (code == null) {
|
||||
AuthScreenDestination
|
||||
} else {
|
||||
MainScreenDestination
|
||||
}
|
||||
}
|
||||
if (destination != null) {
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
navController = navController,
|
||||
startDestination = destination as AppDestination,
|
||||
) {
|
||||
composable<AuthScreenDestination> {
|
||||
AuthScreen(navController = navController)
|
||||
}
|
||||
composable<MainScreenDestination> {
|
||||
MainScreen(navController = navController)
|
||||
}
|
||||
composable<BookScreenDestination> {
|
||||
BookScreen(navController = navController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import ru.myitschool.work.ui.nav.AppDestination
|
||||
|
||||
sealed interface AuthAction {
|
||||
class Open(val destination: AppDestination): AuthAction
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
sealed interface AuthIntent {
|
||||
data class Send(val text: String): AuthIntent
|
||||
data class TextInput(val text: String): AuthIntent
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun AuthScreen(
|
||||
viewModel: AuthViewModel = viewModel(),
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.actionFlow.collect { action ->
|
||||
when (action) {
|
||||
is AuthAction.Open -> navController.navigate(action.destination) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.auth_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
when (val currentState = state) {
|
||||
is AuthState.Data -> Content(viewModel, currentState)
|
||||
is AuthState.Loading -> {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
viewModel: AuthViewModel,
|
||||
state: AuthState.Data
|
||||
) {
|
||||
var inputText by remember { mutableStateOf("") }
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
TextField(
|
||||
modifier = Modifier.testTag(TestIds.Auth.CODE_INPUT).fillMaxWidth(),
|
||||
value = inputText,
|
||||
onValueChange = {
|
||||
inputText = it
|
||||
viewModel.onIntent(AuthIntent.TextInput(it))
|
||||
},
|
||||
label = { Text(stringResource(R.string.auth_label)) }
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Auth.SIGN_BUTTON).fillMaxWidth(),
|
||||
onClick = {
|
||||
viewModel.onIntent(AuthIntent.Send(inputText))
|
||||
},
|
||||
enabled = state.isEnabledSend
|
||||
) {
|
||||
Text(stringResource(R.string.auth_sign_in))
|
||||
}
|
||||
if (state.error != null) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Auth.ERROR),
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Red,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
sealed interface AuthState {
|
||||
object Loading: AuthState
|
||||
data class Data(
|
||||
val isEnabledSend: Boolean,
|
||||
val error: String?
|
||||
): AuthState
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package ru.myitschool.work.ui.screen.auth
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.domain.auth.CheckAndSaveAuthCodeUseCase
|
||||
import ru.myitschool.work.domain.auth.CheckCodeFormatUseCase
|
||||
import ru.myitschool.work.ui.nav.MainScreenDestination
|
||||
|
||||
class AuthViewModel : ViewModel() {
|
||||
private val checkCodeFormatUseCase by lazy { CheckCodeFormatUseCase() }
|
||||
private val checkAndSaveAuthCodeUseCase by lazy { CheckAndSaveAuthCodeUseCase(AuthRepository) }
|
||||
private val _uiState = MutableStateFlow<AuthState>(
|
||||
AuthState.Data(
|
||||
isEnabledSend = false,
|
||||
error = null
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
|
||||
private val _actionFlow: MutableSharedFlow<AuthAction> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<AuthAction> = _actionFlow
|
||||
|
||||
fun onIntent(intent: AuthIntent) {
|
||||
when (intent) {
|
||||
is AuthIntent.Send -> {
|
||||
viewModelScope.launch {
|
||||
checkAndSaveAuthCodeUseCase.invoke(intent.text).fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(AuthAction.Open(MainScreenDestination))
|
||||
},
|
||||
onFailure = { error ->
|
||||
updateStateIfData { oldState ->
|
||||
oldState.copy(
|
||||
error = error.message
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
is AuthIntent.TextInput -> {
|
||||
updateStateIfData { oldState ->
|
||||
oldState.copy(
|
||||
isEnabledSend = checkCodeFormatUseCase.invoke(intent.text),
|
||||
error = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStateIfData(lambda: (AuthState.Data) -> AuthState) {
|
||||
_uiState.update { state ->
|
||||
(state as? AuthState.Data)?.let { lambda.invoke(it) } ?: state
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
sealed interface BookAction {
|
||||
object Back: BookAction
|
||||
object BackWithSuccess: BookAction
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
sealed interface BookIntent {
|
||||
data object Refresh: BookIntent
|
||||
data class Add(
|
||||
val date: String,
|
||||
val placeId: String
|
||||
): BookIntent
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.ExitTransition
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Tab
|
||||
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.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.toRoute
|
||||
import kotlinx.serialization.Serializable
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds
|
||||
import ru.myitschool.work.ui.screen.main.MainResult
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Composable
|
||||
fun BookScreen(
|
||||
viewModel: BookViewModel = viewModel(),
|
||||
navController: NavController
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.actionFlow.collect { action ->
|
||||
when (action) {
|
||||
is BookAction.Back -> navController.popBackStack()
|
||||
is BookAction.BackWithSuccess -> {
|
||||
navController.previousBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.set(MainResult.REFRESH_KEY, true)
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
navController.popBackStack()
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_back),
|
||||
contentDescription = stringResource(R.string.book_back)
|
||||
)
|
||||
}
|
||||
|
||||
when (val currentState = state) {
|
||||
is BookState.Data -> ContentState(viewModel, currentState)
|
||||
is BookState.Error -> ErrorState(viewModel, currentState)
|
||||
is BookState.Loading -> LoadingState()
|
||||
is BookState.Empty -> EmptyState()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingState() {
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyState() {
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Book.EMPTY),
|
||||
text = stringResource(R.string.book_empty),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorState(
|
||||
viewModel: BookViewModel,
|
||||
state: BookState.Error
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Book.ERROR),
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.Black,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON).fillMaxWidth(),
|
||||
onClick = {
|
||||
viewModel.onIntent(BookIntent.Refresh)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.main_refresh))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentState(
|
||||
viewModel: BookViewModel,
|
||||
state: BookState.Data
|
||||
) {
|
||||
val navController = rememberNavController()
|
||||
val startDestination = SelectedTabDestination(index = 0)
|
||||
var selectedDestination by rememberSaveable {
|
||||
mutableIntStateOf(startDestination.index)
|
||||
}
|
||||
var selectedPlaceId by rememberSaveable {
|
||||
mutableStateOf<String?>(null)
|
||||
}
|
||||
Box {
|
||||
Column {
|
||||
PrimaryTabRow(
|
||||
modifier = Modifier,
|
||||
selectedTabIndex = selectedDestination,
|
||||
) {
|
||||
state.items.forEachIndexed { index, destination ->
|
||||
Tab(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Book.getIdDateItemByPosition(index)),
|
||||
selected = selectedDestination == index,
|
||||
onClick = {
|
||||
navController.navigate(
|
||||
route = SelectedTabDestination(index = index)
|
||||
) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
selectedDestination = index
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Book.ITEM_DATE),
|
||||
text = LocalDate.parse(destination.date)
|
||||
.format(
|
||||
DateTimeFormatter.ofPattern(
|
||||
"dd.MM"
|
||||
)
|
||||
),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
TabNavHost(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
state = state,
|
||||
onPlaceSelected = { id ->
|
||||
selectedPlaceId = id
|
||||
}
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(all = 24.dp)
|
||||
.align(Alignment.BottomEnd),
|
||||
) {
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.testTag(TestIds.Book.BOOK_BUTTON),
|
||||
onClick = {
|
||||
val id = selectedPlaceId
|
||||
if (id != null) {
|
||||
viewModel.onIntent(
|
||||
BookIntent.Add(
|
||||
date = state.items[selectedDestination].date,
|
||||
placeId = id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_check),
|
||||
contentDescription = stringResource(R.string.book_add)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navController: NavHostController,
|
||||
startDestination: SelectedTabDestination,
|
||||
state: BookState.Data,
|
||||
onPlaceSelected: (String) -> Unit,
|
||||
) {
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
enterTransition = { EnterTransition.None },
|
||||
exitTransition = { ExitTransition.None },
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
) {
|
||||
composable<SelectedTabDestination> {
|
||||
val index = it.toRoute<SelectedTabDestination>().index
|
||||
val data = state.items[index]
|
||||
val (selectedOption, onOptionSelected) = remember {
|
||||
mutableStateOf(data.places[0].id)
|
||||
}
|
||||
onPlaceSelected.invoke(selectedOption)
|
||||
|
||||
Column(modifier.selectableGroup()) {
|
||||
data.places.forEachIndexed { index, place ->
|
||||
val isSelected = place.id == selectedOption
|
||||
Row(
|
||||
Modifier
|
||||
.testTag(TestIds.Book.getIdPlaceItemByPosition(index))
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.selectable(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
onPlaceSelected(place.id)
|
||||
onOptionSelected(place.id)
|
||||
},
|
||||
role = Role.RadioButton
|
||||
)
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
modifier = Modifier.testTag(TestIds.Book.ITEM_PLACE_SELECTOR),
|
||||
selected = isSelected,
|
||||
onClick = null
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Book.ITEM_PLACE_TEXT)
|
||||
.padding(start = 16.dp),
|
||||
text = place.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SelectedTabDestination(
|
||||
val index: Int
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed interface BookState {
|
||||
data object Loading : BookState
|
||||
|
||||
data object Empty : BookState
|
||||
data class Error(
|
||||
val error: String
|
||||
) : BookState
|
||||
|
||||
data class Data(
|
||||
val items: PersistentList<Item>
|
||||
) : BookState {
|
||||
|
||||
data class Item(
|
||||
val date: String,
|
||||
val places: PersistentList<Place>,
|
||||
)
|
||||
|
||||
data class Place(
|
||||
val id: String,
|
||||
val name: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package ru.myitschool.work.ui.screen.book
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
import ru.myitschool.work.domain.book.GetBookingDataUseCase
|
||||
import ru.myitschool.work.domain.book.SendBookRequestUseCase
|
||||
import ru.myitschool.work.domain.book.entities.BookRequestData
|
||||
import kotlin.getValue
|
||||
|
||||
class BookViewModel : ViewModel() {
|
||||
private val bookRepository by lazy { BookRepository(AuthRepository) }
|
||||
private val getBookingDataUseCase by lazy { GetBookingDataUseCase(bookRepository) }
|
||||
private val sendBookRequestUseCase by lazy { SendBookRequestUseCase(bookRepository) }
|
||||
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 {
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun onIntent(intent: BookIntent) {
|
||||
when (intent) {
|
||||
is BookIntent.Refresh -> {
|
||||
refresh()
|
||||
}
|
||||
|
||||
is BookIntent.Add -> {
|
||||
viewModelScope.launch {
|
||||
sendBookRequestUseCase.invoke(
|
||||
BookRequestData(
|
||||
date = intent.date,
|
||||
placeId = intent.placeId
|
||||
)
|
||||
).fold(
|
||||
onSuccess = {
|
||||
_actionFlow.emit(BookAction.BackWithSuccess)
|
||||
},
|
||||
onFailure = { error ->
|
||||
error.printStackTrace()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { BookState.Loading }
|
||||
_uiState.update {
|
||||
getBookingDataUseCase.invoke().fold(
|
||||
onSuccess = { data ->
|
||||
if (data.isEmpty()) {
|
||||
BookState.Empty
|
||||
} else {
|
||||
BookState.Data(
|
||||
items = data.map { item ->
|
||||
BookState.Data.Item(
|
||||
date = item.date,
|
||||
places = item.places.map { place ->
|
||||
BookState.Data.Place(
|
||||
id = place.id,
|
||||
name = place.name
|
||||
)
|
||||
}.toPersistentList()
|
||||
)
|
||||
}.toPersistentList()
|
||||
)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
BookState.Error(
|
||||
error = error.message.orEmpty()
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import ru.myitschool.work.ui.nav.AppDestination
|
||||
import ru.myitschool.work.ui.screen.book.BookIntent
|
||||
|
||||
sealed interface MainAction {
|
||||
class Open(
|
||||
val destination: AppDestination,
|
||||
val clearBackStack: Boolean = false
|
||||
): MainAction
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
sealed interface MainIntent {
|
||||
data object Refresh: MainIntent
|
||||
data object Logout: MainIntent
|
||||
data object Add: MainIntent
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
object MainResult {
|
||||
const val REFRESH_KEY = "refresh"
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.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.AsyncImage
|
||||
import coil3.request.ImageRequest
|
||||
import ru.myitschool.work.R
|
||||
import ru.myitschool.work.core.TestIds
|
||||
|
||||
@Composable
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = viewModel(),
|
||||
navController: NavController
|
||||
) {
|
||||
val isRefreshNeeded = navController.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.getStateFlow(MainResult.REFRESH_KEY, false)
|
||||
?.collectAsState()
|
||||
?.value
|
||||
?: false
|
||||
|
||||
LaunchedEffect(isRefreshNeeded) {
|
||||
if (isRefreshNeeded) {
|
||||
println("!!!!!!!! refresh after book")
|
||||
navController.currentBackStackEntry
|
||||
?.savedStateHandle
|
||||
?.remove<Boolean>(MainResult.REFRESH_KEY)
|
||||
viewModel.onIntent(MainIntent.Refresh)
|
||||
}
|
||||
}
|
||||
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.actionFlow.collect { action ->
|
||||
when (action) {
|
||||
is MainAction.Open -> {
|
||||
navController.navigate(action.destination) {
|
||||
if (action.clearBackStack) {
|
||||
popUpTo(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val currentState = state) {
|
||||
is MainState.Data -> ContentState(viewModel, currentState)
|
||||
is MainState.Error -> ErrorState(viewModel, currentState)
|
||||
is MainState.Loading -> LoadingState()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingState() {
|
||||
Box(
|
||||
Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ErrorState(
|
||||
viewModel: MainViewModel,
|
||||
state: MainState.Error
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ERROR),
|
||||
text = state.error,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.Black,
|
||||
)
|
||||
Spacer(modifier = Modifier.size(16.dp))
|
||||
Button(
|
||||
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON).fillMaxWidth(),
|
||||
onClick = {
|
||||
println("!!!!!!!! refresh on click error")
|
||||
viewModel.onIntent(MainIntent.Refresh)
|
||||
},
|
||||
) {
|
||||
Text(stringResource(R.string.main_refresh))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentState(
|
||||
viewModel: MainViewModel,
|
||||
state: MainState.Data
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(all = 24.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Main.PROFILE_IMAGE)
|
||||
.size(64.dp)
|
||||
.clip(CircleShape),
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(state.photoUrl)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Main.PROFILE_NAME)
|
||||
.padding(horizontal = 4.dp),
|
||||
text = state.name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.Black,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
println("!!!!!!!! refresh on click main")
|
||||
viewModel.onIntent(MainIntent.Refresh)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
contentDescription = stringResource(R.string.main_refresh)
|
||||
)
|
||||
}
|
||||
IconButton(
|
||||
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = {
|
||||
viewModel.onIntent(MainIntent.Logout)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_logout),
|
||||
contentDescription = stringResource(R.string.main_logout)
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn {
|
||||
itemsIndexed(state.books) { index, book ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.testTag(TestIds.Main.getIdItemByPosition(index))
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
|
||||
text = book.place,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Black,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
|
||||
text = book.date,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON).align(Alignment.BottomEnd),
|
||||
onClick = {
|
||||
viewModel.onIntent(MainIntent.Add)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_add),
|
||||
contentDescription = stringResource(R.string.book_add)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
|
||||
sealed interface MainState {
|
||||
data object Loading: MainState
|
||||
data class Error(
|
||||
val error: String
|
||||
): MainState
|
||||
data class Data(
|
||||
val name: String,
|
||||
val photoUrl: String,
|
||||
val books: PersistentList<Book>
|
||||
): MainState {
|
||||
data class Book(
|
||||
val date: String,
|
||||
val place: String,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ru.myitschool.work.ui.screen.main
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.myitschool.work.data.repo.AuthRepository
|
||||
import ru.myitschool.work.data.repo.BookRepository
|
||||
import ru.myitschool.work.domain.auth.LogoutUseCase
|
||||
import ru.myitschool.work.domain.main.GetMainDataUseCase
|
||||
import ru.myitschool.work.ui.nav.AuthScreenDestination
|
||||
import ru.myitschool.work.ui.nav.BookScreenDestination
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.getValue
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
private val getMainDataUseCase by lazy {
|
||||
GetMainDataUseCase(BookRepository(AuthRepository))
|
||||
}
|
||||
private val logoutUseCase by lazy {
|
||||
LogoutUseCase(AuthRepository)
|
||||
}
|
||||
private val _uiState = MutableStateFlow<MainState>(MainState.Loading)
|
||||
val uiState: StateFlow<MainState> = _uiState.asStateFlow()
|
||||
|
||||
private val _actionFlow: MutableSharedFlow<MainAction> = MutableSharedFlow()
|
||||
val actionFlow: SharedFlow<MainAction> = _actionFlow
|
||||
|
||||
init {
|
||||
println("!!!!!!!! refresh init")
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun onIntent(intent: MainIntent) {
|
||||
when (intent) {
|
||||
is MainIntent.Add -> {
|
||||
viewModelScope.launch {
|
||||
_actionFlow.emit(MainAction.Open(BookScreenDestination))
|
||||
}
|
||||
}
|
||||
is MainIntent.Refresh -> {
|
||||
refresh()
|
||||
}
|
||||
is MainIntent.Logout -> {
|
||||
viewModelScope.launch {
|
||||
logoutUseCase.invoke()
|
||||
_actionFlow.emit(MainAction.Open(AuthScreenDestination, true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
println("!!!!!!!! refresh")
|
||||
viewModelScope.launch {
|
||||
_uiState.update { MainState.Loading }
|
||||
_uiState.update {
|
||||
getMainDataUseCase.invoke().fold(
|
||||
onSuccess = { data ->
|
||||
MainState.Data(
|
||||
name = data.name,
|
||||
photoUrl = data.photoUrl,
|
||||
books = data.book.map { book ->
|
||||
MainState.Data.Book(
|
||||
date = LocalDate
|
||||
.parse(book.date)
|
||||
.format(
|
||||
DateTimeFormatter.ofPattern(DATE_FORMAT)
|
||||
),
|
||||
place = book.place
|
||||
)
|
||||
}.toPersistentList()
|
||||
)
|
||||
},
|
||||
onFailure = { error ->
|
||||
MainState.Error(
|
||||
error = error.message?.takeIf { it.isNotBlank() } ?: "Unknown error"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DATE_FORMAT = "dd.MM.yyyy"
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/ru/myitschool/work/ui/theme/Color.kt
Normal file
11
app/src/main/java/ru/myitschool/work/ui/theme/Color.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package ru.myitschool.work.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
57
app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt
Normal file
57
app/src/main/java/ru/myitschool/work/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
package ru.myitschool.work.ui.theme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun WorkTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
34
app/src/main/java/ru/myitschool/work/ui/theme/Type.kt
Normal file
34
app/src/main/java/ru/myitschool/work/ui/theme/Type.kt
Normal file
@@ -0,0 +1,34 @@
|
||||
package ru.myitschool.work.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
5
app/src/main/res/drawable/ic_add.xml
Normal file
5
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_back.xml
Normal file
5
app/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_check.xml
Normal file
5
app/src/main/res/drawable/ic_check.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_logout.xml
Normal file
5
app/src/main/res/drawable/ic_logout.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
5
app/src/main/res/drawable/ic_refresh.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
|
||||
|
||||
</vector>
|
||||
10
app/src/main/res/values/colors.xml
Normal file
10
app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="purple_200">#FFBB86FC</color>
|
||||
<color name="purple_500">#FF6200EE</color>
|
||||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
||||
15
app/src/main/res/values/strings.xml
Normal file
15
app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<resources>
|
||||
<string name="app_name">Work</string>
|
||||
<string name="title_activity_root">RootActivity</string>
|
||||
<string name="auth_title">Привет! Введи код для авторизации</string>
|
||||
<string name="auth_label">Код</string>
|
||||
<string name="auth_sign_in">Войти</string>
|
||||
|
||||
<string name="main_refresh">Обновить</string>
|
||||
<string name="main_logout">Выйти</string>
|
||||
<string name="main_add">Добавить</string>
|
||||
|
||||
<string name="book_add">Забронировать</string>
|
||||
<string name="book_back">Назад</string>
|
||||
<string name="book_empty">Всё забронировано</string>
|
||||
</resources>
|
||||
1
testLib
1
testLib
Submodule testLib deleted from b6291e4770
Reference in New Issue
Block a user