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

@@ -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>