From 4fb1e3edcaef6bfabf77cdd34354e8a07b86dc7c Mon Sep 17 00:00:00 2001 From: pc2025 Date: Tue, 20 Oct 2026 20:53:52 +0300 Subject: [PATCH] New materail3 UI --- app/build.gradle.kts | 2 + .../java/agzam/vyatsu/ui/home/HomeScreen.kt | 44 +- .../agzam/vyatsu/ui/info/BuildingsScreen.kt | 151 +++++++ .../java/agzam/vyatsu/ui/info/CallsScreen.kt | 367 +++++++++++++++ .../agzam/vyatsu/ui/picker/PickerScreen.kt | 424 +++++++++++++++++- .../agzam/vyatsu/ui/picker/PickerViewModel.kt | 25 ++ .../vyatsu/ui/schedule/ScheduleScreen.kt | 226 ++++++++++ .../vyatsu/ui/schedule/components/DayChips.kt | 92 ++-- .../ui/schedule/components/LessonSlot.kt | 95 +++- .../schedule/components/ScheduleDateHeader.kt | 68 +++ .../ui/schedule/components/ScheduleStates.kt | 73 +++ .../vyatsu/ui/settings/SettingsScreen.kt | 303 +++++++++++-- .../ui/teacher/TeacherScheduleScreen.kt | 233 +++++++++- app/src/main/res/drawable/cached_24dp.xml | 9 + .../res/drawable/calendar_month_24dp_fill.xml | 9 + .../drawable/calendar_month_24dp_outline.xml | 9 + .../main/res/drawable/chevron_right_24dp.xml | 9 + app/src/main/res/drawable/download_24dp.xml | 9 + .../main/res/drawable/download_done_24dp.xml | 9 + app/src/main/res/drawable/event_busy_24dp.xml | 9 + .../main/res/drawable/groups_24dp_fill.xml | 9 + .../main/res/drawable/groups_24dp_outline.xml | 9 + app/src/main/res/drawable/info_24dp.xml | 9 + .../res/drawable/location_on_24dp_fill.xml | 9 + .../res/drawable/location_on_24dp_outline.xml | 9 + .../notifications_active_24dp_fill.xml | 9 + .../drawable/notifications_active_outline.xml | 9 + .../main/res/drawable/person_24dp_fill.xml | 9 + .../main/res/drawable/person_24dp_outline.xml | 9 + app/src/main/res/drawable/refresh_24dp.xml | 9 + .../main/res/drawable/settings_24dp_fill.xml | 9 + .../res/drawable/settings_24dp_outline.xml | 9 + app/src/main/res/drawable/sync_alt_24dp.xml | 9 + 33 files changed, 2198 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleDateHeader.kt create mode 100644 app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleStates.kt create mode 100644 app/src/main/res/drawable/cached_24dp.xml create mode 100644 app/src/main/res/drawable/calendar_month_24dp_fill.xml create mode 100644 app/src/main/res/drawable/calendar_month_24dp_outline.xml create mode 100644 app/src/main/res/drawable/chevron_right_24dp.xml create mode 100644 app/src/main/res/drawable/download_24dp.xml create mode 100644 app/src/main/res/drawable/download_done_24dp.xml create mode 100644 app/src/main/res/drawable/event_busy_24dp.xml create mode 100644 app/src/main/res/drawable/groups_24dp_fill.xml create mode 100644 app/src/main/res/drawable/groups_24dp_outline.xml create mode 100644 app/src/main/res/drawable/info_24dp.xml create mode 100644 app/src/main/res/drawable/location_on_24dp_fill.xml create mode 100644 app/src/main/res/drawable/location_on_24dp_outline.xml create mode 100644 app/src/main/res/drawable/notifications_active_24dp_fill.xml create mode 100644 app/src/main/res/drawable/notifications_active_outline.xml create mode 100644 app/src/main/res/drawable/person_24dp_fill.xml create mode 100644 app/src/main/res/drawable/person_24dp_outline.xml create mode 100644 app/src/main/res/drawable/refresh_24dp.xml create mode 100644 app/src/main/res/drawable/settings_24dp_fill.xml create mode 100644 app/src/main/res/drawable/settings_24dp_outline.xml create mode 100644 app/src/main/res/drawable/sync_alt_24dp.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1fbec63..d296908 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,4 +117,6 @@ dependencies { androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) + + } diff --git a/app/src/main/java/agzam/vyatsu/ui/home/HomeScreen.kt b/app/src/main/java/agzam/vyatsu/ui/home/HomeScreen.kt index fe84eb8..4549670 100644 --- a/app/src/main/java/agzam/vyatsu/ui/home/HomeScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/home/HomeScreen.kt @@ -1,5 +1,6 @@ package agzam.vyatsu.ui.home +import agzam.vyatsu.R import agzam.vyatsu.ui.info.BuildingsScreen import agzam.vyatsu.ui.info.CallsScreen 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.teacher.TeacherScheduleScreen import agzam.vyatsu.ui.teacher.TeacherScheduleViewModel +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -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.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -27,6 +28,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue 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) { SCHEDULE("Расписание"), @@ -35,8 +38,12 @@ private enum class HomeTab(val label: String) { SETTINGS("Настройки") } + + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen( + modifier: Modifier = Modifier, scheduleViewModel: ScheduleViewModel, teacherScheduleViewModel: TeacherScheduleViewModel, updateViewModel: UpdateViewModel, @@ -47,19 +54,32 @@ fun HomeScreen( val teacherState by teacherScheduleViewModel.uiState.collectAsState() Scaffold( + modifier = Modifier, bottomBar = { - NavigationBar { + NavigationBar { // Новое нижнее меню HomeTab.entries.forEach { t -> NavigationBarItem( selected = tab == t, onClick = { tab = t }, icon = { Icon( - imageVector = when (t) { - HomeTab.SCHEDULE -> Icons.Default.DateRange - HomeTab.CALLS -> Icons.Default.Info - HomeTab.BUILDINGS -> Icons.Default.Home - HomeTab.SETTINGS -> Icons.Default.Settings + painter = when (t) { + HomeTab.SCHEDULE -> { + if (tab == t) painterResource(R.drawable.calendar_month_24dp_fill) + else painterResource(R.drawable.calendar_month_24dp_outline) + } + 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 ) @@ -70,7 +90,7 @@ fun HomeScreen( } } ) { innerPadding -> - androidx.compose.foundation.layout.Box( + Box( Modifier .fillMaxSize() .padding(innerPadding) diff --git a/app/src/main/java/agzam/vyatsu/ui/info/BuildingsScreen.kt b/app/src/main/java/agzam/vyatsu/ui/info/BuildingsScreen.kt index 3a2d9d2..e4c727d 100644 --- a/app/src/main/java/agzam/vyatsu/ui/info/BuildingsScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/info/BuildingsScreen.kt @@ -23,7 +23,156 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp 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 fun BuildingsScreen() { val context = LocalContext.current @@ -56,3 +205,5 @@ fun BuildingsScreen() { } } } + + */ diff --git a/app/src/main/java/agzam/vyatsu/ui/info/CallsScreen.kt b/app/src/main/java/agzam/vyatsu/ui/info/CallsScreen.kt index afd2103..67d9eb7 100644 --- a/app/src/main/java/agzam/vyatsu/ui/info/CallsScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/info/CallsScreen.kt @@ -1,21 +1,386 @@ package agzam.vyatsu.ui.info 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.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items 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.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar 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.text.font.FontWeight 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 +import kotlin.time.Duration.Companion.milliseconds + + +@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, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + 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, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (isCurrent) { + LinearProgressIndicator( + progress = { progress }, + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceContainerHighest + ) + } + } + } +} + +@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 fun CallsScreen() { LazyColumn( @@ -40,3 +405,5 @@ fun CallsScreen() { } } } + + */ \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt index e995598..a5f05cd 100644 --- a/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerScreen.kt @@ -1,29 +1,32 @@ package agzam.vyatsu.ui.picker +import agzam.vyatsu.R import agzam.vyatsu.domain.model.Group 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.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator @@ -36,30 +39,429 @@ import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment 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 + + @OptIn(ExperimentalMaterial3Api::class) @Composable fun PickerScreen( + modifier: Modifier = Modifier, viewModel: PickerViewModel, 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() var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) } 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( Modifier .fillMaxSize() @@ -333,4 +735,4 @@ private fun TeacherList(teachers: List, onPick: (Teacher) -> Unit) { HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant) } } -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt index 6a1ba6d..d979924 100644 --- a/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt +++ b/app/src/main/java/agzam/vyatsu/ui/picker/PickerViewModel.kt @@ -201,4 +201,29 @@ class PickerViewModel( fun retryTeachers() { 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 ?: "Не удалось обновить преподавателей" + ) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt b/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt index cab661e..d388ffe 100644 --- a/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/schedule/ScheduleScreen.kt @@ -1,8 +1,13 @@ 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.FULL_DATE 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.dayName 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.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -28,6 +35,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -38,8 +46,225 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource 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) + + if (day == null || + day.intervals.all { it.lessons.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 = day.intervals.withIndex() + .filter { (_, slot) -> + slot.lessons.any { lesson -> + !lesson.subject.isNullOrBlank() || + lesson.raw.isNotBlank() + } + }, + key = { it.index } + ) { (slotIndex, slot) -> + LessonSlot( + slotIndex = slotIndex, + slot = slot + ) + } + } + } + } + } + } + } + } + } +} + + + + + +/* @OptIn(ExperimentalMaterial3Api::class) @Composable fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { @@ -187,3 +412,4 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) { } } } +*/ \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/schedule/components/DayChips.kt b/app/src/main/java/agzam/vyatsu/ui/schedule/components/DayChips.kt index 5513908..9f5f59e 100644 --- a/app/src/main/java/agzam/vyatsu/ui/schedule/components/DayChips.kt +++ b/app/src/main/java/agzam/vyatsu/ui/schedule/components/DayChips.kt @@ -1,10 +1,13 @@ package agzam.vyatsu.ui.schedule.components import agzam.vyatsu.domain.model.Day +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyRow @@ -15,6 +18,7 @@ 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.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -22,6 +26,7 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.collectLatest import java.time.LocalDate @@ -90,44 +95,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 isTodayChip = todayDate == day.date - FilterChip( - selected = selected, + val isToday = todayDate == day.date + + Surface( onClick = { onSelect(day.date) }, - label = { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - dayName(day.date.dayOfWeek), - style = MaterialTheme.typography.labelMedium - ) - Text( - day.date.format(CHIP_DATE), - style = MaterialTheme.typography.labelSmall, - color = if (selected) { - MaterialTheme.colorScheme.onSecondaryContainer - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } + modifier = Modifier.size( + width = 72.dp, + height = 72.dp + ), + shape = RoundedCornerShape(20.dp), + color = if (selected) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceContainerLow }, - modifier = Modifier.padding(vertical = 2.dp), - border = if (isTodayChip && !selected) { - FilterChipDefaults.filterChipBorder( - borderColor = MaterialTheme.colorScheme.primary, - borderWidth = 1.dp, - enabled = true, - selected = false + contentColor = if (selected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + }, + border = if (isToday && !selected) { + BorderStroke( + width = 1.5.dp, + color = MaterialTheme.colorScheme.primary ) } else { - FilterChipDefaults.filterChipBorder(enabled = true, selected = selected) - }, - shape = RoundedCornerShape(8.dp) - ) + null + } + ) { + 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 + ) + } + } } } + } \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/schedule/components/LessonSlot.kt b/app/src/main/java/agzam/vyatsu/ui/schedule/components/LessonSlot.kt index 62fa24d..d3a04eb 100644 --- a/app/src/main/java/agzam/vyatsu/ui/schedule/components/LessonSlot.kt +++ b/app/src/main/java/agzam/vyatsu/ui/schedule/components/LessonSlot.kt @@ -3,12 +3,15 @@ package agzam.vyatsu.ui.schedule.components import agzam.vyatsu.domain.model.LessonType import agzam.vyatsu.domain.model.TimeSlot import androidx.compose.foundation.background +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.foundation.layout.width 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.MaterialTheme import androidx.compose.material3.Text @@ -18,6 +21,94 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext 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 internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = true) { val context = LocalContext.current @@ -125,4 +216,6 @@ internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = t } } } -} \ No newline at end of file +} + + */ \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleDateHeader.kt b/app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleDateHeader.kt new file mode 100644 index 0000000..bb934f6 --- /dev/null +++ b/app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleDateHeader.kt @@ -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, + 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 +// ) + } +} \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleStates.kt b/app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleStates.kt new file mode 100644 index 0000000..9a4287b --- /dev/null +++ b/app/src/main/java/agzam/vyatsu/ui/schedule/components/ScheduleStates.kt @@ -0,0 +1,73 @@ +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.material.icons.Icons +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(12.dp) + ) { + Icon( + painter = painterResource(R.drawable.event_busy_24dp), + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + 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("Повторить") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/agzam/vyatsu/ui/settings/SettingsScreen.kt b/app/src/main/java/agzam/vyatsu/ui/settings/SettingsScreen.kt index 0c47d7f..066070a 100644 --- a/app/src/main/java/agzam/vyatsu/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/settings/SettingsScreen.kt @@ -1,28 +1,299 @@ package agzam.vyatsu.ui.settings import agzam.vyatsu.BuildConfig +import agzam.vyatsu.R import agzam.vyatsu.update.UpdateState +import android.widget.HorizontalScrollView 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.ColumnScope 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.heightIn import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsPadding 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.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment 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 + +@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 fun SettingsScreen(viewModel: UpdateViewModel) { val autoEnabled by viewModel.autoEnabled.collectAsState() @@ -87,36 +358,10 @@ fun SettingsScreen(viewModel: UpdateViewModel) { 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 private fun SettingsItem( title: String, @@ -150,3 +395,5 @@ private fun SettingsItem( private fun SettingsDivider() { HorizontalDivider(Modifier.padding(horizontal = 16.dp)) } + + */ diff --git a/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleScreen.kt b/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleScreen.kt index 402091d..00cba78 100644 --- a/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleScreen.kt +++ b/app/src/main/java/agzam/vyatsu/ui/teacher/TeacherScheduleScreen.kt @@ -1,8 +1,11 @@ package agzam.vyatsu.ui.teacher +import agzam.vyatsu.R 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.ScheduleDateHeader +import agzam.vyatsu.ui.schedule.components.ScheduleError import agzam.vyatsu.ui.schedule.components.WeekSelector import agzam.vyatsu.ui.schedule.components.dayName import agzam.vyatsu.ui.schedule.components.hPadding @@ -17,7 +20,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -28,6 +33,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -38,8 +44,231 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow 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) @Composable fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher: () -> Unit) { @@ -185,4 +414,6 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher: } } } -} \ No newline at end of file +} + + */ \ No newline at end of file diff --git a/app/src/main/res/drawable/cached_24dp.xml b/app/src/main/res/drawable/cached_24dp.xml new file mode 100644 index 0000000..6bb671c --- /dev/null +++ b/app/src/main/res/drawable/cached_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/calendar_month_24dp_fill.xml b/app/src/main/res/drawable/calendar_month_24dp_fill.xml new file mode 100644 index 0000000..0a7f2d2 --- /dev/null +++ b/app/src/main/res/drawable/calendar_month_24dp_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/calendar_month_24dp_outline.xml b/app/src/main/res/drawable/calendar_month_24dp_outline.xml new file mode 100644 index 0000000..45b2062 --- /dev/null +++ b/app/src/main/res/drawable/calendar_month_24dp_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/chevron_right_24dp.xml b/app/src/main/res/drawable/chevron_right_24dp.xml new file mode 100644 index 0000000..5b9810c --- /dev/null +++ b/app/src/main/res/drawable/chevron_right_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/download_24dp.xml b/app/src/main/res/drawable/download_24dp.xml new file mode 100644 index 0000000..d5e89d9 --- /dev/null +++ b/app/src/main/res/drawable/download_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/download_done_24dp.xml b/app/src/main/res/drawable/download_done_24dp.xml new file mode 100644 index 0000000..4cfe953 --- /dev/null +++ b/app/src/main/res/drawable/download_done_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/event_busy_24dp.xml b/app/src/main/res/drawable/event_busy_24dp.xml new file mode 100644 index 0000000..7227bf0 --- /dev/null +++ b/app/src/main/res/drawable/event_busy_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/groups_24dp_fill.xml b/app/src/main/res/drawable/groups_24dp_fill.xml new file mode 100644 index 0000000..a04e4a4 --- /dev/null +++ b/app/src/main/res/drawable/groups_24dp_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/groups_24dp_outline.xml b/app/src/main/res/drawable/groups_24dp_outline.xml new file mode 100644 index 0000000..ed88dc3 --- /dev/null +++ b/app/src/main/res/drawable/groups_24dp_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/info_24dp.xml b/app/src/main/res/drawable/info_24dp.xml new file mode 100644 index 0000000..b6f74a0 --- /dev/null +++ b/app/src/main/res/drawable/info_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/location_on_24dp_fill.xml b/app/src/main/res/drawable/location_on_24dp_fill.xml new file mode 100644 index 0000000..ccbe57f --- /dev/null +++ b/app/src/main/res/drawable/location_on_24dp_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/location_on_24dp_outline.xml b/app/src/main/res/drawable/location_on_24dp_outline.xml new file mode 100644 index 0000000..842be9a --- /dev/null +++ b/app/src/main/res/drawable/location_on_24dp_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/notifications_active_24dp_fill.xml b/app/src/main/res/drawable/notifications_active_24dp_fill.xml new file mode 100644 index 0000000..1365350 --- /dev/null +++ b/app/src/main/res/drawable/notifications_active_24dp_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/notifications_active_outline.xml b/app/src/main/res/drawable/notifications_active_outline.xml new file mode 100644 index 0000000..dc31468 --- /dev/null +++ b/app/src/main/res/drawable/notifications_active_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/person_24dp_fill.xml b/app/src/main/res/drawable/person_24dp_fill.xml new file mode 100644 index 0000000..85ef185 --- /dev/null +++ b/app/src/main/res/drawable/person_24dp_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/person_24dp_outline.xml b/app/src/main/res/drawable/person_24dp_outline.xml new file mode 100644 index 0000000..7907fb6 --- /dev/null +++ b/app/src/main/res/drawable/person_24dp_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/refresh_24dp.xml b/app/src/main/res/drawable/refresh_24dp.xml new file mode 100644 index 0000000..e61dcf1 --- /dev/null +++ b/app/src/main/res/drawable/refresh_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/settings_24dp_fill.xml b/app/src/main/res/drawable/settings_24dp_fill.xml new file mode 100644 index 0000000..05e2b1b --- /dev/null +++ b/app/src/main/res/drawable/settings_24dp_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/settings_24dp_outline.xml b/app/src/main/res/drawable/settings_24dp_outline.xml new file mode 100644 index 0000000..dae79dc --- /dev/null +++ b/app/src/main/res/drawable/settings_24dp_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/sync_alt_24dp.xml b/app/src/main/res/drawable/sync_alt_24dp.xml new file mode 100644 index 0000000..f33a486 --- /dev/null +++ b/app/src/main/res/drawable/sync_alt_24dp.xml @@ -0,0 +1,9 @@ + + + -- 2.47.3