forked from agzam/vyatsu-schedule
feat(ui): add combined group/teacher picker with segments
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
package agzam.vyatsu.ui.picker
|
||||
|
||||
import agzam.vyatsu.domain.model.Group
|
||||
import agzam.vyatsu.domain.model.Teacher
|
||||
import androidx.compose.foundation.clickable
|
||||
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.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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SearchBar
|
||||
import androidx.compose.material3.SearchBarDefaults
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PickerScreen(
|
||||
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 ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.windowInsetsPadding(WindowInsets.systemBars)
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||
SegmentedButton(
|
||||
selected = segment == PickerSegment.GROUPS,
|
||||
onClick = { segment = PickerSegment.GROUPS },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
|
||||
) {
|
||||
Text("Группы")
|
||||
}
|
||||
SegmentedButton(
|
||||
selected = segment == PickerSegment.TEACHERS,
|
||||
onClick = { segment = PickerSegment.TEACHERS },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
|
||||
) {
|
||||
Text("Преподаватели")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
when (segment) {
|
||||
PickerSegment.GROUPS -> GroupSection(
|
||||
state = state,
|
||||
onSearch = viewModel::searchGroup,
|
||||
onRetry = viewModel::retryGroups,
|
||||
onRefresh = viewModel::refreshGroups,
|
||||
onGroupPicked = onGroupPicked
|
||||
)
|
||||
PickerSegment.TEACHERS -> TeacherSection(
|
||||
state = state,
|
||||
onSearch = viewModel::searchTeacher,
|
||||
onRetry = viewModel::retryTeachers,
|
||||
onTeacherPicked = onTeacherPicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun GroupSection(
|
||||
state: PickerUiState,
|
||||
onSearch: (String) -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onGroupPicked: (Group) -> Unit
|
||||
) {
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
|
||||
if (!isSearchActive) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Выберите группу",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = onRefresh) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.groupsLoading && state.groups.isEmpty()) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (state.error != null && state.groups.isEmpty()) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
state.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
TextButton(onClick = onRetry) { Text("Повторить") }
|
||||
}
|
||||
return
|
||||
}
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
expanded = isSearchActive,
|
||||
onExpandedChange = { isSearchActive = it },
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
query = state.groupQuery,
|
||||
onQueryChange = onSearch,
|
||||
onSearch = { isSearchActive = false },
|
||||
expanded = isSearchActive,
|
||||
onExpandedChange = { isSearchActive = it },
|
||||
placeholder = { Text("Поиск группы") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
if (isSearchActive && state.groupQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { onSearch("") }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = "Очистить")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
GroupList(
|
||||
groups = state.filteredGroups,
|
||||
onPick = { group ->
|
||||
isSearchActive = false
|
||||
onGroupPicked(group)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSearchActive) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
GroupList(state.filteredGroups, onGroupPicked)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GroupList(groups: List<Group>, onPick: (Group) -> Unit) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(groups) { group ->
|
||||
Column(modifier = Modifier.fillMaxWidth().clickable { onPick(group) }) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Text(group.name, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun TeacherSection(
|
||||
state: PickerUiState,
|
||||
onSearch: (String) -> Unit,
|
||||
onRetry: () -> Unit,
|
||||
onTeacherPicked: (Teacher) -> Unit
|
||||
) {
|
||||
var isSearchActive by remember { mutableStateOf(false) }
|
||||
val progress = state.syncProgress
|
||||
val syncing = progress != null && progress.done < progress.total
|
||||
|
||||
if (!isSearchActive) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Выберите преподавателя",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = onRetry) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (syncing) {
|
||||
Column(Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress.done / progress.total.toFloat() },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Кафедры: ${progress.done}/${progress.total}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.teachersLoading && state.teachers.isEmpty() && !syncing) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return
|
||||
}
|
||||
if (state.error != null && state.teachers.isEmpty() && !syncing) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
state.error,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
TextButton(onClick = onRetry) { Text("Повторить") }
|
||||
}
|
||||
return
|
||||
}
|
||||
SearchBar(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
expanded = isSearchActive,
|
||||
onExpandedChange = { isSearchActive = it },
|
||||
inputField = {
|
||||
SearchBarDefaults.InputField(
|
||||
query = state.teacherQuery,
|
||||
onQueryChange = onSearch,
|
||||
onSearch = { isSearchActive = false },
|
||||
expanded = isSearchActive,
|
||||
onExpandedChange = { isSearchActive = it },
|
||||
placeholder = { Text("Поиск преподавателя") },
|
||||
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
if (isSearchActive && state.teacherQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { onSearch("") }) {
|
||||
Icon(Icons.Default.Clear, contentDescription = "Очистить")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
TeacherList(
|
||||
teachers = state.filteredTeachers,
|
||||
onPick = { teacher ->
|
||||
isSearchActive = false
|
||||
onTeacherPicked(teacher)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!isSearchActive) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
when {
|
||||
state.teachers.isEmpty() -> Text(
|
||||
text = "Кафедры без расписания на эту неделю",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
else -> TeacherList(state.filteredTeachers, onTeacherPicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TeacherList(teachers: List<Teacher>, onPick: (Teacher) -> Unit) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(teachers) { teacher ->
|
||||
Column(modifier = Modifier.fillMaxWidth().clickable { onPick(teacher) }) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Text(teacher.name, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user