Files
vyatsu-schedule/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt
T

350 lines
14 KiB
Kotlin
Raw Normal View History

2026-09-02 23:47:00 +03:00
package agzam.vyatsu.ui.schedule
import agzam.vyatsu.domain.model.Day
2026-09-05 00:21:39 +03:00
import agzam.vyatsu.domain.model.LessonType
2026-09-03 20:53:17 +03:00
import agzam.vyatsu.domain.model.SchedulePeriod
2026-09-02 23:47:00 +03:00
import agzam.vyatsu.domain.model.TimeSlot
2026-09-03 23:34:45 +03:00
import androidx.compose.foundation.clickable
2026-09-02 23:47:00 +03:00
import androidx.compose.foundation.layout.Arrangement
2026-09-04 01:07:09 +03:00
import androidx.compose.foundation.layout.Box
2026-09-02 23:47:00 +03:00
import androidx.compose.foundation.layout.Column
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.systemBars
2026-09-04 01:07:09 +03:00
import androidx.compose.foundation.layout.widthIn
2026-09-02 23:47:00 +03:00
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
2026-09-03 20:53:17 +03:00
import androidx.compose.foundation.lazy.items
2026-09-02 23:47:00 +03:00
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
2026-09-03 23:34:45 +03:00
import androidx.compose.material.icons.filled.ArrowDropDown
2026-09-02 23:47:00 +03:00
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
2026-09-04 01:07:09 +03:00
import androidx.compose.material3.DropdownMenu
2026-09-03 23:16:57 +03:00
import androidx.compose.material3.DropdownMenuItem
2026-09-02 23:47:00 +03:00
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.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
2026-09-03 23:16:57 +03:00
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
2026-09-02 23:47:00 +03:00
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
2026-09-03 23:34:45 +03:00
import androidx.compose.ui.draw.clip
2026-09-02 23:47:00 +03:00
import androidx.compose.ui.unit.dp
import java.time.DayOfWeek
2026-09-03 20:53:17 +03:00
import java.time.LocalDate
2026-09-02 23:47:00 +03:00
import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
val state by viewModel.uiState.collectAsState()
Column(Modifier.fillMaxSize()) {
TopAppBar(
2026-09-04 02:05:52 +03:00
title = { Text(state.groupName ?: "Расписание", style = MaterialTheme.typography.titleMedium) },
2026-09-02 23:47:00 +03:00
actions = {
TextButton(onClick = { onSwitchGroup() }) {
Text("Сменить группу")
}
IconButton(onClick = { viewModel.refresh() }) {
Icon(Icons.Default.Refresh, contentDescription = "Обновить")
}
},
2026-09-04 01:22:35 +03:00
windowInsets = WindowInsets(0.dp)
2026-09-02 23:47:00 +03:00
)
2026-09-03 20:53:17 +03:00
Column(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 0.dp)) {
2026-09-02 23:47:00 +03:00
when {
state.isSyncing && state.schedule == null -> {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
}
}
state.schedule == null -> {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Не удалось загрузить расписание",
style = MaterialTheme.typography.bodyLarge
)
TextButton(onClick = { viewModel.refresh() }) {
Text("Повторить")
}
}
}
else -> {
val schedule = state.schedule!!
2026-09-03 20:53:17 +03:00
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 }
2026-09-02 23:47:00 +03:00
if (day == null) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
} else {
2026-09-03 20:53:17 +03:00
val isTodayDay = day.date == state.todayDate
val dayLabel = if (isTodayDay) "Сегодня " else ""
2026-09-02 23:47:00 +03:00
2026-09-03 23:34:45 +03:00
Row(
Modifier.fillMaxWidth(),
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
)
}
2026-09-04 01:22:35 +03:00
HorizontalDivider(Modifier.padding(top = 8.dp, bottom = 0.dp))
2026-09-02 23:47:00 +03:00
DayChips(
2026-09-03 20:53:17 +03:00
days = weekDays,
selectedDate = state.dayDate,
todayDate = state.todayDate,
2026-09-02 23:47:00 +03:00
onSelect = viewModel::selectDay
)
2026-09-04 01:22:35 +03:00
HorizontalDivider(Modifier.padding(top=0.dp, bottom = 8.dp))
2026-09-02 23:47:00 +03:00
if (day.intervals.all { it.lessons.isEmpty() }) {
Text("Занятий нет", style = MaterialTheme.typography.bodyLarge)
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(day.intervals) { slotIndex, slot ->
LessonSlot(slotIndex = slotIndex, slot = slot)
}
}
}
}
}
}
}
}
}
2026-09-03 23:16:57 +03:00
@OptIn(ExperimentalMaterial3Api::class)
2026-09-03 20:53:17 +03:00
@Composable
2026-09-03 23:16:57 +03:00
private fun WeekSelector(
2026-09-03 20:53:17 +03:00
periods: List<SchedulePeriod>,
selectedWeekStart: LocalDate?,
todayWeekStart: LocalDate?,
onSelect: (LocalDate) -> Unit
) {
2026-09-03 23:16:57 +03:00
val selected = periods.firstOrNull { it.weekStart == selectedWeekStart }
var expanded by remember { mutableStateOf(false) }
2026-09-04 01:07:09 +03:00
Box {
2026-09-03 23:34:45 +03:00
Row(
2026-09-03 23:16:57 +03:00
modifier = Modifier
2026-09-03 23:34:45 +03:00
.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
)
}
2026-09-04 01:07:09 +03:00
DropdownMenu(
2026-09-03 23:16:57 +03:00
expanded = expanded,
2026-09-04 01:07:09 +03:00
onDismissRequest = { expanded = false },
modifier = Modifier.widthIn(min = 220.dp)
2026-09-03 23:16:57 +03:00
) {
periods.forEach { period ->
val isTodayWeek = todayWeekStart == period.weekStart
DropdownMenuItem(
text = {
2026-09-04 01:07:09 +03:00
Text(
weekLabel(period),
style = MaterialTheme.typography.bodyLarge
)
},
trailingIcon = if (isTodayWeek) {
{
2026-09-03 23:16:57 +03:00
Text(
2026-09-04 01:07:09 +03:00
"сегодня",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
2026-09-03 23:16:57 +03:00
)
}
2026-09-04 01:07:09 +03:00
} else {
null
2026-09-03 23:16:57 +03:00
},
onClick = {
expanded = false
onSelect(period.weekStart)
}
)
}
2026-09-03 20:53:17 +03:00
}
}
}
2026-09-02 23:47:00 +03:00
@Composable
private fun DayChips(
days: List<Day>,
2026-09-03 20:53:17 +03:00
selectedDate: LocalDate?,
todayDate: LocalDate?,
onSelect: (LocalDate) -> Unit
2026-09-02 23:47:00 +03:00
) {
LazyRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
2026-09-03 20:53:17 +03:00
items(days, key = { it.date.toEpochDay() }) { day ->
val selected = selectedDate == day.date
val isTodayChip = todayDate == day.date
2026-09-02 23:47:00 +03:00
FilterChip(
selected = selected,
2026-09-03 20:53:17 +03:00
onClick = { onSelect(day.date) },
2026-09-02 23:47:00 +03:00
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)
)
}
}
}
2026-09-03 20:53:17 +03:00
private fun weekLabel(period: SchedulePeriod): String {
val first = period.weekStart
return "с ${first.format(CHIP_DATE)}"
}
2026-09-02 23:47:00 +03:00
@Composable
private fun LessonSlot(slotIndex: Int, slot: TimeSlot) {
Column(Modifier.fillMaxWidth()) {
Text(
"${slotIndex + 1} пара · ${slot.start.format(TIME)}–${slot.end.format(TIME)}",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
if (slot.lessons.isEmpty()) {
Text("—", style = MaterialTheme.typography.bodyMedium)
} else {
slot.lessons.forEach { lesson ->
Column(Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
2026-09-05 00:21:39 +03:00
var any = false
if(lesson.type != LessonType.OTHER) {
Text(
lesson.type.label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
any = true
}
2026-09-04 04:40:29 +03:00
2026-09-05 00:21:39 +03:00
val subject = lesson.subject?.takeIf { it.isNotBlank() } ?: lesson.raw
if(subject.isNotEmpty()) {
Text(
subject,
style = MaterialTheme.typography.bodyLarge
2026-09-03 20:54:44 +03:00
)
2026-09-05 00:21:39 +03:00
any = true
}
val meta = listOfNotNull(
lesson.subgroup,
lesson.teacher,
lesson.room
).filter { it.isNotBlank() }.joinToString(" · ")
if(meta.isNotEmpty()) {
Text(
meta,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
any = true
}
if(!any) {
Text(
"-",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
2026-09-02 23:47:00 +03:00
}
}
}
}
}
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")
2026-09-03 20:53:17 +03:00
private val TIME: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm")