New materail3 UI

This commit is contained in:
2026-10-20 20:53:52 +03:00
parent 84da62c45d
commit 4fb1e3edca
33 changed files with 2198 additions and 85 deletions
@@ -1,29 +1,32 @@
package agzam.vyatsu.ui.picker
import agzam.vyatsu.R
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.Teacher
import androidx.compose.foundation.clickable
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
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.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@@ -36,30 +39,429 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerScreen(
modifier: Modifier = Modifier,
viewModel: PickerViewModel,
onGroupPicked: (Group) -> Unit,
onTeacherPicked: (Teacher) -> Unit
onTeacherPicked: (Teacher) -> Unit,
) {
val state by viewModel.uiState.collectAsState()
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
LaunchedEffect(segment)
{
viewModel.selectSegment(segment)
}
val keyboardController = LocalSoftwareKeyboardController.current
val groupsListState = rememberLazyListState()
val teachersListState = rememberLazyListState()
val listState = when (segment) {
PickerSegment.GROUPS -> groupsListState
PickerSegment.TEACHERS -> teachersListState
}
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.collect { isScrolling ->
if (isScrolling) {
keyboardController?.hide()
}
}
}
val query = when (segment) {
PickerSegment.GROUPS -> state.groupQuery
PickerSegment.TEACHERS -> state.teacherQuery
}
val onSearch: (String) -> Unit = when (segment) {
PickerSegment.GROUPS -> viewModel::searchGroup
PickerSegment.TEACHERS -> viewModel::searchTeacher
}
val placeholder = when (segment) {
PickerSegment.GROUPS -> "Поиск группы"
PickerSegment.TEACHERS -> "Поиск преподавателя"
}
val isLoading = when (segment) {
PickerSegment.GROUPS -> state.groupsLoading
PickerSegment.TEACHERS -> state.teachersLoading
}
val isEmpty = when (segment) {
PickerSegment.GROUPS -> state.groups.isEmpty()
PickerSegment.TEACHERS -> state.teachers.isEmpty()
}
val onRetry: () -> Unit = when (segment) {
PickerSegment.GROUPS -> viewModel::retryGroups
PickerSegment.TEACHERS -> viewModel::retryTeachers
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Кого ищем",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
actions = {
IconButton(onClick = {
when (segment) {
PickerSegment.GROUPS -> viewModel.refreshGroups()
PickerSegment.TEACHERS -> viewModel.refreshTeachers()
}
}) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Выберите группу или преподавателя",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth()
) {
SegmentedButton(
selected = segment == PickerSegment.GROUPS,
onClick = {
segment = PickerSegment.GROUPS
},
shape = SegmentedButtonDefaults.itemShape(
index = 0,
count = 2
),
icon = {
Icon(
painter = if (segment == PickerSegment.GROUPS)
painterResource(R.drawable.groups_24dp_fill)
else painterResource(R.drawable.groups_24dp_outline),
contentDescription = "Select group"
)
}
) {
Text("Группы")
}
SegmentedButton(
selected = segment == PickerSegment.TEACHERS,
onClick = {
segment = PickerSegment.TEACHERS
},
shape = SegmentedButtonDefaults.itemShape(
index = 1,
count = 2
),
icon = {
Icon(
painter = if (segment == PickerSegment.TEACHERS)
painterResource(R.drawable.person_24dp_fill)
else painterResource(R.drawable.person_24dp_outline),
contentDescription = "Select teacher"
)
}
) {
Text("Преподаватели")
}
}
Spacer(Modifier.padding(8.dp))
PickerSearchBar(
query = query,
onQueryChange = onSearch,
placeholder = placeholder
)
Spacer(Modifier.height(16.dp))
if (segment == PickerSegment.TEACHERS) {
val progress = state.syncProgress
if (progress != null &&
progress.total > 0 &&
progress.done < progress.total
) {
LinearProgressIndicator(
progress = {
progress.done.toFloat() /
progress.total.toFloat()
},
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Text(
text = "Кафедры: ${progress.done}/${progress.total}",
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.height(8.dp))
}
}
if (isLoading && isEmpty) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
} else if (state.error != null && isEmpty) {
Text(
text = state.error ?: "",
color = MaterialTheme.colorScheme.error
)
TextButton(onClick = onRetry) {
Text("Повторить")
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 16.dp)
) {
when (segment) {
PickerSegment.GROUPS -> {
items(
items = state.filteredGroups,
key = { it.name }
) { group ->
PickerCard(
name = group.name,
segment = segment,
onClick = {
onGroupPicked(group)
}
)
}
}
PickerSegment.TEACHERS -> {
items(
items = state.filteredTeachers,
key = { it.name }
) { teacher ->
PickerCard(
name = teacher.name,
segment = segment,
onClick = {
onTeacherPicked(teacher)
}
)
}
}
}
}
}
}
}
}
@Composable
private fun PickerCard(
modifier: Modifier = Modifier,
name: String,
segment: PickerSegment,
onClick: () -> Unit,
) {
val iconRes = when (segment) {
PickerSegment.GROUPS -> R.drawable.groups_24dp_outline
PickerSegment.TEACHERS -> R.drawable.person_24dp_outline
}
Card(
modifier = modifier
.fillMaxWidth()
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
shape = MaterialTheme.shapes.large,
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp,
pressedElevation = 4.dp
),
onClick = onClick
) {
Row (
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.small
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(iconRes),
contentDescription = "Icons",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
Text (
text = name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = "Select",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PickerSearchBar(
query: String,
onQueryChange: (String) -> Unit,
placeholder: String,
modifier: Modifier = Modifier
) {
SearchBarDefaults.InputField(
query = query,
onQueryChange = onQueryChange,
onSearch = {},
expanded = false,
onExpandedChange = {},
modifier = modifier.fillMaxWidth(),
placeholder = {
Text(placeholder)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(
onClick = { onQueryChange("") }
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Очистить поиск"
)
}
}
}
)
}
/*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerScreen(
modifier: Modifier = Modifier,
viewModel: PickerViewModel,
onGroupPicked: (Group) -> Unit,
onTeacherPicked: (Teacher) -> Unit,
) {
val state by viewModel.uiState.collectAsState()
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
LaunchedEffect(segment) { viewModel.selectSegment(segment) }
Scaffold { innerPadding ->
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Кого ищем",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
actions = {
IconButton(onClick = { onRefresh() } ) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
}
)
}
) { innerPadding ->
Column(
Modifier
.fillMaxSize()
@@ -333,4 +735,4 @@ private fun TeacherList(teachers: List<Teacher>, onPick: (Teacher) -> Unit) {
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
}
}
}
}*/
@@ -201,4 +201,29 @@ class PickerViewModel(
fun retryTeachers() {
syncAndObserveTeachers()
}
// Добавил обновление списка преподов
fun refreshTeachers() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(
teachersLoading = true,
error = null
)
try {
withContext(Dispatchers.IO) {
teacherRepository.forceRefresh()
}
syncAndObserveTeachers()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
teachersLoading = false,
error = e.message ?: "Не удалось обновить преподавателей"
)
}
}
}
}