forked from agzam/vyatsu-schedule
336 lines
12 KiB
Kotlin
336 lines
12 KiB
Kotlin
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)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|