feat(ui): add combined group/teacher picker with segments

This commit is contained in:
2026-09-19 20:16:41 +03:00
parent b9b3a18f0c
commit f5696fbbd6
6 changed files with 542 additions and 259 deletions
+15 -7
View File
@@ -1,9 +1,9 @@
package agzam.vyatsu
import agzam.vyatsu.di.AppContainer
import agzam.vyatsu.ui.groups.GroupPickerScreen
import agzam.vyatsu.ui.groups.GroupPickerViewModel
import agzam.vyatsu.ui.home.HomeScreen
import agzam.vyatsu.ui.picker.PickerScreen
import agzam.vyatsu.ui.picker.PickerViewModel
import agzam.vyatsu.ui.schedule.ScheduleViewModel
import agzam.vyatsu.ui.settings.UpdateViewModel
import androidx.compose.material3.AlertDialog
@@ -52,10 +52,11 @@ fun AppRoot() {
}
if (state.noGroup) {
val groupPickerViewModel: GroupPickerViewModel = viewModel(factory = groupPickerFactory(container))
GroupPickerScreen(
viewModel = groupPickerViewModel,
onGroupPicked = { group -> scheduleViewModel.selectGroup(group.id) }
val pickerViewModel: PickerViewModel = viewModel(factory = pickerFactory(container))
PickerScreen(
viewModel = pickerViewModel,
onGroupPicked = { group -> scheduleViewModel.selectGroup(group.id) },
onTeacherPicked = { }
)
return
}
@@ -72,5 +73,12 @@ private class SimpleFactory<T : ViewModel>(private val create: () -> T) : ViewMo
}
private fun scheduleFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { ScheduleViewModel(c.scheduleRepository, c.preferencesRepository, c.groupRepository) }
private fun groupPickerFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { GroupPickerViewModel(c.groupRepository) }
private fun pickerFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory {
PickerViewModel(
c.groupRepository,
c.teacherRepository,
c.teacherScheduleRepository,
c.preferencesRepository
)
}
private fun updateFactory(c: AppContainer): ViewModelProvider.Factory = SimpleFactory { UpdateViewModel(c.appUpdater, c.preferencesRepository) }
@@ -1,152 +0,0 @@
package agzam.vyatsu.ui.groups
import agzam.vyatsu.domain.model.Group
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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.unit.dp
/**
* Страница выбора группы
* @author Agzam
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GroupPickerScreen(viewModel: GroupPickerViewModel, onGroupPicked: (Group) -> Unit) {
val state by viewModel.uiState.collectAsState()
var isSearchActive by remember { mutableStateOf(false) }
Scaffold { innerPadding ->
Column(
Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if (!isSearchActive) {
Row(
Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Выберите группу",
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.weight(1f)
)
IconButton(onClick = { viewModel.refresh() }) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
}
}
}
if (state.isLoading && state.groups.isEmpty()) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
CircularProgressIndicator()
}
return@Column
}
if (state.error != null && state.groups.isEmpty()) {
Column(Modifier.fillMaxWidth()) {
Text(state.error.orEmpty(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error)
TextButton(onClick = { viewModel.retry() }) { Text("Повторить") }
}
return@Column
}
SearchBar(
modifier = Modifier.fillMaxWidth(),
expanded = isSearchActive,
onExpandedChange = { isSearchActive = it },
inputField = {
SearchBarDefaults.InputField(
query = state.searchQuery,
onQueryChange = { viewModel.search(it) },
onSearch = { isSearchActive = false },
expanded = isSearchActive,
onExpandedChange = { isSearchActive = it },
placeholder = { Text("Поиск группы") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (isSearchActive && state.searchQuery.isNotEmpty()) {
IconButton(onClick = { viewModel.search("") }) {
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).padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterStart
) {
Text(group.name, style = MaterialTheme.typography.bodyMedium)
// XXX: стоит ли добавлять факультет или это засорит интерфейс
// Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.Bottom) {
// if (group.faculty.isNotEmpty()) Text(text = group.faculty + " · ", color = MaterialTheme.colorScheme.onSurfaceVariant)
// }
}
}
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
}
}
}
@@ -1,100 +0,0 @@
package agzam.vyatsu.ui.groups
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.repository.GroupRepository
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* @author Agzam
*/
class GroupPickerViewModel(private val repository: GroupRepository) : ViewModel() {
private val _uiState = MutableStateFlow(GroupPickerUiState())
val uiState: StateFlow<GroupPickerUiState> = _uiState.asStateFlow()
init {
load()
}
private fun load() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val ok = try {
repository.refreshIfStale()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Не удалось загрузить группы"
)
false
}
if (!ok && _uiState.value.groups.isEmpty()) {
_uiState.value = _uiState.value.copy(
error = _uiState.value.error ?: "Не удалось обновить список групп"
)
}
}
repository.observeGroups().collect { groups ->
val query = _uiState.value.searchQuery
_uiState.value = _uiState.value.copy(
isLoading = false,
groups = groups,
filteredGroups = filterGroups(groups, query),
error = null
)
}
}
}
fun search(query: String) {
val groups = _uiState.value.groups
_uiState.value = _uiState.value.copy(
searchQuery = query,
filteredGroups = filterGroups(groups, query)
)
}
private fun filterGroups(groups: List<Group>, query: String): List<Group> {
if (query.isBlank()) return groups
val lowerQuery = query.lowercase()
return groups.filter { group ->
group.name.lowercase().contains(lowerQuery) || group.faculty.lowercase().contains(lowerQuery)
}
}
fun retry() {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
load()
}
fun refresh() {
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
repository.forceRefresh()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message ?: "Не удалось обновить список групп"
)
return@withContext
}
}
}
}
}
data class GroupPickerUiState(
val isLoading: Boolean = true,
val error: String? = null,
val groups: List<Group> = emptyList(),
val searchQuery: String = "",
val filteredGroups: List<Group> = emptyList()
)
@@ -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)
}
}
}
@@ -0,0 +1,3 @@
package agzam.vyatsu.ui.picker
enum class PickerSegment { GROUPS, TEACHERS }
@@ -0,0 +1,188 @@
package agzam.vyatsu.ui.picker
import agzam.vyatsu.domain.ScheduleDates
import agzam.vyatsu.domain.model.BulkSyncProgress
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.SchedulePeriod
import agzam.vyatsu.domain.model.Teacher
import agzam.vyatsu.domain.repository.GroupRepository
import agzam.vyatsu.domain.repository.PreferencesRepository
import agzam.vyatsu.domain.repository.TeacherRepository
import agzam.vyatsu.domain.repository.TeacherScheduleRepository
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import java.time.DayOfWeek
import java.time.temporal.TemporalAdjusters
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class PickerUiState(
val segment: PickerSegment = PickerSegment.GROUPS,
val groups: List<Group> = emptyList(),
val groupQuery: String = "",
val filteredGroups: List<Group> = emptyList(),
val groupsLoading: Boolean = true,
val teachers: List<Teacher> = emptyList(),
val teacherQuery: String = "",
val filteredTeachers: List<Teacher> = emptyList(),
val teachersLoading: Boolean = true,
val syncProgress: BulkSyncProgress? = null,
val error: String? = null
)
class PickerViewModel(
private val groupRepository: GroupRepository,
private val teacherRepository: TeacherRepository,
private val teacherScheduleRepository: TeacherScheduleRepository,
private val preferencesRepository: PreferencesRepository
) : ViewModel() {
private val _uiState = MutableStateFlow(PickerUiState())
val uiState: StateFlow<PickerUiState> = _uiState.asStateFlow()
private var teachersJob: kotlinx.coroutines.Job? = null
init {
groupsLoad()
teachersLoad()
}
fun selectSegment(segment: PickerSegment) {
_uiState.value = _uiState.value.copy(segment = segment)
}
private fun groupsLoad() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
groupRepository.refreshIfStale()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Не удалось загрузить группы"
)
}
}
groupRepository.observeGroups().collect { groups ->
val query = _uiState.value.groupQuery
_uiState.value = _uiState.value.copy(
groupsLoading = false,
groups = groups,
filteredGroups = filterGroups(groups, query),
error = null
)
}
}
}
fun searchGroup(query: String) {
val groups = _uiState.value.groups
_uiState.value = _uiState.value.copy(
groupQuery = query,
filteredGroups = filterGroups(groups, query)
)
}
private fun filterGroups(groups: List<Group>, query: String): List<Group> {
if (query.isBlank()) return groups
val lowerQuery = query.lowercase()
return groups.filter { group ->
group.name.lowercase().contains(lowerQuery) || group.faculty.lowercase().contains(lowerQuery)
}
}
fun retryGroups() {
_uiState.value = _uiState.value.copy(groupsLoading = true, error = null)
groupsLoad()
}
fun refreshGroups() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
groupRepository.forceRefresh()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Не удалось обновить список групп"
)
}
}
}
}
private fun teachersLoad() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
try {
teacherRepository.refreshIfStale()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
error = e.message ?: "Не удалось загрузить кафедры"
)
}
}
syncAndObserveTeachers()
}
}
private fun syncAndObserveTeachers() {
val today = ScheduleDates.moscowToday()
val weekStart = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
val period = SchedulePeriod(
weekStart = weekStart,
weekEnd = weekStart.plusDays(6),
season = ScheduleDates.currentSeason(today)
)
_uiState.value = _uiState.value.copy(syncProgress = null, teachersLoading = true)
viewModelScope.launch {
runCatching {
teacherScheduleRepository.syncPeriod(today).collect { progress ->
_uiState.value = _uiState.value.copy(syncProgress = progress)
}
}
}
teachersJob?.cancel()
teachersJob = viewModelScope.launch {
teacherScheduleRepository.observeTeachers(period).collect { teachers ->
val query = _uiState.value.teacherQuery
_uiState.value = _uiState.value.copy(
teachersLoading = false,
teachers = teachers,
filteredTeachers = filterTeachers(teachers, query),
error = null
)
}
}
}
fun searchTeacher(query: String) {
val teachers = _uiState.value.teachers
_uiState.value = _uiState.value.copy(
teacherQuery = query,
filteredTeachers = filterTeachers(teachers, query)
)
}
private fun filterTeachers(teachers: List<Teacher>, query: String): List<Teacher> {
if (query.isBlank()) return teachers
val lowerQuery = query.lowercase()
return teachers.filter { teacher -> teacher.name.lowercase().contains(lowerQuery) }
}
fun retryTeachers() {
syncAndObserveTeachers()
}
fun selectTeacher(teacher: Teacher) {
viewModelScope.launch {
preferencesRepository.clearSelectedGroup()
preferencesRepository.setSelectedTeacherId(teacher.name)
}
}
}