Initial commit

This commit is contained in:
2025-10-29 09:29:38 +03:00
parent 7c829437db
commit fd0f2f0af1
27 changed files with 879 additions and 22 deletions

View File

@@ -1,5 +1,7 @@
plugins {
composeCompiler
kotlinAndroid
kotlinSerialization version Version.Kotlin.language
androidApplication
}
@@ -32,6 +34,14 @@ android {
}
dependencies {
defaultLibrary()
defaultComposeLibrary()
val ktor = "3.3.1"
implementation("io.ktor:ktor-client-core:$ktor")
implementation("io.ktor:ktor-client-cio:$ktor")
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")
}

View File

@@ -0,0 +1,229 @@
@file:OptIn(ExperimentalTestApi::class)
package ru.myitschool.work
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.createAndroidComposeRule
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.MainScreen
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 java.nio.charset.Charset
@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 роверкаонтентаа_экране() = runWithInit(1) {
serverRule.mockResponses(
"/user" to Response(assetFile = "profile.json"),
"/user" to Response(assetFile = "profile2.json")
)
onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") {
loadButton {
assertIsDisplayed()
performClick()
}
}
step("Проверяем корректное заполнение контента") {
nameText {
assertIsDisplayed()
assertTextEquals("Test Testovich")
}
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 1")
timeText.assertTextEquals("end row 1")
}
childAt<MainScreen.ListItem>(1) {
roomText.assertTextEquals("row 2")
timeText.assertTextEquals("end row 2")
}
}
}
step("Обновляем страницу") {
loadButton {
assertIsDisplayed()
performClick()
}
}
step("Проверяем корректное заполнение контента") {
nameText {
assertIsDisplayed()
assertTextEquals("Ivan Ivanov")
}
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 3")
timeText.assertTextEquals("end row 3")
}
}
}
}
}
@Test
fun роверкаобавленияанных() = runWithInit(1) {
val postResponseRoom = "Content-Disposition: form-data; name=room\nContent-Length: 8\n\nRoomTest"
val postResponseTime = "Content-Disposition: form-data; name=time\nContent-Length: 8\n\nTimeTest"
var requestComplete = false
serverRule.mockResponses(
"/user" to Response(assetFile = "profile.json"),
"/book" to Response(statusCode = 200),
"/user" to Response(assetFile = "profile2.json"),
)
serverRule.setRecorderListener { request ->
if (request.path == "/book") {
val text = request.body.readString(Charset.defaultCharset())
.replace("\r", "")
println(text)
assert(text.contains(postResponseRoom)) { "Значение Room не найдено" }
assert(text.contains(postResponseTime)) { "Значение Time не найдено" }
requestComplete = true
}
}
onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") {
loadButton {
assertIsDisplayed()
performClick()
}
}
step("Проверяем корректное заполнение контента") {
nameText {
assertIsDisplayed()
assertTextEquals("Test Testovich")
}
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 1")
timeText.assertTextEquals("end row 1")
}
childAt<MainScreen.ListItem>(1) {
roomText.assertTextEquals("row 2")
timeText.assertTextEquals("end row 2")
}
}
}
step("Заполняем поля для ввода") {
roomInput {
assertIsDisplayed()
performTextInput("RoomTest")
}
timeInput {
assertIsDisplayed()
performTextInput("TimeTest")
}
addButton {
assertIsDisplayed()
performClick()
}
}
flakySafely(timeoutMs = 2_000, intervalMs = 100) {
assert(requestComplete) { "/book запроса не было" }
}
step("Проверяем обработку результата") {
nameText {
assertIsDisplayed()
assertTextEquals("Ivan Ivanov")
}
}
}
}
@Test
fun роверка_обработки_ошибок() = runWithInit(1) {
serverRule.mockResponses(
"/user" to Response(assetFile = "error.json", statusCode = 400),
"/user" to Response(assetFile = "error2.json", statusCode = 500),
"/user" to Response(assetFile = "profile.json"),
"/book" to Response(assetFile = "error.json", statusCode = 400)
)
onComposeScreen<MainScreen>(composeTestRule) {
step("Нажимаем на кнопку загрузки данных") {
loadButton {
assertIsDisplayed()
performClick()
}
}
step("Проверяем ошибку и повторно выполняем запрос") {
errorContent {
assertTextEquals("TEST Error 1 TEST")
}
loadButton {
assertIsDisplayed()
performClick()
}
errorContent {
assertTextEquals("TEST Error 2 TEST")
}
loadButton {
assertIsDisplayed()
performClick()
}
}
step("Проверяем корректное заполнение контента") {
nameText {
assertIsDisplayed()
assertTextEquals("Test Testovich")
}
list {
firstChild<MainScreen.ListItem> {
roomText.assertTextEquals("row 1")
timeText.assertTextEquals("end row 1")
}
childAt<MainScreen.ListItem>(1) {
roomText.assertTextEquals("row 2")
timeText.assertTextEquals("end row 2")
}
}
}
step("Заполняем поля для ввода") {
roomInput {
assertIsDisplayed()
performTextInput("RoomTest")
}
timeInput {
assertIsDisplayed()
performTextInput("TimeTest")
}
addButton {
assertIsDisplayed()
performClick()
}
}
step("Проверяем обработку ошибки") {
errorSend {
assertTextEquals("TEST Error 1 TEST")
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
package ru.myitschool.work.screens
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.hasTestTag
import com.kaspersky.components.composesupport.core.KNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListItemNode
import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode
class MainScreen(
semanticsProvider: SemanticsNodeInteractionsProvider
) : UiBaseScreen<MainScreen>(semanticsProvider) {
val loadButton = child<KNode> {
hasTestTag("load_button")
}
val nameText: KNode = child<KNode> {
hasTestTag("name")
}
val roomInput: KNode = child<KNode> {
hasTestTag("input_room")
}
val timeInput: KNode = child<KNode> {
hasTestTag("input_time")
}
val addButton: KNode = child<KNode> {
hasTestTag("add")
}
val errorContent: KNode = child<KNode> {
hasTestTag("error_text_content")
}
val errorSend: KNode = child<KNode> {
hasTestTag("error_text_send")
}
val list = KLazyListNode(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag("booking") },
itemTypeBuilder = {
itemType(::ListItem)
},
positionMatcher = { position -> hasTestTag("position=$position") }
)
class ListItem(
semanticsNode: SemanticsNode,
semanticsProvider: SemanticsNodeInteractionsProvider,
) : KLazyListItemNode<ListItem>(semanticsNode, semanticsProvider) {
val roomText: KNode = child {
hasTestTag("room")
}
val timeText: KNode = child {
hasTestTag("time")
}
}
}

View File

@@ -0,0 +1,10 @@
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
)

View File

@@ -0,0 +1,27 @@
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)
)
}
}
}

View File

@@ -0,0 +1,86 @@
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,
)

View File

@@ -0,0 +1,3 @@
{
"error": "TEST Error 1 TEST"
}

View File

@@ -0,0 +1,3 @@
{
"error": "TEST Error 2 TEST"
}

View File

@@ -0,0 +1,13 @@
{
"name": "Test Testovich",
"booking": [
{
"room": "row 1",
"time": "end row 1"
},
{
"room": "row 2",
"time": "end row 2"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"name": "Ivan Ivanov",
"booking": [
{
"room": "row 3",
"time": "end row 3"
}
]
}

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -10,7 +12,19 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Default"
tools:targetApi="31" />
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".ui.root.RootActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"
android:label="@string/title_activity_root">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.core
object Constants {
const val HOST = "http://localhost:8090"
const val USER_URL = "/user"
const val FULL_USER_URL = "$HOST$USER_URL"
const val BOOK_URL = "/book"
const val FULL_BOOK_URL = "$HOST$BOOK_URL"
}

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.domain.entities
data class BookingEntity(
val roomName: String,
val time: String,
)

View File

@@ -0,0 +1,6 @@
package ru.myitschool.work.domain.entities
data class UserEntity(
val name: String,
val bookingList: List<BookingEntity>
)

View File

@@ -0,0 +1,198 @@
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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
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.platform.testTag
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
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 ->
Screen(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Screen(
modifier: Modifier = Modifier,
viewModel: RootViewModel = viewModel()
) {
val state by viewModel.uiState.collectAsState()
when (val currentState = state) {
is RootState.Content -> {
Column(
modifier = modifier,
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.testTag("name").weight(1f),
text = currentState.userEntity.name,
style = MaterialTheme.typography.headlineSmall
)
ButtonGetData(viewModel)
}
LazyColumn(
modifier = Modifier.testTag("booking").weight(1f)
) {
itemsIndexed(currentState.userEntity.bookingList) { index, book ->
Row(
modifier = Modifier
.testTag("position=$index")
.padding(vertical = 16.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.testTag("room").weight(1f),
text = book.roomName,
style = MaterialTheme.typography.bodyLarge
)
Text(
modifier = Modifier.testTag("time"),
text = book.time,
style = MaterialTheme.typography.bodySmall
)
}
}
}
Row(
modifier = Modifier.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
var roomText by remember { mutableStateOf("") }
var timeText by remember { mutableStateOf("") }
Column(modifier = Modifier.weight(1f)) {
TextField(
modifier = Modifier.testTag("input_room").fillMaxWidth(),
value = roomText,
onValueChange = { roomText = it },
label = { Text("Room") }
)
TextField(
modifier = Modifier
.testTag("input_time")
.padding(top = 8.dp)
.fillMaxWidth(),
value = timeText,
onValueChange = { timeText = it },
label = { Text("Time") }
)
}
Column(
modifier = Modifier.padding(start = 8.dp),
) {
Button(
modifier = Modifier.testTag("add"),
onClick = {
viewModel.onIntent(
RootIntent.AddBook(room = roomText, time = timeText)
)
roomText = ""
timeText = ""
}
) {
Text(text = "Add")
}
if (currentState.errorText != null) {
Text(
modifier = Modifier.testTag("error_text_send"),
text = currentState.errorText
)
}
}
}
}
}
is RootState.Error -> {
Column(
modifier = modifier,
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.testTag("error_text_content"),
text = currentState.message
)
ButtonGetData(viewModel)
}
}
is RootState.Loading -> {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(modifier = Modifier.size(64.dp))
}
}
is RootState.NotLoaded -> {
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
ButtonGetData(viewModel)
}
}
}
}
@Composable
private fun ButtonGetData(
viewModel: RootViewModel
) {
Button(
modifier = Modifier.testTag("load_button"),
onClick = { viewModel.onIntent(RootIntent.LoadData) }
) {
Text(
text = "Get load"
)
}
}

