package agzam.vyatsu.ui.schedule import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.LessonType import agzam.vyatsu.domain.model.SchedulePeriod import agzam.vyatsu.domain.model.TimeSlot import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable 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.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar 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.draw.clip import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import java.time.DayOfWeek import java.time.LocalDate import java.time.format.DateTimeFormatter val hPadding = 16.dp @OptIn(ExperimentalMaterial3Api::class) @Composable fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { val state by viewModel.uiState.collectAsState() Column(Modifier.fillMaxSize()) { TopAppBar( title = { Text(state.groupName ?: "Расписание", style = MaterialTheme.typography.titleMedium) }, actions = { if (state.isLoading && state.schedule != null) { CircularProgressIndicator( modifier = Modifier.size(24.dp), strokeWidth = 2.dp ) } TextButton(onClick = { onSwitchGroup() }) { Text("Сменить группу") } IconButton(onClick = { viewModel.refresh() }) { Icon(Icons.Default.Refresh, contentDescription = "Обновить") } }, windowInsets = WindowInsets(0.dp) ) Column(Modifier.fillMaxSize().padding(horizontal = 0.dp, vertical = 0.dp)) { when { state.schedule == null && state.loadError -> { Column( Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Не удалось загрузить расписание", style = MaterialTheme.typography.bodyLarge ) TextButton(onClick = { viewModel.refresh() }) { Text("Повторить") } } } state.schedule == null -> { Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center ) { CircularProgressIndicator() } } else -> { val schedule = state.schedule!! val weekDays = state.weekStart ?.let { ws -> schedule.periods.firstOrNull { it.weekStart == ws } } ?.let { schedule.daysOf(it) } .orEmpty() val day = schedule.days.firstOrNull { it.date == state.dayDate } if (day == null) { Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) } else { val isTodayDay = day.date == state.todayDate val dayLabel = if (isTodayDay) "Сегодня " else "" Row( Modifier.fillMaxWidth().padding(horizontal = hPadding), verticalAlignment = Alignment.CenterVertically ) { Text( "$dayLabel${dayName(day.date.dayOfWeek)} · ${day.date.format(FULL_DATE)}", style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f) ) WeekSelector( periods = schedule.periods, selectedWeekStart = state.weekStart, todayWeekStart = state.todayDate?.let { schedule.periodOf(it)?.weekStart }, onSelect = viewModel::selectWeek ) } HorizontalDivider(Modifier.padding(top = 8.dp, bottom = 0.dp, start = hPadding, end = hPadding)) DayChips( days = weekDays, selectedDate = state.dayDate, todayDate = state.todayDate, onSelect = viewModel::selectDay ) HorizontalDivider(Modifier.padding(top=0.dp, start = hPadding, end = hPadding)) if (day.intervals.all { it.lessons.isEmpty() }) { Text("Занятий нет", style = MaterialTheme.typography.bodyLarge) } else { LazyColumn( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(vertical = 8.dp) ) { itemsIndexed(day.intervals) { slotIndex, slot -> LessonSlot(slotIndex = slotIndex, slot = slot) } } } } } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun WeekSelector( periods: List, selectedWeekStart: LocalDate?, todayWeekStart: LocalDate?, onSelect: (LocalDate) -> Unit ) { val selected = periods.firstOrNull { it.weekStart == selectedWeekStart } var expanded by remember { mutableStateOf(false) } Box { Row( modifier = Modifier .clip(MaterialTheme.shapes.small) .clickable(enabled = true, onClick = { expanded = true }) .padding(horizontal = 10.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( selected?.let(::weekLabel) ?: "Период", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary ) Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = null, tint = MaterialTheme.colorScheme.primary ) } DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, modifier = Modifier.widthIn(min = 220.dp) ) { periods.forEach { period -> val isTodayWeek = todayWeekStart == period.weekStart DropdownMenuItem( text = { Text( weekLabel(period), style = MaterialTheme.typography.bodyLarge ) }, trailingIcon = if (isTodayWeek) { { Text( "сегодня", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary ) } } else { null }, onClick = { expanded = false onSelect(period.weekStart) } ) } } } } @Composable private fun DayChips( days: List, selectedDate: LocalDate?, todayDate: LocalDate?, onSelect: (LocalDate) -> Unit ) { LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) { items(days, key = { it.date.toEpochDay() }) { day -> val selected = selectedDate == day.date val isTodayChip = todayDate == day.date FilterChip( selected = selected, onClick = { onSelect(day.date) }, label = { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( dayName(day.date.dayOfWeek), style = MaterialTheme.typography.labelMedium ) Text( day.date.format(CHIP_DATE), style = MaterialTheme.typography.labelSmall, color = if (selected) { MaterialTheme.colorScheme.onSecondaryContainer } else { MaterialTheme.colorScheme.onSurfaceVariant } ) } }, modifier = Modifier.padding(vertical = 2.dp), border = if (isTodayChip && !selected) { FilterChipDefaults.filterChipBorder( borderColor = MaterialTheme.colorScheme.primary, borderWidth = 1.dp, enabled = true, selected = false ) } else { FilterChipDefaults.filterChipBorder(enabled = true, selected = selected) }, shape = RoundedCornerShape(8.dp) ) } } } private fun weekLabel(period: SchedulePeriod): String { val first = period.weekStart return "с ${first.format(CHIP_DATE)}" } @Composable private fun LessonSlot(slotIndex: Int, slot: TimeSlot) { val context = LocalContext.current var indexColor = 0 Column(Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { HorizontalDivider( modifier = Modifier.width(10.dp + hPadding).padding(start = hPadding), color = MaterialTheme.colorScheme.primary ) Text( "${slotIndex + 1} пара · ${slot.start.format(TIME)}–${slot.end.format(TIME)}", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(horizontal = 4.dp) ) HorizontalDivider(color = MaterialTheme.colorScheme.primary, modifier = Modifier.weight(1f).padding(end = hPadding)) } if (slot.lessons.isEmpty()) { Text("—", style = MaterialTheme.typography.bodyMedium) } else { slot.lessons.forEachIndexed { index, lesson -> var baseSurface = Modifier.fillMaxWidth().padding(vertical = 2.dp) if ((indexColor) % 2 != 0) { baseSurface = baseSurface.background( MaterialTheme.colorScheme.surfaceContainer ); } indexColor++ // XXX: // Убрать SelectionContainer? // Режим редактирования расписания? // Меню на onClick? // Меню на onLongClick? SelectionContainer() { Column( baseSurface // XXX: combinedClickable? //.combinedClickable( // onClick = { // Toast.makeText( // context, // lesson.raw, // Toast.LENGTH_SHORT // ).show() // }, // onLongClick = {}, // // TODO: onLongClickLabel = "(Accessibility)" //), ) { var any = false val meta = listOfNotNull( if (lesson.type == LessonType.OTHER) { null } else { lesson.type.label }, lesson.subgroup, lesson.teacher, lesson.room ).filter { it.isNotBlank() }.joinToString(" · ") if (meta.isNotEmpty()) { Text( meta, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = hPadding) ) any = true } val subject = lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw if (subject.isNotEmpty()) { Text( subject, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(horizontal = hPadding) ) any = true } if (!any) { Text( "пусто", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = hPadding) ) } } } } } } } private fun dayName(dayOfWeek: DayOfWeek): String = when (dayOfWeek) { DayOfWeek.MONDAY -> "Пн" DayOfWeek.TUESDAY -> "Вт" DayOfWeek.WEDNESDAY -> "Ср" DayOfWeek.THURSDAY -> "Чт" DayOfWeek.FRIDAY -> "Пт" DayOfWeek.SATURDAY -> "Сб" DayOfWeek.SUNDAY -> "Вс" } private val FULL_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") private val CHIP_DATE: DateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM") private val TIME: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")