Compare commits

...

2 Commits

Author SHA1 Message Date
40a8428a19 Merge remote-tracking branch 'origin/main' 2025-12-05 05:01:00 +07:00
9e603f87e6 UI/UX solution 2025-12-05 05:00:33 +07:00
14 changed files with 331 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
# НТО 2025. II отборочный этап. Командные задания — Android
# НТО 2025. II отборочный этап. Командные задания — Android
## 📖 Предыстория

View File

@@ -1,4 +1,4 @@
package ru.myitschool.work.data.repo
package ru.myitschool.work.data.repo
import android.content.Context
import androidx.datastore.preferences.preferencesDataStore

View File

@@ -15,6 +15,7 @@ class RootActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
actionBar?.hide()
setContent {
WorkTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->

View File

@@ -2,6 +2,7 @@ package ru.myitschool.work.ui.screen.auth
import androidx.compose.foundation.layout.Arrangement
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
@@ -9,7 +10,9 @@ 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.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
@@ -20,11 +23,14 @@ 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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import io.ktor.websocket.Frame
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.MainScreenDestination
@@ -50,8 +56,20 @@ fun AuthScreen(
.fillMaxSize()
.padding(all = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
verticalArrangement = Arrangement.Top
) {
Spacer(modifier = Modifier.size(48.dp))
Text(
text = stringResource(R.string.auth_name),
style = MaterialTheme.typography.displaySmall.copy(
fontWeight = FontWeight.Bold
),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(48.dp))
Text(
text = stringResource(R.string.auth_title),
style = MaterialTheme.typography.headlineSmall,
@@ -60,7 +78,7 @@ fun AuthScreen(
when (val currentState = state) {
is AuthState.Data -> Content(viewModel, currentState)
is AuthState.Loading -> {
Spacer(modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.size(24.dp))
CircularProgressIndicator(
modifier = Modifier.size(64.dp)
)
@@ -76,28 +94,31 @@ private fun Content(
) {
Spacer(modifier = Modifier.size(16.dp))
OutlinedTextField(
modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(),
value = state.code,
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
label = { Text(stringResource(R.string.auth_label)) },
placeholder = { Text("Введите код сотрудника") }
)
Spacer(modifier = Modifier.size(8.dp))
if (state.isErrorVisible) {
Text(
modifier = Modifier
.fillMaxWidth()
.testTag(TestIds.Auth.ERROR),
text = "Неверный код или ошибка сервера",
color = Color.Red,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.size(8.dp))
}
TextField(
modifier = Modifier
.testTag(TestIds.Auth.CODE_INPUT)
.fillMaxWidth(),
value = state.code,
onValueChange = { viewModel.onIntent(AuthIntent.TextInput(it)) },
label = { Text(stringResource(R.string.auth_label)) }
)
Spacer(modifier = Modifier.size(16.dp))
Button(
@@ -109,6 +130,16 @@ private fun Content(
},
enabled = state.isButtonEnabled
) {
Text(stringResource(R.string.auth_sign_in))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.auth_sign_in))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_arrow_right),
contentDescription = "Войти"
)
}
}
}

View File

@@ -16,6 +16,7 @@ import androidx.compose.foundation.selection.selectable
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Surface
@@ -27,9 +28,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
@Composable
@@ -112,14 +116,34 @@ private fun BookErrorContent(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.main_screen_refresh))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_refresh),
contentDescription = "Обновить"
)
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_back),
contentDescription = "Назад"
)
Spacer(modifier = Modifier.size(8.dp))
Text(text = stringResource(R.string.book_screen_back))
}
}
}
}
@@ -145,14 +169,34 @@ private fun BookEmptyContent(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.main_screen_refresh))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_refresh),
contentDescription = "Обновить"
)
}
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_back),
contentDescription = "Назад"
)
Spacer(modifier = Modifier.size(8.dp))
Text(text = stringResource(R.string.book_screen_back))
}
}
}
}
@@ -179,19 +223,39 @@ private fun BookDataContent(
modifier = Modifier.testTag(TestIds.Book.BACK_BUTTON),
onClick = onBack
) {
Text("Назад")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.ic_back),
contentDescription = "Назад"
)
Spacer(modifier = Modifier.size(8.dp))
Text(text = stringResource(R.string.book_screen_back))
}
}
Button(
modifier = Modifier.testTag(TestIds.Book.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.main_screen_refresh))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_refresh),
contentDescription = "Обновить"
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
// Вкладки с датами
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
@@ -214,7 +278,7 @@ private fun BookDataContent(
Spacer(modifier = Modifier.size(16.dp))
// Список мест
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -262,7 +326,17 @@ private fun BookDataContent(
onClick = onBook,
enabled = state.places.any { it.isSelected }
) {
Text("Забронировать")
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.book_screen_book))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_arrow_right),
contentDescription = "Забронировать"
)
}
}
}
}

View File