View File

@@ -0,0 +1,9 @@
package ru.myitschool.work.ui.root
sealed interface RootIntent {
data object LoadData: RootIntent
data class AddBook(
val room: String,
val time: String
): RootIntent
}

View File

@@ -0,0 +1,13 @@
package ru.myitschool.work.ui.root
import ru.myitschool.work.domain.entities.UserEntity
sealed interface RootState {
data object NotLoaded: RootState
data object Loading: RootState
data class Error(val message: String): RootState
data class Content(
val userEntity: UserEntity,
val errorText: String?,
): RootState
}

View File

@@ -0,0 +1,68 @@
package ru.myitschool.work.ui.root
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.myitschool.work.data.AppRepository
import ru.myitschool.work.domain.AddBookUseCase
import ru.myitschool.work.domain.GetUserDataUseCase
class RootViewModel : ViewModel() {
private val getUserDataUseCase by lazy {
GetUserDataUseCase(
repository = AppRepository
)
}
private val addBookUseCase by lazy {
AddBookUseCase(
repository = AppRepository
)
}
private val _uiState = MutableStateFlow<RootState>(RootState.NotLoaded)
val uiState: StateFlow<RootState> = _uiState.asStateFlow()
fun onIntent(intent: RootIntent) {
when (intent) {
is RootIntent.LoadData -> loadData()
is RootIntent.AddBook -> addBook(intent)
}
}
private fun loadData() {
viewModelScope.launch {
_uiState.emit(RootState.Loading)
getUserDataUseCase.invoke().fold(
onSuccess = { value ->
_uiState.emit(RootState.Content(userEntity = value, errorText = null))
},
onFailure = { error ->
_uiState.emit(RootState.Error(error.message.orEmpty()))
}
)
}
}
private fun addBook(intent: RootIntent.AddBook) {
viewModelScope.launch {
addBookUseCase.invoke(
room = intent.room,
time = intent.time
).fold(
onSuccess = {
loadData()
},
onFailure = { error ->
_uiState.update { state ->
(state as? RootState.Content)?.copy(
errorText = error.message
) ?: state
}
}
)
}
}
}

View 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)

View 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
)
}

View 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
)
*/
)

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name">Work</string>
<string name="title_activity_root">NTO-2025</string>
</resources>

View File

@@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Default" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>