Compare commits

...
Author SHA1 Message Date
parzival-2077 2a4e010b21 Some update UI 2 2026-10-21 12:33:55 +03:00
parzival-2077 e1019b18d3 some update 2026-10-21 12:11:30 +03:00
parzival-2077 4fb1e3edca New materail3 UI 2026-10-20 20:53:52 +03:00
33 changed files with 2190 additions and 103 deletions
+2
View File
@@ -117,4 +117,6 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
} }
@@ -1,5 +1,6 @@
package agzam.vyatsu.ui.home package agzam.vyatsu.ui.home
import agzam.vyatsu.R
import agzam.vyatsu.ui.info.BuildingsScreen import agzam.vyatsu.ui.info.BuildingsScreen
import agzam.vyatsu.ui.info.CallsScreen import agzam.vyatsu.ui.info.CallsScreen
import agzam.vyatsu.ui.schedule.ScheduleScreen import agzam.vyatsu.ui.schedule.ScheduleScreen
@@ -8,18 +9,18 @@ import agzam.vyatsu.ui.settings.SettingsScreen
import agzam.vyatsu.ui.settings.UpdateViewModel import agzam.vyatsu.ui.settings.UpdateViewModel
import agzam.vyatsu.ui.teacher.TeacherScheduleScreen import agzam.vyatsu.ui.teacher.TeacherScheduleScreen
import agzam.vyatsu.ui.teacher.TeacherScheduleViewModel import agzam.vyatsu.ui.teacher.TeacherScheduleViewModel
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -27,6 +28,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
private enum class HomeTab(val label: String) { private enum class HomeTab(val label: String) {
SCHEDULE("Расписание"), SCHEDULE("Расписание"),
@@ -35,8 +38,12 @@ private enum class HomeTab(val label: String) {
SETTINGS("Настройки") SETTINGS("Настройки")
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen( fun HomeScreen(
modifier: Modifier = Modifier,
scheduleViewModel: ScheduleViewModel, scheduleViewModel: ScheduleViewModel,
teacherScheduleViewModel: TeacherScheduleViewModel, teacherScheduleViewModel: TeacherScheduleViewModel,
updateViewModel: UpdateViewModel, updateViewModel: UpdateViewModel,
@@ -47,19 +54,32 @@ fun HomeScreen(
val teacherState by teacherScheduleViewModel.uiState.collectAsState() val teacherState by teacherScheduleViewModel.uiState.collectAsState()
Scaffold( Scaffold(
modifier = Modifier,
bottomBar = { bottomBar = {
NavigationBar { NavigationBar { // Новое нижнее меню
HomeTab.entries.forEach { t -> HomeTab.entries.forEach { t ->
NavigationBarItem( NavigationBarItem(
selected = tab == t, selected = tab == t,
onClick = { tab = t }, onClick = { tab = t },
icon = { icon = {
Icon( Icon(
imageVector = when (t) { painter = when (t) {
HomeTab.SCHEDULE -> Icons.Default.DateRange HomeTab.SCHEDULE -> {
HomeTab.CALLS -> Icons.Default.Info if (tab == t) painterResource(R.drawable.calendar_month_24dp_fill)
HomeTab.BUILDINGS -> Icons.Default.Home else painterResource(R.drawable.calendar_month_24dp_outline)
HomeTab.SETTINGS -> Icons.Default.Settings }
HomeTab.CALLS -> {
if (tab == t) painterResource(R.drawable.notifications_active_24dp_fill)
else painterResource(R.drawable.notifications_active_outline)
}
HomeTab.BUILDINGS -> {
if (tab == t) painterResource(R.drawable.location_on_24dp_fill)
else painterResource(R.drawable.location_on_24dp_outline)
}
HomeTab.SETTINGS -> {
if (tab == t) painterResource(R.drawable.settings_24dp_fill)
else painterResource(R.drawable.settings_24dp_outline)
}
}, },
contentDescription = t.label contentDescription = t.label
) )
@@ -70,7 +90,7 @@ fun HomeScreen(
} }
} }
) { innerPadding -> ) { innerPadding ->
androidx.compose.foundation.layout.Box( Box(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)
@@ -23,7 +23,156 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import agzam.vyatsu.BuildConfig import agzam.vyatsu.BuildConfig
import agzam.vyatsu.R
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BuildingsScreen() { // Новый корпусов
val context = LocalContext.current
Scaffold(
modifier = Modifier,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Text(
text = "Корпуса", // Заголовок
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 12.dp,
bottom = 24.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Column(
modifier = Modifier
.padding(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "Расположение корпусов", // Еще заголовков
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
Text(
text = "Адреса учебных корпусов",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
items(BUILDINGS) { building ->
// Карточки корпусов
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
onClick = {
val uri = Uri.parse("geo:${building.latitude},${building.longitude}?q=${building.latitude},${building.longitude}(Корпус ${building.number})")
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
},
colors = CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
modifier = Modifier.size(60.dp),
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) {
Box(
contentAlignment = Alignment.Center
) {
Text(
text = "${building.number}",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
}
// Название
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "Корпус ${building.number}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = "${building.address}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Время
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon (
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = "Enter"
)
}
}
}
}
}
}
}
/*
@Composable @Composable
fun BuildingsScreen() { fun BuildingsScreen() {
val context = LocalContext.current val context = LocalContext.current
@@ -56,3 +205,5 @@ fun BuildingsScreen() {
} }
} }
} }
*/
@@ -1,21 +1,370 @@
package agzam.vyatsu.ui.info package agzam.vyatsu.ui.info
import agzam.vyatsu.data.static.CALLS import agzam.vyatsu.data.static.CALLS
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.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CallsScreen() {
var now by remember {
mutableStateOf(LocalTime.now())
}
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
now = LocalTime.now()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
LaunchedEffect(Unit) {
while (true) {
now = LocalTime.now()
delay(15_000L)
}
}
val intervals = remember {
val formatter = DateTimeFormatter.ofPattern("H:mm")
CALLS.mapIndexed { index, call ->
CallInterval(
number = index + 1,
start = LocalTime.parse(call.start, formatter),
end = LocalTime.parse(call.end, formatter)
)
}
}
val current = intervals.firstOrNull {
!now.isBefore(it.start) && now.isBefore(it.end)
}
val next = intervals.firstOrNull {
now.isBefore(it.start)
}
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Text(
text = "Звонки",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 12.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
CallsStatusCard(
now = now,
current = current,
next = next,
first = intervals.firstOrNull()
)
}
items(
items = intervals,
key = { it.number }
) { interval ->
val isCurrent =
current?.number == interval.number
CallCard(
interval = interval,
now = now,
isCurrent = isCurrent
)
}
}
}
}
private data class CallInterval(
val number: Int,
val start: LocalTime,
val end: LocalTime
)
@Composable
private fun CallCard(
interval: CallInterval,
now: LocalTime,
isCurrent: Boolean
) {
val isFinished = !now.isBefore(interval.end)
val status = when {
isCurrent -> "Идёт сейчас"
isFinished -> "Завершена"
else -> "Впереди"
}
val totalSeconds = ChronoUnit.SECONDS.between(
interval.start,
interval.end
).coerceAtLeast(1)
val elapsedSeconds = ChronoUnit.SECONDS.between(
interval.start,
now
)
val progress = (
elapsedSeconds.toFloat() / totalSeconds
).coerceIn(0f, 1f)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = if (isCurrent) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceContainerLow
}
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
modifier = Modifier.size(56.dp),
shape = RoundedCornerShape(16.dp),
color = if (isCurrent) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.secondaryContainer
},
contentColor = if (isCurrent) {
MaterialTheme.colorScheme.onPrimary
} else {
MaterialTheme.colorScheme.onSecondaryContainer
}
) {
Box(
contentAlignment = Alignment.Center
) {
Text(
text = interval.number.toString(),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = if (isCurrent) {
"Текущая пара"
} else {
"Пара"
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = status,
style = MaterialTheme.typography.bodySmall
)
}
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = interval.start.toString(),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = interval.end.toString(),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
}
@Composable
private fun CallsStatusCard(
now: LocalTime,
current: CallInterval?,
next: CallInterval?,
first: CallInterval?
) {
val title: String
val subtitle: String
val progress: Float
when {
current != null -> {
title = "Сейчас идёт ${current.number} пара"
subtitle = "До конца ${minutesUntil(now, current.end)} мин"
val total = ChronoUnit.SECONDS.between(
current.start,
current.end
).coerceAtLeast(1)
val elapsed = ChronoUnit.SECONDS.between(
current.start,
now
)
progress = (
elapsed.toFloat() / total
).coerceIn(0f, 1f)
}
next != null -> {
title = if (first?.number == next.number) {
"Занятия ещё не начались"
} else {
"Сейчас перемена"
}
subtitle = "До ${next.number} пары ${minutesUntil(now, next.start)} мин"
progress = 0f
}
else -> {
title = "Занятия закончились"
subtitle = "На сегодня пар больше нет"
progress = 1f
}
}
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = subtitle,
style = MaterialTheme.typography.headlineSmall
)
if (current != null) {
LinearProgressIndicator(
progress = { progress },
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.surfaceContainerHighest
)
}
}
}
}
private fun minutesUntil(
now: LocalTime,
target: LocalTime
): Long {
val seconds = ChronoUnit.SECONDS.between(
now,
target
).coerceAtLeast(0)
return (seconds + 59) / 60
}
/*
// Агзам
@Composable @Composable
fun CallsScreen() { fun CallsScreen() {
LazyColumn( LazyColumn(
@@ -40,3 +389,5 @@ fun CallsScreen() {
} }
} }
} }
*/
@@ -1,65 +1,466 @@
package agzam.vyatsu.ui.picker package agzam.vyatsu.ui.picker
import agzam.vyatsu.R
import agzam.vyatsu.domain.model.Group import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.Teacher import agzam.vyatsu.domain.model.Teacher
import androidx.compose.foundation.clickable import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PickerScreen( fun PickerScreen(
modifier: Modifier = Modifier,
viewModel: PickerViewModel, viewModel: PickerViewModel,
onGroupPicked: (Group) -> Unit, onGroupPicked: (Group) -> Unit,
onTeacherPicked: (Teacher) -> Unit onTeacherPicked: (Teacher) -> Unit,
) {
val state by viewModel.uiState.collectAsState()
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
LaunchedEffect(segment)
{
viewModel.selectSegment(segment)
}
val keyboardController = LocalSoftwareKeyboardController.current
val groupsListState = rememberLazyListState()
val teachersListState = rememberLazyListState()
val listState = when (segment) {
PickerSegment.GROUPS -> groupsListState
PickerSegment.TEACHERS -> teachersListState
}
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.collect { isScrolling ->
if (isScrolling) {
keyboardController?.hide()
}
}
}
val query = when (segment) {
PickerSegment.GROUPS -> state.groupQuery
PickerSegment.TEACHERS -> state.teacherQuery
}
val onSearch: (String) -> Unit = when (segment) {
PickerSegment.GROUPS -> viewModel::searchGroup
PickerSegment.TEACHERS -> viewModel::searchTeacher
}
val placeholder = when (segment) {
PickerSegment.GROUPS -> "Поиск группы"
PickerSegment.TEACHERS -> "Поиск преподавателя"
}
val isLoading = when (segment) {
PickerSegment.GROUPS -> state.groupsLoading
PickerSegment.TEACHERS -> state.teachersLoading
}
val isEmpty = when (segment) {
PickerSegment.GROUPS -> state.groups.isEmpty()
PickerSegment.TEACHERS -> state.teachers.isEmpty()
}
val onRetry: () -> Unit = when (segment) {
PickerSegment.GROUPS -> viewModel::retryGroups
PickerSegment.TEACHERS -> viewModel::retryTeachers
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Кого ищем",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
actions = {
IconButton(onClick = {
when (segment) {
PickerSegment.GROUPS -> viewModel.refreshGroups()
PickerSegment.TEACHERS -> viewModel.refreshTeachers()
}
}) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Выберите группу или преподавателя",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth()
) {
SegmentedButton(
selected = segment == PickerSegment.GROUPS,
onClick = {
segment = PickerSegment.GROUPS
},
shape = SegmentedButtonDefaults.itemShape(
index = 0,
count = 2
),
icon = {
Icon(
painter = if (segment == PickerSegment.GROUPS)
painterResource(R.drawable.groups_24dp_fill)
else painterResource(R.drawable.groups_24dp_outline),
contentDescription = "Select group"
)
}
) {
Text("Группы")
}
SegmentedButton(
selected = segment == PickerSegment.TEACHERS,
onClick = {
segment = PickerSegment.TEACHERS
},
shape = SegmentedButtonDefaults.itemShape(
index = 1,
count = 2
),
icon = {
Icon(
painter = if (segment == PickerSegment.TEACHERS)
painterResource(R.drawable.person_24dp_fill)
else painterResource(R.drawable.person_24dp_outline),
contentDescription = "Select teacher"
)
}
) {
Text("Преподаватели")
}
}
Spacer(Modifier.padding(8.dp))
PickerSearchBar(
query = query,
onQueryChange = onSearch,
placeholder = placeholder
)
Spacer(Modifier.height(16.dp))
if (segment == PickerSegment.TEACHERS) {
val progress = state.syncProgress
if (progress != null &&
progress.total > 0 &&
progress.done < progress.total
) {
LinearProgressIndicator(
progress = {
progress.done.toFloat() /
progress.total.toFloat()
},
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Text(
text = "Кафедры: ${progress.done}/${progress.total}",
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.height(8.dp))
}
}
if (isLoading && isEmpty) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
} else if (state.error != null && isEmpty) {
Text(
text = state.error ?: "",
color = MaterialTheme.colorScheme.error
)
TextButton(onClick = onRetry) {
Text("Повторить")
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 16.dp)
) {
when (segment) {
PickerSegment.GROUPS -> {
items(
items = state.filteredGroups,
key = { it.name }
) { group ->
PickerCard(
name = group.name,
segment = segment,
onClick = {
onGroupPicked(group)
}
)
}
}
PickerSegment.TEACHERS -> {
items(
items = state.filteredTeachers,
key = { it.name }
) { teacher ->
PickerCard(
name = teacher.name,
segment = segment,
onClick = {
onTeacherPicked(teacher)
}
)
}
}
}
}
}
}
}
}
@Composable
private fun PickerCard(
modifier: Modifier = Modifier,
name: String,
segment: PickerSegment,
onClick: () -> Unit,
) {
val iconRes = when (segment) {
PickerSegment.GROUPS -> R.drawable.groups_24dp_outline
PickerSegment.TEACHERS -> R.drawable.person_24dp_outline
}
Card(
modifier = modifier
.fillMaxWidth()
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
shape = MaterialTheme.shapes.large,
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp,
pressedElevation = 4.dp
),
onClick = onClick
) {
Row (
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.small
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(iconRes),
contentDescription = "Icons",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
Text (
text = name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = "Select",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PickerSearchBar(
query: String,
onQueryChange: (String) -> Unit,
placeholder: String,
modifier: Modifier = Modifier
) {
SearchBarDefaults.InputField(
query = query,
onQueryChange = onQueryChange,
onSearch = {},
expanded = false,
onExpandedChange = {},
modifier = modifier.fillMaxWidth(),
placeholder = {
Text(placeholder)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(
onClick = { onQueryChange("") }
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Очистить поиск"
)
}
}
}
)
}
/*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerScreen(
modifier: Modifier = Modifier,
viewModel: PickerViewModel,
onGroupPicked: (Group) -> Unit,
onTeacherPicked: (Teacher) -> Unit,
) { ) {
val state by viewModel.uiState.collectAsState() val state by viewModel.uiState.collectAsState()
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) } var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
LaunchedEffect(segment) { viewModel.selectSegment(segment) } LaunchedEffect(segment) { viewModel.selectSegment(segment) }
Scaffold { innerPadding -> Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Кого ищем",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
actions = {
IconButton(onClick = { onRefresh() } ) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
}
)
}
) { innerPadding ->
Column( Column(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
@@ -333,4 +734,4 @@ private fun TeacherList(teachers: List<Teacher>, onPick: (Teacher) -> Unit) {
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant) HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
} }
} }
} }*/
@@ -201,4 +201,29 @@ class PickerViewModel(
fun retryTeachers() { fun retryTeachers() {
syncAndObserveTeachers() syncAndObserveTeachers()
} }
// Добавил обновление списка преподов
fun refreshTeachers() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(
teachersLoading = true,
error = null
)
try {
withContext(Dispatchers.IO) {
teacherRepository.forceRefresh()
}
syncAndObserveTeachers()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
teachersLoading = false,
error = e.message ?: "Не удалось обновить преподавателей"
)
}
}
}
} }
@@ -1,8 +1,13 @@
package agzam.vyatsu.ui.schedule package agzam.vyatsu.ui.schedule
import agzam.vyatsu.R
import agzam.vyatsu.domain.model.SchedulePeriod
import agzam.vyatsu.ui.schedule.components.DayChips import agzam.vyatsu.ui.schedule.components.DayChips
import agzam.vyatsu.ui.schedule.components.FULL_DATE import agzam.vyatsu.ui.schedule.components.FULL_DATE
import agzam.vyatsu.ui.schedule.components.LessonSlot import agzam.vyatsu.ui.schedule.components.LessonSlot
import agzam.vyatsu.ui.schedule.components.ScheduleDateHeader
import agzam.vyatsu.ui.schedule.components.ScheduleEmptyDay
import agzam.vyatsu.ui.schedule.components.ScheduleError
import agzam.vyatsu.ui.schedule.components.WeekSelector import agzam.vyatsu.ui.schedule.components.WeekSelector
import agzam.vyatsu.ui.schedule.components.dayName import agzam.vyatsu.ui.schedule.components.dayName
import agzam.vyatsu.ui.schedule.components.hPadding import agzam.vyatsu.ui.schedule.components.hPadding
@@ -17,7 +22,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@@ -28,6 +35,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -38,8 +46,226 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.time.LocalDate
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScheduleScreen(
modifier: Modifier = Modifier,
viewModel: ScheduleViewModel,
onSwitchGroup: () -> Unit
) {
val state by viewModel.uiState.collectAsState()
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Column {
Text(
text = state.groupName ?: "Расписание",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "Расписание занятий",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
actions = {
if (state.isLoading && state.schedule != null) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
}
IconButton(onClick = { viewModel.refresh() }) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
IconButton(onClick = { onSwitchGroup() }) {
Icon(
painter = painterResource(R.drawable.sync_alt_24dp),
contentDescription = "Изменить выбор"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when {
state.schedule == null && state.loadError -> {
ScheduleError(
onRetry = viewModel::refresh
)
}
state.schedule == null -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
else -> {
val schedule = state.schedule!!
val weekDays = state.weekStart
?.let { weekStart ->
schedule.periods.firstOrNull {
it.weekStart == weekStart
}
}
?.let { period ->
schedule.daysOf(period)
}
.orEmpty()
if (weekDays.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Нет расписания на эту неделю")
}
} else {
val selectedDayIndex = weekDays
.indexOfFirst { it.date == state.dayDate }
.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = selectedDayIndex,
pageCount = { weekDays.size }
)
LaunchedEffect(
state.dayDate,
weekDays
) {
val targetPage = weekDays.indexOfFirst {
it.date == state.dayDate
}
if (
targetPage >= 0 &&
targetPage != pagerState.currentPage
) {
pagerState.scrollToPage(targetPage)
}
}
LaunchedEffect(pagerState, weekDays) {
snapshotFlow {
pagerState.settledPage
}.collect { page ->
val selectedDate =
weekDays.getOrNull(page)?.date
if (
selectedDate != null &&
selectedDate != state.dayDate
) {
viewModel.selectDay(selectedDate)
}
}
}
ScheduleDateHeader(
date = state.dayDate,
todayDate = state.todayDate,
periods = schedule.periods,
selectedWeekStart = state.weekStart,
onSelectWeek = viewModel::selectWeek
)
DayChips(
days = weekDays,
selectedDate = state.dayDate,
todayDate = state.todayDate,
pagerState = pagerState,
onSelect = viewModel::selectDay
)
HorizontalDivider()
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f),
key = { page ->
weekDays[page].date.toEpochDay()
}
) { page ->
val day = weekDays.getOrNull(page)
val visibleIntervals = day?.intervals
?.withIndex()
?.filter { (_, slot) ->
slot.lessons.any { lesson ->
!lesson.subject.isNullOrBlank() ||
lesson.raw.isNotBlank()
}
}
.orEmpty()
if (visibleIntervals.isEmpty()) {
ScheduleEmptyDay()
} else {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(
top = 16.dp,
bottom = 24.dp
)
) {
items(
items = visibleIntervals,
key = { it.index }
) { (slotIndex, slot) ->
LessonSlot(
slotIndex = slotIndex,
slot = slot
)
}
}
}
}
}
}
}
}
}
}
/*
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
@@ -187,3 +413,4 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
} }
} }
} }
*/
@@ -1,10 +1,12 @@
package agzam.vyatsu.ui.schedule.components package agzam.vyatsu.ui.schedule.components
import agzam.vyatsu.domain.model.Day import agzam.vyatsu.domain.model.Day
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListLayoutInfo
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
@@ -12,9 +14,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -22,6 +23,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import java.time.LocalDate import java.time.LocalDate
@@ -90,44 +92,67 @@ internal fun DayChips(
} }
} }
} }
LazyRow(
state = listState,
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(
horizontal = hPadding,
vertical = 8.dp
)
) {
items(
items = days,
key = { it.date.toEpochDay() }
) { day ->
LazyRow(state = listState, 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 selected = selectedDate == day.date
val isTodayChip = todayDate == day.date val isToday = todayDate == day.date
FilterChip(
selected = selected, Surface(
onClick = { onSelect(day.date) }, onClick = { onSelect(day.date) },
label = { modifier = Modifier.size(
Column(horizontalAlignment = Alignment.CenterHorizontally) { width = 72.dp,
Text( height = 72.dp
dayName(day.date.dayOfWeek), ),
style = MaterialTheme.typography.labelMedium shape = RoundedCornerShape(20.dp),
) color = if (selected) {
Text( MaterialTheme.colorScheme.primaryContainer
day.date.format(CHIP_DATE), } else {
style = MaterialTheme.typography.labelSmall, MaterialTheme.colorScheme.surfaceContainerLow
color = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
}, },
modifier = Modifier.padding(vertical = 2.dp), contentColor = if (selected) {
border = if (isTodayChip && !selected) { MaterialTheme.colorScheme.onPrimaryContainer
FilterChipDefaults.filterChipBorder( } else {
borderColor = MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onSurface
borderWidth = 1.dp, },
enabled = true, border = if (isToday && !selected) {
selected = false BorderStroke(
width = 1.5.dp,
color = MaterialTheme.colorScheme.primary
) )
} else { } else {
FilterChipDefaults.filterChipBorder(enabled = true, selected = selected) null
}, }
shape = RoundedCornerShape(8.dp) ) {
) Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = dayName(day.date.dayOfWeek),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Medium
)
Text(
text = day.date.format(CHIP_DATE),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
} }
} }
} }
@@ -3,12 +3,15 @@ package agzam.vyatsu.ui.schedule.components
import agzam.vyatsu.domain.model.LessonType import agzam.vyatsu.domain.model.LessonType
import agzam.vyatsu.domain.model.TimeSlot import agzam.vyatsu.domain.model.TimeSlot
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -18,6 +21,94 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable
internal fun LessonSlot(
slotIndex: Int,
slot: TimeSlot,
showTeacher: Boolean = true
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = hPadding),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "${slotIndex + 1} пара",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "${slot.start.format(TIME)}–${slot.end.format(TIME)}",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
slot.lessons.forEach { lesson ->
val subject = lesson.subject
?.takeIf { it.isNotBlank() }
?: lesson.raw
val details = listOfNotNull(
lesson.type.takeIf { it != LessonType.OTHER }?.label,
lesson.subgroup,
if (showTeacher) lesson.teacher else lesson.group,
lesson.room
).filter { it.isNotBlank() }
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = subject,
style = MaterialTheme.typography.titleMedium
)
if (details.isNotEmpty()) {
Text(
text = details.joinToString(" · "),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
lesson.department
?.takeIf { it.isNotBlank() }
?.let { department ->
HorizontalDivider()
Text(
text = department,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
/*
@Composable @Composable
internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = true) { internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = true) {
val context = LocalContext.current val context = LocalContext.current
@@ -125,4 +216,6 @@ internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = t
} }
} }
} }
} }
*/
@@ -0,0 +1,68 @@
package agzam.vyatsu.ui.schedule.components
import agzam.vyatsu.domain.model.SchedulePeriod
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import java.time.LocalDate
@Composable
internal fun ScheduleDateHeader(
date: LocalDate?,
todayDate: LocalDate?,
periods: List<SchedulePeriod>,
selectedWeekStart: LocalDate?,
onSelectWeek: (LocalDate) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = hPadding,
vertical = 12.dp
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = if (date == todayDate) {
"Сегодня"
} else {
"Выбранный день"
},
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = date?.let {
"${dayName(it.dayOfWeek)} · ${it.format(FULL_DATE)}"
} ?: "Дата не выбрана",
style = MaterialTheme.typography.titleMedium
)
}
// WeekSelector(
// periods = periods,
// selectedWeekStart = selectedWeekStart,
// todayWeekStart = todayDate?.let { today ->
// periods.firstOrNull { period ->
// !today.isBefore(period.weekStart) &&
// !today.isAfter(period.weekEnd)
// }?.weekStart
// },
// onSelect = onSelectWeek
// )
}
}
@@ -0,0 +1,76 @@
package agzam.vyatsu.ui.schedule.components
import agzam.vyatsu.R
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@Composable
internal fun ScheduleEmptyDay() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
painter = painterResource(R.drawable.event_busy_24dp),
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp))
{
Text(
text = "Занятий нет",
style = MaterialTheme.typography.titleMedium
)
Text(
text = "На этот день расписание пустое",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
@Composable
internal fun ScheduleError(
onRetry: () -> Unit
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Не удалось загрузить расписание",
style = MaterialTheme.typography.titleMedium
)
Button(onClick = onRetry) {
Text("Повторить")
}
}
}
}
@@ -1,28 +1,299 @@
package agzam.vyatsu.ui.settings package agzam.vyatsu.ui.settings
import agzam.vyatsu.BuildConfig import agzam.vyatsu.BuildConfig
import agzam.vyatsu.R
import agzam.vyatsu.update.UpdateState import agzam.vyatsu.update.UpdateState
import android.widget.HorizontalScrollView
import androidx.compose.foundation.clickable 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.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(viewModel: UpdateViewModel) {
val autoEnabled by viewModel.autoEnabled.collectAsState()
val updateState by viewModel.updateState.collectAsState()
Scaffold(
modifier = Modifier,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Text(
text = "Настройки",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
SectionTitle("Обновления")
}
item {
SettingsGroup {
SettingsRow(
icon = painterResource(R.drawable.cached_24dp),
title = "Автоматические обновления",
subtitle = "Проверять новые версии при запуске",
trailing = {
Switch(
checked = autoEnabled,
onCheckedChange = viewModel::setAutoEnabled
)
},
onClick = {
viewModel.setAutoEnabled(!autoEnabled)
}
)
HorizontalDivider(
modifier = Modifier.padding(start = 72.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
SettingsRow(
icon = painterResource(R.drawable.download_24dp),
title = "Проверить обновления",
subtitle = updateStatusText(
updateState,
autoEnabled
),
trailing = {
Icon(
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
onClick = viewModel::checkNow
)
}
}
if (
autoEnabled &&
(updateState is UpdateState.Available ||
updateState is UpdateState.ReadyToInstall)
) {
item {
SettingsGroup {
SettingsRow(
icon = painterResource(R.drawable.download_done_24dp),
title = "Установить обновление",
subtitle = installHint(
updateState,
viewModel
),
onClick = {
when (updateState) {
is UpdateState.Available ->
viewModel.downloadAndInstall()
is UpdateState.ReadyToInstall ->
viewModel.installDownloadedApk()
else -> Unit
}
}
)
}
}
}
item {
SectionTitle("О приложении")
}
item {
SettingsGroup {
SettingsRow(
icon = painterResource(R.drawable.info_24dp),
title = "Версия приложения",
subtitle = "Текущая установленная версия",
trailing = {
Text(
text = BuildConfig.VERSION_NAME,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
}
}
}
}
@Composable
private fun SectionTitle(
title: String
) {
Text(
text = title,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 4.dp)
)
}
@Composable
private fun SettingsGroup(
content: @Composable ColumnScope.() -> Unit
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceContainerLow
) {
Column(content = content)
}
}
@Composable
private fun SettingsRow(
icon: Painter,
title: String,
subtitle: String? = null,
trailing: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)? = null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
modifier = Modifier.size(44.dp),
shape = RoundedCornerShape(14.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
painter = icon,
contentDescription = null
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
if (subtitle != null) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
trailing?.invoke()
}
}
private fun updateStatusText(state: UpdateState, autoEnabled: Boolean): String = when (state) {
is UpdateState.Idle -> "Проверить сейчас"
is UpdateState.Checking -> "Проверка…"
is UpdateState.UpToDate -> "Установлена последняя версия"
is UpdateState.Available ->
if (autoEnabled) "Доступна версия ${state.version}"
else "Доступна версия ${state.version} — включите автообновления для установки"
is UpdateState.Downloading -> "Скачивание: ${state.progress?.let { "$it%" } ?: "…"}"
is UpdateState.ReadyToInstall -> "Обновление скачано, нажмите для установки"
is UpdateState.Failed -> "Ошибка: ${state.message}"
}
private fun installHint(state: UpdateState, viewModel: UpdateViewModel): String = when {
state is UpdateState.ReadyToInstall && !viewModel.canRequestInstalls() ->
"Разрешите установку приложений из этого источника"
else -> "Нажмите, чтобы продолжить"
}
@Composable
private fun SectionHeader(title: String) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
/*
@Composable @Composable
fun SettingsScreen(viewModel: UpdateViewModel) { fun SettingsScreen(viewModel: UpdateViewModel) {
val autoEnabled by viewModel.autoEnabled.collectAsState() val autoEnabled by viewModel.autoEnabled.collectAsState()
@@ -87,36 +358,10 @@ fun SettingsScreen(viewModel: UpdateViewModel) {
SettingsDivider() SettingsDivider()
} }
} }
} }*/
private fun updateStatusText(state: UpdateState, autoEnabled: Boolean): String = when (state) {
is UpdateState.Idle -> "Проверить сейчас"
is UpdateState.Checking -> "Проверка…"
is UpdateState.UpToDate -> "Установлена последняя версия"
is UpdateState.Available ->
if (autoEnabled) "Доступна версия ${state.version}"
else "Доступна версия ${state.version} — включите автообновления для установки"
is UpdateState.Downloading -> "Скачивание: ${state.progress?.let { "$it%" } ?: "…"}"
is UpdateState.ReadyToInstall -> "Обновление скачано, нажмите для установки"
is UpdateState.Failed -> "Ошибка: ${state.message}"
}
private fun installHint(state: UpdateState, viewModel: UpdateViewModel): String = when {
state is UpdateState.ReadyToInstall && !viewModel.canRequestInstalls() ->
"Разрешите установку приложений из этого источника"
else -> "Нажмите, чтобы продолжить"
}
@Composable
private fun SectionHeader(title: String) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
/*
@Composable @Composable
private fun SettingsItem( private fun SettingsItem(
title: String, title: String,
@@ -150,3 +395,5 @@ private fun SettingsItem(
private fun SettingsDivider() { private fun SettingsDivider() {
HorizontalDivider(Modifier.padding(horizontal = 16.dp)) HorizontalDivider(Modifier.padding(horizontal = 16.dp))
} }
*/
@@ -1,35 +1,31 @@
package agzam.vyatsu.ui.teacher package agzam.vyatsu.ui.teacher
import agzam.vyatsu.R
import agzam.vyatsu.ui.schedule.components.DayChips import agzam.vyatsu.ui.schedule.components.DayChips
import agzam.vyatsu.ui.schedule.components.FULL_DATE
import agzam.vyatsu.ui.schedule.components.LessonSlot import agzam.vyatsu.ui.schedule.components.LessonSlot
import agzam.vyatsu.ui.schedule.components.WeekSelector import agzam.vyatsu.ui.schedule.components.ScheduleDateHeader
import agzam.vyatsu.ui.schedule.components.dayName import agzam.vyatsu.ui.schedule.components.ScheduleError
import agzam.vyatsu.ui.schedule.components.hPadding
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -38,8 +34,231 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TeacherScheduleScreen(
viewModel: TeacherScheduleViewModel,
onSwitchTeacher: () -> Unit
) {
val state by viewModel.uiState.collectAsState()
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Column {
Text(
text = state.teacherName ?: "Расписание",
style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Расписание преподавателя",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
actions = {
if (state.isLoading && state.schedule != null) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
}
IconButton(
onClick = { viewModel.refresh() }
) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
IconButton(
onClick = onSwitchTeacher
) {
Icon(
painter = painterResource(R.drawable.sync_alt_24dp),
contentDescription = "Сменить преподавателя"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when {
state.schedule == null && state.loadError -> {
ScheduleError(
onRetry = viewModel::refresh
)
}
state.schedule == null -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
else -> {
val schedule = state.schedule!!
val weekDays = schedule.days
val periods = state.weeks.ifEmpty {
listOf(schedule.period)
}
if (weekDays.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Нет расписания на эту неделю")
}
} else {
val selectedDayIndex = weekDays
.indexOfFirst { it.date == state.dayDate }
.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = selectedDayIndex,
pageCount = { weekDays.size }
)
// Синхронизация выбора дня с Pager
LaunchedEffect(
state.dayDate,
weekDays
) {
val targetPage = weekDays.indexOfFirst {
it.date == state.dayDate
}
if (
targetPage >= 0 &&
targetPage != pagerState.currentPage
) {
pagerState.scrollToPage(targetPage)
}
}
// Переключение дня свайпом
LaunchedEffect(pagerState, weekDays) {
snapshotFlow {
pagerState.settledPage
}.collect { page ->
val selectedDate =
weekDays.getOrNull(page)?.date
if (
selectedDate != null &&
selectedDate != state.dayDate
) {
viewModel.selectDay(selectedDate)
}
}
}
ScheduleDateHeader(
date = state.dayDate,
todayDate = state.todayDate,
periods = periods,
selectedWeekStart = state.weekStart,
onSelectWeek = viewModel::selectWeek
)
DayChips(
days = weekDays,
selectedDate = state.dayDate,
todayDate = state.todayDate,
pagerState = pagerState,
onSelect = viewModel::selectDay
)
HorizontalDivider()
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f),
key = { page ->
weekDays[page].date.toEpochDay()
}
) { page ->
val day = weekDays.getOrNull(page)
val visibleSlots = day?.intervals
?.withIndex()
?.filter { (_, slot) ->
slot.lessons.any { lesson ->
!lesson.subject.isNullOrBlank() ||
lesson.raw.isNotBlank()
}
}
.orEmpty()
if (visibleSlots.isEmpty()) {
ScheduleEmptyDay()
} else {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
verticalArrangement =
Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(
top = 16.dp,
bottom = 24.dp
)
) {
items(
items = visibleSlots,
key = { it.index }
) { (slotIndex, slot) ->
LessonSlot(
slotIndex = slotIndex,
slot = slot,
showTeacher = false
)
}
}
}
}
}
}
}
}
}
}
@Composable
fun ScheduleEmptyDay() {
TODO("Not yet implemented")
}
/*
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher: () -> Unit) { fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher: () -> Unit) {
@@ -185,4 +404,6 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher:
} }
} }
} }
} }
*/
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M482,800q-134,0 -228,-93t-94,-227v-7l-64,64 -56,-56 160,-160 160,160 -56,56 -64,-64v7q0,100 70.5,170T482,720q26,0 51,-6t49,-18l60,60q-38,22 -78,33t-82,11ZM760,639L600,479l56,-56 64,64v-7q0,-100 -70.5,-170T478,240q-26,0 -51,6t-49,18l-60,-60q38,-22 78,-33t82,-11q134,0 228,93t94,227v7l64,-64 56,56 -160,160Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,560q-17,0 -28.5,-11.5T440,520q0,-17 11.5,-28.5T480,480q17,0 28.5,11.5T520,520q0,17 -11.5,28.5T480,560ZM291.5,548.5Q280,537 280,520t11.5,-28.5Q303,480 320,480t28.5,11.5Q360,503 360,520t-11.5,28.5Q337,560 320,560t-28.5,-11.5ZM640,560q-17,0 -28.5,-11.5T600,520q0,-17 11.5,-28.5T640,480q17,0 28.5,11.5T680,520q0,17 -11.5,28.5T640,560ZM480,720q-17,0 -28.5,-11.5T440,680q0,-17 11.5,-28.5T480,640q17,0 28.5,11.5T520,680q0,17 -11.5,28.5T480,720ZM291.5,708.5Q280,697 280,680t11.5,-28.5Q303,640 320,640t28.5,11.5Q360,663 360,680t-11.5,28.5Q337,720 320,720t-28.5,-11.5ZM640,720q-17,0 -28.5,-11.5T600,680q0,-17 11.5,-28.5T640,640q17,0 28.5,11.5T680,680q0,17 -11.5,28.5T640,720ZM200,880q-33,0 -56.5,-23.5T120,800v-560q0,-33 23.5,-56.5T200,160h40v-80h80v80h320v-80h80v80h40q33,0 56.5,23.5T840,240v560q0,33 -23.5,56.5T760,880L200,880ZM200,800h560v-400L200,400v400Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M200,880q-33,0 -56.5,-23.5T120,800v-560q0,-33 23.5,-56.5T200,160h40v-80h80v80h320v-80h80v80h40q33,0 56.5,23.5T840,240v560q0,33 -23.5,56.5T760,880L200,880ZM200,800h560v-400L200,400v400ZM200,320h560v-80L200,240v80ZM200,320v-80,80ZM480,560q-17,0 -28.5,-11.5T440,520q0,-17 11.5,-28.5T480,480q17,0 28.5,11.5T520,520q0,17 -11.5,28.5T480,560ZM291.5,548.5Q280,537 280,520t11.5,-28.5Q303,480 320,480t28.5,11.5Q360,503 360,520t-11.5,28.5Q337,560 320,560t-28.5,-11.5ZM640,560q-17,0 -28.5,-11.5T600,520q0,-17 11.5,-28.5T640,480q17,0 28.5,11.5T680,520q0,17 -11.5,28.5T640,560ZM480,720q-17,0 -28.5,-11.5T440,680q0,-17 11.5,-28.5T480,640q17,0 28.5,11.5T520,680q0,17 -11.5,28.5T480,720ZM291.5,708.5Q280,697 280,680t11.5,-28.5Q303,640 320,640t28.5,11.5Q360,663 360,680t-11.5,28.5Q337,720 320,720t-28.5,-11.5ZM640,720q-17,0 -28.5,-11.5T600,680q0,-17 11.5,-28.5T640,640q17,0 28.5,11.5T680,680q0,17 -11.5,28.5T640,720Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M504,480 L320,296l56,-56 240,240 -240,240 -56,-56 184,-184Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,640 L280,440l56,-58 104,104v-326h80v326l104,-104 56,58 -200,200ZM240,800q-33,0 -56.5,-23.5T160,720v-120h80v120h480v-120h80v120q0,33 -23.5,56.5T720,800L240,800Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M382,640 L155,413l57,-57 170,170 366,-366 57,57 -423,423ZM200,800v-80h560v80L200,800Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m388,748 l-56,-56 92,-92 -92,-92 56,-56 92,92 92,-92 56,56 -92,92 92,92 -56,56 -92,-92 -92,92ZM200,880q-33,0 -56.5,-23.5T120,800v-560q0,-33 23.5,-56.5T200,160h40v-80h80v80h320v-80h80v80h40q33,0 56.5,23.5T840,240v560q0,33 -23.5,56.5T760,880L200,880ZM200,800h560v-400L200,400v400ZM200,320h560v-80L200,240v80ZM200,320v-80,80Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M0,720v-63q0,-43 44,-70t116,-27q13,0 25,0.5t23,2.5q-14,21 -21,44t-7,48v65L0,720ZM240,720v-65q0,-32 17.5,-58.5T307,550q32,-20 76.5,-30t96.5,-10q53,0 97.5,10t76.5,30q32,20 49,46.5t17,58.5v65L240,720ZM780,720v-65q0,-26 -6.5,-49T754,563q11,-2 22.5,-2.5t23.5,-0.5q72,0 116,26.5t44,70.5v63L780,720ZM160,520q-33,0 -56.5,-23.5T80,440q0,-34 23.5,-57t56.5,-23q34,0 57,23t23,57q0,33 -23,56.5T160,520ZM800,520q-33,0 -56.5,-23.5T720,440q0,-34 23.5,-57t56.5,-23q34,0 57,23t23,57q0,33 -23,56.5T800,520ZM480,480q-50,0 -85,-35t-35,-85q0,-51 35,-85.5t85,-34.5q51,0 85.5,34.5T600,360q0,50 -34.5,85T480,480Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M0,720v-63q0,-43 44,-70t116,-27q13,0 25,0.5t23,2.5q-14,21 -21,44t-7,48v65L0,720ZM240,720v-65q0,-32 17.5,-58.5T307,550q32,-20 76.5,-30t96.5,-10q53,0 97.5,10t76.5,30q32,20 49,46.5t17,58.5v65L240,720ZM780,720v-65q0,-26 -6.5,-49T754,563q11,-2 22.5,-2.5t23.5,-0.5q72,0 116,26.5t44,70.5v63L780,720ZM325,640h311q-10,-20 -55.5,-35T480,590q-55,0 -100.5,15T325,640ZM160,520q-33,0 -56.5,-23.5T80,440q0,-34 23.5,-57t56.5,-23q34,0 57,23t23,57q0,33 -23,56.5T160,520ZM800,520q-33,0 -56.5,-23.5T720,440q0,-34 23.5,-57t56.5,-23q34,0 57,23t23,57q0,33 -23,56.5T800,520ZM480,480q-50,0 -85,-35t-35,-85q0,-51 35,-85.5t85,-34.5q51,0 85.5,34.5T600,360q0,50 -34.5,85T480,480ZM480,400q17,0 28.5,-11.5T520,360q0,-17 -11.5,-28.5T480,320q-17,0 -28.5,11.5T440,360q0,17 11.5,28.5T480,400ZM481,640ZM480,360Z"
android:fillColor="#e3e3e3"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,680h80v-240h-80v240ZM508.5,348.5Q520,337 520,320t-11.5,-28.5Q497,280 480,280t-28.5,11.5Q440,303 440,320t11.5,28.5Q463,360 480,360t28.5,-11.5ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M536.5,456.5Q560,433 560,400t-23.5,-56.5Q513,320 480,320t-56.5,23.5Q400,367 400,400t23.5,56.5Q447,480 480,480t56.5,-23.5ZM480,880Q319,743 239.5,625.5T160,408q0,-150 96.5,-239T480,80q127,0 223.5,89T800,408q0,100 -79.5,217.5T480,880Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M536.5,456.5Q560,433 560,400t-23.5,-56.5Q513,320 480,320t-56.5,23.5Q400,367 400,400t23.5,56.5Q447,480 480,480t56.5,-23.5ZM480,774q122,-112 181,-203.5T720,408q0,-109 -69.5,-178.5T480,160q-101,0 -170.5,69.5T240,408q0,71 59,162.5T480,774ZM480,880Q319,743 239.5,625.5T160,408q0,-150 96.5,-239T480,80q127,0 223.5,89T800,408q0,100 -79.5,217.5T480,880ZM480,400Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M160,760v-80h80v-280q0,-83 50,-147.5T420,168v-28q0,-25 17.5,-42.5T480,80q25,0 42.5,17.5T540,140v28q80,20 130,84.5T720,400v280h80v80L160,760ZM480,880q-33,0 -56.5,-23.5T400,800h160q0,33 -23.5,56.5T480,880ZM80,400q0,-100 44.5,-183.5T244,78l47,64q-60,44 -95.5,111T160,400L80,400ZM800,400q0,-80 -35.5,-147T669,142l47,-64q75,55 119.5,138.5T880,400h-80Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M80,400q0,-100 44.5,-183.5T244,78l47,64q-60,44 -95.5,111T160,400L80,400ZM800,400q0,-80 -35.5,-147T669,142l47,-64q75,55 119.5,138.5T880,400h-80ZM160,760v-80h80v-280q0,-83 50,-147.5T420,168v-28q0,-25 17.5,-42.5T480,80q25,0 42.5,17.5T540,140v28q80,20 130,84.5T720,400v280h80v80L160,760ZM480,460ZM480,880q-33,0 -56.5,-23.5T400,800h160q0,33 -23.5,56.5T480,880ZM320,680h320v-280q0,-66 -47,-113t-113,-47q-66,0 -113,47t-47,113v280Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M367,433q-47,-47 -47,-113t47,-113q47,-47 113,-47t113,47q47,47 47,113t-47,113q-47,47 -113,47t-113,-47ZM160,800v-112q0,-34 17.5,-62.5T224,582q62,-31 126,-46.5T480,520q66,0 130,15.5T736,582q29,15 46.5,43.5T800,688v112L160,800Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M367,433q-47,-47 -47,-113t47,-113q47,-47 113,-47t113,47q47,47 47,113t-47,113q-47,47 -113,47t-113,-47ZM160,800v-112q0,-34 17.5,-62.5T224,582q62,-31 126,-46.5T480,520q66,0 130,15.5T736,582q29,15 46.5,43.5T800,688v112L160,800ZM240,720h480v-32q0,-11 -5.5,-20T700,654q-54,-27 -109,-40.5T480,600q-56,0 -111,13.5T260,654q-9,5 -14.5,14t-5.5,20v32ZM536.5,376.5Q560,353 560,320t-23.5,-56.5Q513,240 480,240t-56.5,23.5Q400,287 400,320t23.5,56.5Q447,400 480,400t56.5,-23.5ZM480,320ZM480,720Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,800q-134,0 -227,-93t-93,-227q0,-134 93,-227t227,-93q69,0 132,28.5T720,270v-110h80v280L520,440v-80h168q-32,-56 -87.5,-88T480,240q-100,0 -170,70t-70,170q0,100 70,170t170,70q77,0 139,-44t87,-116h84q-28,106 -114,173t-196,67Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m370,880 l-16,-128q-13,-5 -24.5,-12T307,725l-119,50L78,585l103,-78q-1,-7 -1,-13.5v-27q0,-6.5 1,-13.5L78,375l110,-190 119,50q11,-8 23,-15t24,-12l16,-128h220l16,128q13,5 24.5,12t22.5,15l119,-50 110,190 -103,78q1,7 1,13.5v27q0,6.5 -2,13.5l103,78 -110,190 -118,-50q-11,8 -23,15t-24,12L590,880L370,880ZM482,620q58,0 99,-41t41,-99q0,-58 -41,-99t-99,-41q-59,0 -99.5,41T342,480q0,58 40.5,99t99.5,41Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m370,880 l-16,-128q-13,-5 -24.5,-12T307,725l-119,50L78,585l103,-78q-1,-7 -1,-13.5v-27q0,-6.5 1,-13.5L78,375l110,-190 119,50q11,-8 23,-15t24,-12l16,-128h220l16,128q13,5 24.5,12t22.5,15l119,-50 110,190 -103,78q1,7 1,13.5v27q0,6.5 -2,13.5l103,78 -110,190 -118,-50q-11,8 -23,15t-24,12L590,880L370,880ZM440,800h79l14,-106q31,-8 57.5,-23.5T639,633l99,41 39,-68 -86,-65q5,-14 7,-29.5t2,-31.5q0,-16 -2,-31.5t-7,-29.5l86,-65 -39,-68 -99,42q-22,-23 -48.5,-38.5T533,266l-13,-106h-79l-14,106q-31,8 -57.5,23.5T321,327l-99,-41 -39,68 86,64q-5,15 -7,30t-2,32q0,16 2,31t7,30l-86,65 39,68 99,-42q22,23 48.5,38.5T427,694l13,106ZM482,620q58,0 99,-41t41,-99q0,-58 -41,-99t-99,-41q-59,0 -99.5,41T342,480q0,58 40.5,99t99.5,41ZM480,480Z"
android:fillColor="#e3e3e3"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M280,840 L80,640l200,-200 57,56 -104,104h607v80L233,680l104,104 -57,56ZM680,520 L623,464 727,360L120,360v-80h607L623,176l57,-56 200,200 -200,200Z"
android:fillColor="#e3e3e3"/>
</vector>