From f5696fbbd693af05fae7b7bc126c7c9e434e38cb Mon Sep 17 00:00:00 2001 From: Agzamus Date: Sat, 19 Sep 2026 20:16:41 +0300 Subject: [PATCH] feat(ui): add combined group/teacher picker with segments --- app/src/main/java/agzam/vyatsu/AppRoot.kt | 22 +- .../vyatsu/ui/groups/GroupPickerScreen.kt | 152 -------- .../vyatsu/ui/groups/GroupPickerViewModel.kt | 100 ------ .../agzam/vyatsu/ui/picker/PickerScreen.kt | 336 ++++++++++++++++++ .../agzam/vyatsu/ui/picker/PickerSegment.kt | 3 + .../agzam/vyatsu/ui/picker/PickerViewModel.kt | 188 ++++++++++ 6 files changed, 542 insertions(+), 259 deletions(-) delete mode 100644 app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerScreen.kt delete mode 100644 app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerViewModel.kt create mode 100644 app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt create mode 100644 app/src/main/java/agzam/vyatsu/ui/picker/PickerSegment.kt create mode 100644 app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt diff --git a/app/src/main/java/agzam/vyatsu/AppRoot.kt b/app/src/main/java/agzam/vyatsu/AppRoot.kt index 5b16756..a79b208 100644 --- a/app/src/main/java/agzam/vyatsu/AppRoot.kt +++ b/app/src/main/java/agzam/vyatsu/AppRoot.kt @@ -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(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) } diff --git a/app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerScreen.kt b/app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerScreen.kt deleted file mode 100644 index df06eba..0000000 --- a/app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerScreen.kt +++ /dev/null @@ -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, 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) - } - } -} diff --git a/app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerViewModel.kt deleted file mode 100644 index 75fa4bd..0000000 --- a/app/src/main/java/agzam/vyatsu/ui/groups/GroupPickerViewModel.kt +++ /dev/null @@ -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 = _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, query: String): List { - 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 = emptyList(), - val searchQuery: String = "", - val filteredGroups: List = emptyList() -) diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt new file mode 100644 index 0000000..e995598 --- /dev/null +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt @@ -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, 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, 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerSegment.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerSegment.kt new file mode 100644 index 0000000..e9d19cb --- /dev/null +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerSegment.kt @@ -0,0 +1,3 @@ +package agzam.vyatsu.ui.picker + +enum class PickerSegment { GROUPS, TEACHERS } \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt new file mode 100644 index 0000000..a7be5c9 --- /dev/null +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt @@ -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 = emptyList(), + val groupQuery: String = "", + val filteredGroups: List = emptyList(), + val groupsLoading: Boolean = true, + val teachers: List = emptyList(), + val teacherQuery: String = "", + val filteredTeachers: List = 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 = _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, query: String): List { + 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, query: String): List { + 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) + } + } +} \ No newline at end of file