@@ -16,10 +16,12 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.SegmentedButtonDefaults.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -28,12 +30,19 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import coil3.compose.SubcomposeAsyncImage
import org.intellij.lang.annotations.JdkConstants
import ru.myitschool.work.R
import ru.myitschool.work.core.TestIds
import ru.myitschool.work.ui.nav.AuthScreenDestination
import ru.myitschool.work.ui.nav.BookScreenDestination
@@ -130,6 +139,8 @@ private fun MainDataContent(
.padding(16.dp)
) {
Spacer(modifier = Modifier.size(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
@@ -147,38 +158,47 @@ private fun MainDataContent(
}
Spacer(modifier = Modifier.size(16.dp))
Text(
modifier = Modifier.testTag(TestIds.Main.PROFILE_NAME),
modifier = Modifier
.testTag(TestIds.Main.PROFILE_NAME)
.weight(1f),
text = state.name,
style = MaterialTheme.typography.titleMedium
)
Button(
modifier = Modifier
.testTag(TestIds.Main.LOGOUT_BUTTON),
onClick = onLogout,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.errorContainer,
containerColor = MaterialTheme.colorScheme.onErrorContainer
)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Выход")
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_logout),
contentDescription = "Выйти"
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.size(24.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
modifier = Modifier.testTag(TestIds.Main.LOGOUT_BUTTON),
onClick = onLogout
) {
Text("Выход")
}
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) {
Text("Обновить")
}
Button(
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
onClick = onAddBooking
) {
Text("Бронь")
}
}
Text(
text = stringResource(R.string.main_screen_title),
style = MaterialTheme.typography.titleLarge.copy(
fontWeight = FontWeight.Bold
)
)
Spacer(modifier = Modifier.size(16.dp))
Spacer(modifier = Modifier.size(20.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
@@ -192,20 +212,85 @@ private fun MainDataContent(
Column(
modifier = Modifier.padding(12.dp)
) {
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
Row(modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.main_screen_date),
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold
)
)
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_DATE),
text = item.dateLabel,
style = MaterialTheme.typography.bodyMedium
)
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
text = item.roomName,
style = MaterialTheme.typography.bodySmall
)
style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold
)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.main_screen_place),
style = MaterialTheme.typography.bodyMedium
)
Text(
modifier = Modifier.testTag(TestIds.Main.ITEM_PLACE),
text = item.roomName,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
}
Spacer(modifier = Modifier.size(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Button(
modifier = Modifier.testTag(TestIds.Main.REFRESH_BUTTON),
onClick = onRefresh
) { Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.main_screen_refresh))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_refresh),
contentDescription = "Обновить"
)
}
}
Button(
modifier = Modifier.testTag(TestIds.Main.ADD_BUTTON),
onClick = onAddBooking
) { Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = stringResource(R.string.main_screen_new_book))
Spacer(modifier = Modifier.size(8.dp))
Icon(
painter = painterResource(R.drawable.ic_add),
contentDescription = "Новая бронь"
)
}
}
}
}
}
@Composable

View File

@@ -32,26 +32,26 @@ private val LightColorScheme = lightColorScheme(
*/
)
@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)
@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
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="M0.5,8.5v-1h15v1Z"
android:fillType="evenOdd"/>
<path
android:fillColor="#FF000000"
android:pathData="M8.5,15.5h-1V0.5h1Z"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
<path android:fillColor="#FF000000" android:pathData="M11.293,4.707l6.293,6.293l-13.586,0l0,2l13.586,0l-6.293,6.293l1.414,1.414l8.707,-8.707l-8.707,-8.707l-1.414,1.414z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#FF000000"
android:pathData="M11.62,3.81l-4.19,4.19l4.19,4.19l-1.53,1.52l-5.71,-5.71l5.71,-5.71l1.53,1.52z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="32" android:viewportWidth="32" android:width="16dp">
<path android:fillColor="#FF000000" android:pathData="M27.9,2.58a0.86,0.86 0,0 0,-0.07 -0.1,0.71 0.71,0 0,0 -0.19,-0.23l0,0 -0.09,0a1.12,1.12 0,0 0,-0.25 -0.11L27.1,2 27,2H12a1,1 0,0 0,-1 1V9a1,1 0,0 0,2 0V4h7.19L16.71,5A1,1 0,0 0,16 6V25H13V22a1,1 0,0 0,-2 0v4a1,1 0,0 0,1 1h4v2a1,1 0,0 0,0.4 0.8,1 1,0 0,0 0.6,0.2 1,1 0,0 0,0.29 0l10,-3A1,1 0,0 0,28 26V3A1,1 0,0 0,27.9 2.58ZM26,25.26l-8,2.4V6.74l8,-2.4Z"/>
<path android:fillColor="#FF000000" android:pathData="M7.41,17H14a1,1 0,0 0,0 -2H7.41l1.3,-1.29a1,1 0,0 0,-1.42 -1.42l-3,3a1,1 0,0 0,-0.21 0.33,1 1,0 0,0 0,0.76 1,1 0,0 0,0.21 0.33l3,3a1,1 0,0 0,1.42 0,1 1,0 0,0 0,-1.42Z"/>
<path android:fillColor="#FF000000" android:pathData="M20,17a1,1 0,0 0,0 -2h0a1,1 0,1 0,0 2Z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="16dp" android:viewportHeight="24" android:viewportWidth="24" android:width="16dp">
<path android:fillColor="#FF000000" android:pathData="M12,3A8.959,8.959 0,0 0,5 6.339V4H3v6H9V8H6.274A6.982,6.982 0,1 1,5.22 13.751l-1.936,0.5A9,9 0,1 0,12 3Z"/>
</vector>

View File

@@ -1,7 +1,15 @@
<resources>
<string name="app_name">Work</string>
<string name="title_activity_root">RootActivity</string>
<string name="auth_title">Привет! Введи код для авторизации</string>
<string name="auth_name">Система бронирования</string>
<string name="auth_title">Вход в систему</string>
<string name="auth_label">Код</string>
<string name="auth_sign_in">Войти</string>
<string name="main_screen_date">Дата:</string>
<string name="main_screen_place">Место:</string>
<string name="main_screen_title">Мои бронирования</string>
<string name="main_screen_refresh">Обновить</string>
<string name="main_screen_new_book">Новая бронь</string>
<string name="book_screen_book">Забронировать</string>
<string name="book_screen_back">Назад</string>
</resources>

View File

@@ -1,21 +1,16 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
## For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
#
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Default value: -Xmx1024m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
#
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
#Thu Dec 04 14:16:52 GMT+07:00 2025
android.nonTransitiveRClass=true
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8