forked from agzam/vyatsu-schedule
New materail3 UI
This commit is contained in:
@@ -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 ?: "Не удалось обновить преподавателей"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user