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
vantablackTea 84da62c45d optimisation: baked jdk and sdk into runner 2026-10-08 16:56:27 +03:00
Agzamus 57e5bd9af8 feat: department display 2026-10-08 02:10:26 +03:00
Agzamus c7298c0c84 fix: wrong parsed teacher schedule 2026-10-08 01:42:26 +03:00
Agzamus ae0fea1ea6 fix: ssl error & offline teacher schedule 2026-10-07 21:55:53 +03:00
Agzamus af1f374b95 Обновить README.md 2026-09-20 17:42:05 +03:00
48 changed files with 5656 additions and 180 deletions
+2 -33
View File
@@ -18,50 +18,19 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
releases: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up JDK 17 - name: Verify Environment
# Ставим JDK вручную и явно прописываем JAVA_HOME.
# actions/setup-java не всегда передаёт JAVA_HOME дальше в Gitea Actions.
run: | run: |
set -euxo pipefail
cd /tmp
wget -q https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.12%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz
tar -xzf OpenJDK17U-jdk_x64_linux_hotspot_17.0.12_7.tar.gz -C /opt
mv /opt/jdk-17.0.12+7 /opt/java17
export JAVA_HOME=/opt/java17
export PATH=$JAVA_HOME/bin:$PATH
echo "JAVA_HOME=/opt/java17" >> $GITHUB_ENV
echo "$JAVA_HOME/bin" >> $GITHUB_PATH
java -version java -version
javac -version echo "SDK path: $ANDROID_HOME"
- name: Install Android SDK
# Устанавливаем command-line tools и нужные компоненты SDK,
# затем прописываем ANDROID_HOME (читается Gradle/AGP).
run: |
set -euxo pipefail
echo "ANDROID_HOME=/opt/android-sdk" >> "$GITHUB_ENV"
mkdir -p /opt/android-sdk/cmdline-tools
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/clt.zip
unzip -q /tmp/clt.zip -d /tmp/clt
mv /tmp/clt/cmdline-tools /opt/android-sdk/cmdline-tools/latest
yes | /opt/android-sdk/cmdline-tools/latest/bin/sdkmanager --licenses >/dev/null 2>&1 || true
/opt/android-sdk/cmdline-tools/latest/bin/sdkmanager \
"platform-tools" \
"platforms;android-36.1" \
"build-tools;36.1.0"
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: | run: |
chmod +x gradlew chmod +x gradlew
java -version
# Версия по дате (ГГГГ.ММ.ДД), при совпадении тега добавляем уникальный суффикс. # Версия по дате (ГГГГ.ММ.ДД), при совпадении тега добавляем уникальный суффикс.
- name: Generate version from date - name: Generate version from date
+7 -1
View File
@@ -1,2 +1,8 @@
# vyatsu-schedule # ВятГУ расписание _(неофициальный Android клиент)_
Расписание загружается и парсится прямиком с сайта ВятГУ, и сохраняется в локальную БД телефона без промежуточных серверов
- Расписание групп - парсится из PDF-ок ([тута](https://www.vyatsu.ru/studentu-1/spravochnaya-informatsiya/raspisanie-zanyatiy-dlya-studentov.html))
- Расписание преподавателей - загружаются все расписания преподавателей [отсюда](https://www.vyatsu.ru/studentu-1/spravochnaya-informatsiya/teacher.html) на выбранный переиод (ибо раскиданы по кафедрам)
+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
View File
@@ -12,6 +12,7 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:label="@string/app_name" android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.VyatsuSchedule"> android:theme="@style/Theme.VyatsuSchedule">
@@ -0,0 +1,10 @@
package agzam.vyatsu.data.local
import androidx.room.Embedded
/** Занятие преподавателя + кафедра-источник. Проекция LEFT JOIN, схема Room не меняется. */
data class TeacherLessonWithDepartment(
@Embedded val lesson: TeacherLessonEntity,
val departmentId: String,
val departmentName: String?
)
@@ -33,6 +33,9 @@ interface TeacherScheduleDao {
@Query("DELETE FROM teacher_schedules") @Query("DELETE FROM teacher_schedules")
suspend fun deleteAll() suspend fun deleteAll()
@Query("SELECT lastSyncedAt FROM teacher_schedules WHERE departmentId = :departmentId AND season = :season AND weekStart = :weekStart")
suspend fun lastSyncedAt(departmentId: String, season: String, weekStart: Long): Long?
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertDays(days: List<TeacherDayEntity>): List<Long> suspend fun insertDays(days: List<TeacherDayEntity>): List<Long>
@@ -41,6 +44,7 @@ interface TeacherScheduleDao {
@Transaction @Transaction
suspend fun replaceSchedule(schedule: TeacherScheduleEntity, days: List<TeacherDayEntity>, lessons: List<TeacherLessonEntity>) { suspend fun replaceSchedule(schedule: TeacherScheduleEntity, days: List<TeacherDayEntity>, lessons: List<TeacherLessonEntity>) {
deleteSchedule(schedule.departmentId, schedule.season.name, schedule.weekStart)
val scheduleId = insertSchedule(schedule) val scheduleId = insertSchedule(schedule)
val daysWithScheduleId = days.map { it.copy(scheduleId = scheduleId) } val daysWithScheduleId = days.map { it.copy(scheduleId = scheduleId) }
deleteDaysForSchedule(scheduleId) deleteDaysForSchedule(scheduleId)
@@ -57,13 +61,15 @@ interface TeacherScheduleDao {
fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>> fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>>
@Query( @Query(
"SELECT teacher_lessons.* FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " + "SELECT teacher_lessons.*, teacher_schedules.departmentId AS departmentId, departments.name AS departmentName " +
"FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
"INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " + "INNER JOIN teacher_schedules ON teacher_schedules.id = teacher_days.scheduleId " +
"LEFT JOIN departments ON departments.departmentId = teacher_schedules.departmentId " +
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " + "WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
"AND teacher_schedules.weekEnd >= :weekStart " + "AND teacher_schedules.weekEnd >= :weekStart " +
"ORDER BY teacher_lessons.date, teacher_lessons.slotIndex, teacher_lessons.lessonIndex" "ORDER BY teacher_lessons.date, teacher_lessons.slotIndex, teacher_lessons.lessonIndex"
) )
fun observeLessonsForPeriod(season: String, weekStart: Long): Flow<List<TeacherLessonEntity>> fun observeLessonsForPeriod(season: String, weekStart: Long): Flow<List<TeacherLessonWithDepartment>>
@Query( @Query(
"SELECT DISTINCT teacher FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " + "SELECT DISTINCT teacher FROM teacher_lessons INNER JOIN teacher_days ON teacher_days.id = teacher_lessons.dayId " +
@@ -23,9 +23,18 @@ object TeacherScheduleParser {
data class ParsedTeacherSchedule(val teachers: List<String>, val days: List<Day>) data class ParsedTeacherSchedule(val teachers: List<String>, val days: List<Day>)
/**
* Столбец расписания преподавателя: имя и абсолютный индекс ячейки в шапке.
* Индекс нужен для точного выравнивания с рядами данных — в шапке могут
* присутствовать пустые колонки-разделители, которые не должны сдвигать
* соответствие «преподаватель → столбец».
*/
private data class Column(val teacher: String, val headerIndex: Int)
fun parse(html: String): ParsedTeacherSchedule { fun parse(html: String): ParsedTeacherSchedule {
val doc = Jsoup.parse(html) val doc = Jsoup.parse(html)
var headerFound = false var headerFound = false
var columns: List<Column> = emptyList()
var teachers: List<String> = emptyList() var teachers: List<String> = emptyList()
val days = mutableListOf<Day>() val days = mutableListOf<Day>()
var currentDate: LocalDate? = null var currentDate: LocalDate? = null
@@ -43,10 +52,12 @@ object TeacherScheduleParser {
val first = cellText(tds[0]) val first = cellText(tds[0])
val second = cellText(tds[1]) val second = cellText(tds[1])
if (!headerFound && first == "День" && second == "Интервал") { if (!headerFound && first == "День" && second == "Интервал") {
teachers = tds.drop(2) columns = tds.mapIndexedNotNull { index, td ->
.map { cellText(it.selectFirst("span") ?: it) } if (index < 2) return@mapIndexedNotNull null
.filter { it.isNotEmpty() } val name = cellText(td.selectFirst("span") ?: td)
.distinct() if (name.isEmpty()) null else Column(name, index)
}.distinctBy { it.teacher }
teachers = columns.map { it.teacher }
headerFound = true headerFound = true
continue continue
} }
@@ -58,7 +69,7 @@ object TeacherScheduleParser {
currentIntervals = mutableListOf() currentIntervals = mutableListOf()
} }
if (intervalMatch != null && currentIntervals != null) { if (intervalMatch != null && currentIntervals != null) {
parseIntervalRow(tds, intervalMatch, if (dayMatch != null) 2 else 1, teachers, currentIntervals!!) parseIntervalRow(tds, intervalMatch, dayMatch != null, columns, currentIntervals!!)
} }
} }
flushDay() flushDay()
@@ -83,16 +94,20 @@ object TeacherScheduleParser {
private fun parseIntervalRow( private fun parseIntervalRow(
tds: List<Element>, tds: List<Element>,
intervalMatch: MatchResult, intervalMatch: MatchResult,
startCol: Int, dayRow: Boolean,
teachers: List<String>, columns: List<Column>,
intervals: MutableList<TimeSlot> intervals: MutableList<TimeSlot>
) { ) {
val start = LocalTime.parse(intervalMatch.groupValues[1]) val start = LocalTime.parse(intervalMatch.groupValues[1])
val end = LocalTime.parse(intervalMatch.groupValues[2]) val end = LocalTime.parse(intervalMatch.groupValues[2])
val lessons = mutableListOf<Lesson>() val lessons = mutableListOf<Lesson>()
for (i in teachers.indices) { for (column in columns) {
val cell = tds.getOrNull(startCol + i) ?: continue // Ряд с днём содержит обе ведущие ячейки (День, Интервал) и совпадает
lessons += parseCell(cell, teachers[i]) // по индексам с шапкой; ряд только с интервалом пропускает ячейку дня
// (rowspan) — его ячейки сдвинуты на одну позицию влево.
val dataIndex = if (dayRow) column.headerIndex else column.headerIndex - 1
val cell = tds.getOrNull(dataIndex) ?: continue
lessons += parseCell(cell, column.teacher)
} }
if (lessons.isNotEmpty()) intervals.add(TimeSlot(start, end, lessons)) if (lessons.isNotEmpty()) intervals.add(TimeSlot(start, end, lessons))
} }
@@ -4,6 +4,7 @@ import agzam.vyatsu.data.local.DepartmentPeriodEntity
import agzam.vyatsu.data.local.TeacherDao import agzam.vyatsu.data.local.TeacherDao
import agzam.vyatsu.data.local.TeacherDayEntity import agzam.vyatsu.data.local.TeacherDayEntity
import agzam.vyatsu.data.local.TeacherLessonEntity import agzam.vyatsu.data.local.TeacherLessonEntity
import agzam.vyatsu.data.local.TeacherLessonWithDepartment
import agzam.vyatsu.data.local.TeacherScheduleDao import agzam.vyatsu.data.local.TeacherScheduleDao
import agzam.vyatsu.data.local.TeacherScheduleEntity import agzam.vyatsu.data.local.TeacherScheduleEntity
import agzam.vyatsu.data.remote.TeacherScheduleParser import agzam.vyatsu.data.remote.TeacherScheduleParser
@@ -56,7 +57,7 @@ class TeacherScheduleRepositoryImpl(
scheduleDao.observeDistinctTeachers(period.season.name, period.weekStart.toEpochDay()) scheduleDao.observeDistinctTeachers(period.season.name, period.weekStart.toEpochDay())
.map { names -> names.map { name -> Teacher(name = name) } } .map { names -> names.map { name -> Teacher(name = name) } }
override suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress> = flow { override suspend fun syncPeriod(date: LocalDate, force: Boolean): Flow<BulkSyncProgress> = flow {
// Unique index (departmentId, season, weekStart) breaks on interleaved // Unique index (departmentId, season, weekStart) breaks on interleaved
// delete+insert from concurrent syncPeriod() callers. // delete+insert from concurrent syncPeriod() callers.
syncMutex.withLock { syncMutex.withLock {
@@ -68,18 +69,27 @@ class TeacherScheduleRepositoryImpl(
return@withLock return@withLock
} }
targets.map { it.season to it.weekStart } val now = now()
.distinct() var skippedCount = 0
.forEach { (season, weekStart) -> val nonSkipped = mutableListOf<DepartmentPeriodEntity>()
withContext(Dispatchers.IO) { for (target in targets) {
scheduleDao.deletePeriod(season.name, weekStart) val last = withContext(Dispatchers.IO) {
scheduleDao.lastSyncedAt(target.departmentId, target.season.name, target.weekStart)
}
val shouldSkip = !force && last != null && now - last < STALE_WINDOW_MS
if (shouldSkip) {
skippedCount++
} else {
nonSkipped.add(target)
} }
} }
emit(BulkSyncProgress(total = targets.size, done = skippedCount, failed = 0))
val total = targets.size val total = targets.size
val semaphore = Semaphore(PARALLELISM) val semaphore = Semaphore(PARALLELISM)
val jobs = coroutineScope { val jobs = coroutineScope {
targets.map { target -> nonSkipped.map { target ->
async { async {
semaphore.withPermit { semaphore.withPermit {
try { try {
@@ -102,7 +112,7 @@ class TeacherScheduleRepositoryImpl(
} }
} }
var done = 0 var done = skippedCount
var failed = 0 var failed = 0
for (job in jobs) { for (job in jobs) {
if (job.await()) done++ else failed++ if (job.await()) done++ else failed++
@@ -229,30 +239,30 @@ class TeacherScheduleRepositoryImpl(
private fun buildTeacherSchedule( private fun buildTeacherSchedule(
period: SchedulePeriod, period: SchedulePeriod,
dayEntities: List<TeacherDayEntity>, dayEntities: List<TeacherDayEntity>,
lessonEntities: List<TeacherLessonEntity> lessons: List<TeacherLessonWithDepartment>
): TeacherSchedule? { ): TeacherSchedule? {
val weekDays = dayEntities val weekDays = dayEntities
.filter { .filter {
it.date >= period.weekStart.toEpochDay() && it.date <= period.weekEnd.toEpochDay() it.date >= period.weekStart.toEpochDay() && it.date <= period.weekEnd.toEpochDay()
} }
if (weekDays.isEmpty()) return null if (weekDays.isEmpty()) return null
val lessonsByDate = lessonEntities.groupBy { it.date } val lessonsByDate = lessons.groupBy { it.lesson.date }
val days = weekDays val days = weekDays
.sortedBy { it.date } .sortedBy { it.date }
.distinctBy { it.date } .distinctBy { it.date }
.map { day -> .map { day ->
val lessons = lessonsByDate[day.date].orEmpty() val dayLessons = lessonsByDate[day.date].orEmpty()
Log.d( Log.d(
TAG, TAG,
"build: date=${day.date} totalLessons=${lessons.size} " + "build: date=${day.date} totalLessons=${dayLessons.size} " +
"groupedByStart=${lessons.groupBy { it.start }.keys.sorted()}" "groupedByStart=${dayLessons.groupBy { it.lesson.start }.keys.sorted()}"
) )
val slots = lessons.groupBy { it.start } val slots = dayLessons.groupBy { it.lesson.start }
.toSortedMap() .toSortedMap()
.map { (startMin, slotLessons) -> .map { (startMin, slotLessons) ->
val first = slotLessons.first() val first = slotLessons.first().lesson
val times = slotLessons.map { "${it.start}-${it.end}" }.distinct() val times = slotLessons.map { "${it.lesson.start}-${it.lesson.end}" }.distinct()
val teachers = slotLessons.map { it.teacher }.distinct() val teachers = slotLessons.map { it.lesson.teacher }.distinct()
Log.d( Log.d(
TAG, TAG,
"build: date=${day.date} start=$startMin firstEnd=$first.end " + "build: date=${day.date} start=$startMin firstEnd=$first.end " +
@@ -262,7 +272,7 @@ class TeacherScheduleRepositoryImpl(
start = LocalTime.of(startMin / 60, startMin % 60), start = LocalTime.of(startMin / 60, startMin % 60),
end = LocalTime.of(first.end / 60, first.end % 60), end = LocalTime.of(first.end / 60, first.end % 60),
lessons = slotLessons lessons = slotLessons
.sortedBy { it.lessonIndex } .sortedBy { it.lesson.lessonIndex }
.map { it.toDomain() } .map { it.toDomain() }
) )
} }
@@ -271,16 +281,33 @@ class TeacherScheduleRepositoryImpl(
return TeacherSchedule(period = period, days = days) return TeacherSchedule(period = period, days = days)
} }
private fun TeacherLessonEntity.toDomain(): Lesson = Lesson( private fun TeacherLessonWithDepartment.toDomain(): Lesson = Lesson(
raw = raw, raw = lesson.raw,
room = room, room = lesson.room,
group = groupName, group = lesson.groupName,
subgroup = subgroup, subgroup = lesson.subgroup,
subject = subject, subject = lesson.subject,
type = type, type = lesson.type,
teacher = teacher teacher = lesson.teacher,
department = departmentLabel(departmentId, departmentName)
) )
// Названия, не выводимые акронимом из слов (например «топ-уровня в сфере…»)
private val DEPARTMENT_LABELS = mapOf(
"974" to "Топ ИИ"
)
private fun departmentLabel(departmentId: String, departmentName: String?): String {
DEPARTMENT_LABELS[departmentId]?.let { return it }
val name = departmentName ?: return "каф. $departmentId"
val words = name.removePrefix("Кафедра ").removePrefix("кафедра ")
.substringBefore(" (")
.split(" ")
.filter { it.isNotBlank() && it != "и" }
val acronym = words.joinToString("") { it.first().uppercase().toString() }
return acronym.ifBlank { "каф. $departmentId" }
}
private fun LocalTime.toMinutesOfDay(): Int = hour * 60 + minute private fun LocalTime.toMinutesOfDay(): Int = hour * 60 + minute
private fun now(): Long = System.currentTimeMillis() private fun now(): Long = System.currentTimeMillis()
@@ -26,4 +26,6 @@ data class Lesson(
/** Преподаватель **/ /** Преподаватель **/
val teacher: String? = null, val teacher: String? = null,
/** Кафедра-источник (только в расписании преподавателей) **/
val department: String? = null,
) )
@@ -10,7 +10,7 @@ import java.time.LocalDate
interface TeacherScheduleRepository { interface TeacherScheduleRepository {
fun observeTeachers(period: SchedulePeriod): Flow<List<Teacher>> fun observeTeachers(period: SchedulePeriod): Flow<List<Teacher>>
fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?> fun observeSchedule(period: SchedulePeriod): Flow<TeacherSchedule?>
suspend fun syncPeriod(date: LocalDate): Flow<BulkSyncProgress> suspend fun syncPeriod(date: LocalDate, force: Boolean = false): Flow<BulkSyncProgress>
suspend fun hasData(period: SchedulePeriod): Boolean suspend fun hasData(period: SchedulePeriod): Boolean
suspend fun weeksFor(date: LocalDate): List<SchedulePeriod> suspend fun weeksFor(date: LocalDate): List<SchedulePeriod>
suspend fun clearAll() suspend fun clearAll()
@@ -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)
} }
} }
} }*/
@@ -146,7 +146,15 @@ class PickerViewModel(
syncJob = viewModelScope.launch { syncJob = viewModelScope.launch {
try { try {
teacherScheduleRepository.syncPeriod(today).collect { progress -> teacherScheduleRepository.syncPeriod(today).collect { progress ->
_uiState.value = _uiState.value.copy(syncProgress = progress) _uiState.value = _uiState.value.copy(
syncProgress = progress,
error = if (progress.failed > 0) {
"Не удалось загрузить расписание: ${progress.failed} из " +
"${progress.total} кафедр. Проверьте подключение к интернету."
} else {
_uiState.value.error
}
)
} }
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
@@ -193,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),
)
Text(
day.date.format(CHIP_DATE),
style = MaterialTheme.typography.labelSmall,
color = if (selected) { color = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer MaterialTheme.colorScheme.primaryContainer
} else { } else {
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.surfaceContainerLow
}
)
}
}, },
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
@@ -70,6 +161,16 @@ internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = t
) { ) {
var any = false var any = false
if (lesson.department != null) {
Text(
lesson.department,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = hPadding)
)
any = true
}
val meta = listOfNotNull( val meta = listOfNotNull(
if (lesson.type == LessonType.OTHER) { if (lesson.type == LessonType.OTHER) {
null null
@@ -116,3 +217,5 @@ 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) {
@@ -186,3 +405,5 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher:
} }
} }
} }
*/
@@ -89,7 +89,9 @@ class TeacherScheduleViewModel(
if (!scheduleRepository.hasData(period)) { if (!scheduleRepository.hasData(period)) {
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false) _uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
try { try {
scheduleRepository.syncPeriod(period.weekStart).collect { } // force = true: hasData() already proved there is nothing cached for
// this period, so never let the staleness window skip the fetch.
scheduleRepository.syncPeriod(period.weekStart, force = true).collect { }
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
@@ -199,7 +201,7 @@ class TeacherScheduleViewModel(
try { try {
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false) _uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
try { try {
scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { } scheduleRepository.syncPeriod(ScheduleDates.moscowToday(), force = true).collect { }
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
@@ -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>
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV
BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv
b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX
BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es
CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/
r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje
2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt
bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj
K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4
12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on
ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls
eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9
vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM
BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg
JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy
gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92
CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm
OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq
JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye
qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz
nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7
DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3
QEUxeCp6
-----END CERTIFICATE-----
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Сертификат vyatsu.ru перевыпущен 05.10.2026 и теперь выдан в иерархии
GlobalSign Root R46, которая в системный trust store Android попала
только в Android 14. На устройствах с Android 13 и ниже все HTTPS-запросы
к сайту падают с "Trust anchor for certification path not found".
Добавляем этот корень как дополнительный якорь доверия ТОЛЬКО для доменов
vyatsu.ru — системные корни сохраняются, остальные сайты не затронуты.
-->
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
<cleartextTrafficPermitted>false</cleartextTrafficPermitted>
</base-config>
<domain-config>
<domain includeSubdomains="true">vyatsu.ru</domain>
<trust-anchors>
<certificates src="system" />
<certificates src="@raw/globalsign_root_r46" />
</trust-anchors>
</domain-config>
</network-security-config>
@@ -15,6 +15,14 @@ class TeacherScheduleParserTest {
checkNotNull(javaClass.getResourceAsStream("/prepod.html")) checkNotNull(javaClass.getResourceAsStream("/prepod.html"))
.readBytes().toString(Charsets.UTF_8) .readBytes().toString(Charsets.UTF_8)
private fun fixture517(): String =
checkNotNull(javaClass.getResourceAsStream("/prepod517.html"))
.readBytes().toString(Charsets.UTF_8)
private fun fixture974(): String =
checkNotNull(javaClass.getResourceAsStream("/prepod974.html"))
.readBytes().toString(Charsets.UTF_8)
@Test @Test
fun teachersHeaderParsedInOrderAndDeduped() { fun teachersHeaderParsedInOrderAndDeduped() {
val parsed = TeacherScheduleParser.parse(fixture()) val parsed = TeacherScheduleParser.parse(fixture())
@@ -83,4 +91,94 @@ class TeacherScheduleParserTest {
val total = parsed.days.sumOf { day -> day.intervals.sumOf { it.lessons.size } } val total = parsed.days.sumOf { day -> day.intervals.sumOf { it.lessons.size } }
assertEquals(11, total) assertEquals(11, total)
} }
private fun spacerColumnFixture(): String = """
<table>
<tr>
<td>День</td><td>Интервал</td><td></td>
<td><span>Марков Р.В.</span></td><td><span>Мельцов В.Ю.</span></td>
</tr>
<tr>
<td>четверг 08.10.26</td><td>08:20-09:50</td><td></td>
<td>2-206 Математика Практическое занятие УТб-2301-02-00</td>
<td>1-334 Организация параллельных вычислительных систем Лекция ИВТб-4301-05-00</td>
</tr>
<tr>
<td>10:00-11:30</td><td></td>
<td>2-206 Математика Практическое занятие УТб-2301-02-00</td>
<td>1-334 Организация параллельных вычислительных систем Лекция ИВТб-4301-05-00</td>
</tr>
</table>
""".trimIndent()
@Test
fun emptyHeaderSpacerDoesNotShiftTeacherColumns() {
val parsed = TeacherScheduleParser.parse(spacerColumnFixture())
assertEquals(listOf("Марков Р.В.", "Мельцов В.Ю."), parsed.teachers)
assertEquals(1, parsed.days.size)
assertEquals(LocalDate.of(2026, 10, 8), parsed.days[0].date)
assertEquals(2, parsed.days[0].intervals.size)
for (slot in parsed.days[0].intervals) {
val markov = slot.lessons.filter { it.teacher == "Марков Р.В." }
val meltsov = slot.lessons.filter { it.teacher == "Мельцов В.Ю." }
assertEquals(1, markov.size)
assertEquals(1, meltsov.size)
assertEquals("Математика", markov[0].subject)
assertEquals("Организация параллельных вычислительных систем", meltsov[0].subject)
}
}
@Test
fun realPage517TeacherColumnsAligned() {
val html = checkNotNull(javaClass.getResourceAsStream("/prepod517.html"))
.readBytes().toString(Charsets.UTF_8)
val parsed = TeacherScheduleParser.parse(html)
assertTrue(parsed.teachers.contains("Мельцов В.Ю."))
val day = parsed.days.first { it.date == LocalDate.of(2026, 10, 8) }
val all = day.intervals.flatMap { it.lessons }
val markov = all.filter { it.teacher == "Марков Р.В." }
val meltsov = all.filter { it.teacher == "Мельцов В.Ю." }
assertTrue("Мельцов должен иметь пары 08.10", meltsov.isNotEmpty())
assertTrue(
"У Мельцова не должно быть математики Маркова",
meltsov.none { it.subject?.contains("Математика") == true }
)
assertTrue(
"У Маркова должна остаться математика",
markov.any { it.subject?.contains("Математика") == true }
)
assertTrue(
"У Мельцова параллельные вычислительные системы",
meltsov.any { it.subject?.contains("Организация параллельных") == true }
)
}
@Test
fun realPage517MeltsovLabsSitAt1000And1145NotAt0820() {
val parsed = TeacherScheduleParser.parse(fixture517())
val day = parsed.days.first { it.date == LocalDate.of(2026, 10, 6) }
val labsAt = { time: java.time.LocalTime ->
day.intervals.firstOrNull { it.start == time }
?.lessons
?.filter { it.teacher == "Мельцов В.Ю." && it.type == LessonType.LABORATORY }
.orEmpty()
}
assertEquals(
"В 08:20 у Мельцова на странице 517 лабораторных нет",
0, labsAt(java.time.LocalTime.of(8, 20)).size
)
assertTrue(labsAt(java.time.LocalTime.of(10, 0)).isNotEmpty())
assertTrue(labsAt(java.time.LocalTime.of(11, 45)).isNotEmpty())
}
@Test
fun realPage974MeltsovHasPracticeAt0820() {
val parsed = TeacherScheduleParser.parse(fixture974())
val day = parsed.days.first { it.date == LocalDate.of(2026, 10, 6) }
val slot = day.intervals.first { it.start == java.time.LocalTime.of(8, 20) }
val mel = slot.lessons.filter { it.teacher == "Мельцов В.Ю." }
assertTrue(mel.isNotEmpty())
assertTrue("08:20 на странице 974 — практическое занятие", mel.all { it.type == LessonType.PRACTICE })
assertEquals("ИВТб-2305-06-00", mel.first().group)
}
} }
File diff suppressed because it is too large Load Diff
+980
View File
@@ -0,0 +1,980 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 5.0 Transitional//EN">
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; CHARSET=utf-8">
<TITLE></TITLE>
<STYLE TYPE="text/css">
body { background: #ffffff; margin: 0; font-family: Arial; font-size: 8pt; font-style: normal; }
tr.R0{ height: 25px; }
tr.R0 td.R0C0{ font-family: Arial; font-size: 14pt; font-style: normal; font-weight: bold; color: #000000; background-color: #ffffff; text-align: left; }
tr.R0 td.R0C1{ text-align: left; }
tr.R1{ height: 21px; }
tr.R1 td.R15C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R1 td.R15C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R1 td.R1C0{ font-family: Arial; font-size: 8pt; font-style: normal; font-weight: bold; color: #000000; background-color: #fcfaeb; text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R1 td.R1C2{ font-family: Arial; font-size: 12pt; font-style: normal; font-weight: bold; color: #000000; background-color: #fcfaeb; text-align: center; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R1 td.R9C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R1 td.R9C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R1 td.R9C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R1 td.R9C6{ text-align: left; }
tr.R16{ height: 43px; }
tr.R16 td.R16C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R16 td.R16C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R16 td.R16C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R16 td.R16C6{ text-align: left; }
tr.R17{ height: 141px; }
tr.R17 td.R17C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R17 td.R17C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R17 td.R17C6{ text-align: left; }
tr.R17 td.R58C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R18{ height: 15px; }
tr.R18 td.R18C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R18 td.R18C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R18 td.R18C6{ text-align: left; }
tr.R18 td.R22C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R18 td.R22C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R18 td.R65C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R2{ height: 30px; }
tr.R2 td.R2C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R2 td.R2C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R2 td.R2C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R2 td.R2C6{ text-align: left; }
tr.R30{ height: 16px; }
tr.R30 td.R30C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R30 td.R30C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R30 td.R30C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R30 td.R30C6{ text-align: left; }
tr.R30 td.R36C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R30 td.R36C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R37{ height: 24px; }
tr.R37 td.R37C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R37 td.R37C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R37 td.R37C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R37 td.R37C6{ text-align: left; }
tr.R37 td.R43C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R37 td.R43C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R47{ height: 27px; }
tr.R47 td.R47C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R47 td.R47C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R47 td.R47C6{ text-align: left; }
tr.R49{ height: 26px; }
tr.R49 td.R49C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R49 td.R49C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R49 td.R49C6{ text-align: left; }
tr.R49 td.R50C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R49 td.R50C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R7{ height: 29px; }
tr.R7 td.R23C0{ font-family: Arial; font-size: 14pt; font-style: normal; text-align: center; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R7 td.R7C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R7 td.R7C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R7 td.R7C6{ text-align: left; }
tr.R7 td.R8C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R7 td.R8C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R74{ height: 23px; }
tr.R74 td.R74C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R74 td.R74C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 1px solid; border-right: #000000 1px solid; }
tr.R74 td.R74C6{ text-align: left; }
tr.R74 td.R78C1{ text-align: left; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R74 td.R78C2{ text-align: left; vertical-align: middle; border-left: #000000 1px solid; border-top: #000000 1px solid; border-bottom: #000000 3px solid; border-right: #000000 1px solid; }
tr.R86{ height: 99px; }
tr.R86 td.R86C3{ font-family: Arial; font-size: 8pt; font-style: normal; text-align: center; vertical-align: middle; }
table {table-layout: fixed; padding: 0px; padding-left: 2px; vertical-align:bottom; border-collapse:collapse;width: 100%; font-family: Arial; font-size: 8pt; font-style: normal; }
td { padding: 0px; padding-left: 2px; overflow:hidden; }
</STYLE>
</HEAD>
<BODY STYLE="background: #ffffff; margin: 0; font-family: Arial; font-size: 8pt; font-style: normal; ">
<TABLE style="width:100%; height:0px; " CELLSPACING=0>
<COL WIDTH=31>
<COL WIDTH=89>
<COL WIDTH=420>
<COL WIDTH=420>
<COL WIDTH=420>
<COL WIDTH=420>
<COL WIDTH=63>
<COL>
<TR CLASS=R0>
<TD CLASS="R0C0" COLSPAN=3><SPAN STYLE="white-space:nowrap;max-width:0px;">Обновление&nbsp;07.10.2026</SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R1C0"><SPAN STYLE="white-space:nowrap;max-width:0px;">День</SPAN></TD>
<TD CLASS="R1C0"><SPAN STYLE="white-space:nowrap;max-width:0px;">Интервал</SPAN></TD>
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Долженкова&nbsp;М.Л.</SPAN></TD>
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Коржавина&nbsp;А.С.</SPAN></TD>
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Мельцов&nbsp;В.Ю.</SPAN></TD>
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Прозоров&nbsp;Д.Е.</SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R2>
<TD CLASS="R2C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">понедельник&nbsp;28.09.26&nbsp;</SPAN></TD>
<TD CLASS="R2C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R2>
<TD CLASS="R2C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R2C2">1-109 Программирование Лабораторная работа ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа</TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R2>
<TD CLASS="R2C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R2C2">1-109 Программирование Лабораторная работа ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа</TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R2>
<TD CLASS="R2C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R2C2">1-109 Программирование Лабораторная работа ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа</TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R2>
<TD CLASS="R2C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R2C2">1-109 Программирование Лабораторная работа ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа</TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C2"><SPAN></SPAN></TD>
<TD CLASS="R2C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R8C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R8C2"><SPAN></SPAN></TD>
<TD CLASS="R8C2"><SPAN></SPAN></TD>
<TD CLASS="R8C2"><SPAN></SPAN></TD>
<TD CLASS="R8C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">вторник&nbsp;29.09.26&nbsp;</SPAN></TD>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2">1-237 Теория автоматов Практическое занятие ИВТб-2301-06-00 ИВТб-2301-06-00, 01 подгруппа ИВТб-2301-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2">1-237 Теория автоматов Практическое занятие ИВТб-2302-06-00 ИВТб-2302-06-00, 01 подгруппа ИВТб-2302-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R15C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R16>
<TD CLASS="R16C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">среда&nbsp;30.09.26&nbsp;</SPAN></TD>
<TD CLASS="R16C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R16C2">2-207 Программирование Практическое занятие ИВТб-1306-06-00 ИВТб-1306-06-00, 01 подгруппа ИВТб-1306-06-00, 02 подгруппа ИВТб-1306-06-00, 03 подгруппа</TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R17>
<TD CLASS="R17C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2">2-209 Информатика Лекция ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа ИВТб-1301-06-00, 02 подгруппа ИВТб-1301-06-00, 03 подгруппа ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа ИВТб-1302-06-00, 02 подгруппа ИВТб-1302-06-00, 03 подгруппа ИВТб-1303-06-00 ИВТб-1303-06-00, 01 подгруппа ИВТб-1303-06-00, 02 подгруппа ИВТб-1303-06-00, 03 подгруппа ИВТб-1304-06-00 ИВТб-1304-06-00, 01 подгруппа ИВТб-1304-06-00, 02 подгруппа ИВТб-1304-06-00, 03 подгруппа ИВТб-1305-06-00 ИВТб-1305-06-00, 01 подгруппа ИВТб-1305-06-00, 02 подгруппа ИВТб-1305-06-00, 03 подгруппа ИВТб-1306-06-00 ИВТб-1306-06-00, 01 подгруппа ИВТб-1306-06-00, 02 подгруппа ИВТб-1306-06-00, 03 подгруппа</TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R16>
<TD CLASS="R16C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R16C2">1-334 Программирование Практическое занятие ИВТб-1305-06-00 ИВТб-1305-06-00, 01 подгруппа ИВТб-1305-06-00, 02 подгруппа ИВТб-1305-06-00, 03 подгруппа</TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R22C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R23C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">четверг&nbsp;01.10.26&nbsp;</SPAN></TD>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R7C2">1-237 Интеллектуальные системы Лабораторная работа ИВТб-4301-05-00 ИВТб-4301-05-00, 01 подгруппа ИВТб-4301-05-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R7C2">1-237 Интеллектуальные системы Лабораторная работа ИВТб-4301-05-00 ИВТб-4301-05-00, 01 подгруппа ИВТб-4301-05-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R17>
<TD CLASS="R17C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R17C2">2-209 Программирование Лекция ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа ИВТб-1301-06-00, 02 подгруппа ИВТб-1301-06-00, 03 подгруппа ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа ИВТб-1302-06-00, 02 подгруппа ИВТб-1302-06-00, 03 подгруппа ИВТб-1303-06-00 ИВТб-1303-06-00, 01 подгруппа ИВТб-1303-06-00, 02 подгруппа ИВТб-1303-06-00, 03 подгруппа ИВТб-1304-06-00 ИВТб-1304-06-00, 01 подгруппа ИВТб-1304-06-00, 02 подгруппа ИВТб-1304-06-00, 03 подгруппа ИВТб-1305-06-00 ИВТб-1305-06-00, 01 подгруппа ИВТб-1305-06-00, 02 подгруппа ИВТб-1305-06-00, 03 подгруппа ИВТб-1306-06-00 ИВТб-1306-06-00, 01 подгруппа ИВТб-1306-06-00, 02 подгруппа ИВТб-1306-06-00, 03 подгруппа</TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R22C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R30>
<TD CLASS="R30C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">пятница&nbsp;02.10.26&nbsp;</SPAN></TD>
<TD CLASS="R30C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R30>
<TD CLASS="R30C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R16>
<TD CLASS="R16C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R16C2">1-435 Программирование Практическое занятие ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа ИВТб-1301-06-00, 02 подгруппа ИВТб-1301-06-00, 03 подгруппа</TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R7C2">1-109 Программирование Лабораторная работа ИВТб-1301-06-00 ИВТб-1301-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R7C2">1-109 Программирование Лабораторная работа ИВТб-1301-06-00 ИВТб-1301-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2">1-237 Теория автоматов Практическое занятие ИВТб-2304-06-00 ИВТб-2304-06-00, 01 подгруппа ИВТб-2304-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R30>
<TD CLASS="R30C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C2"><SPAN></SPAN></TD>
<TD CLASS="R30C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R30>
<TD CLASS="R36C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R36C2"><SPAN></SPAN></TD>
<TD CLASS="R36C2"><SPAN></SPAN></TD>
<TD CLASS="R36C2"><SPAN></SPAN></TD>
<TD CLASS="R36C2"><SPAN></SPAN></TD>
<TD CLASS="R30C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">суббота&nbsp;03.10.26&nbsp;</SPAN></TD>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R43C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R43C2"><SPAN></SPAN></TD>
<TD CLASS="R43C2"><SPAN></SPAN></TD>
<TD CLASS="R43C2"><SPAN></SPAN></TD>
<TD CLASS="R43C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R16>
<TD CLASS="R16C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">понедельник&nbsp;05.10.26&nbsp;</SPAN></TD>
<TD CLASS="R16C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R16C2">1-311 Программирование Практическое занятие ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа ИВТб-1302-06-00, 02 подгруппа ИВТб-1302-06-00, 03 подгруппа</TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R7C2">1-109 Программирование Лабораторная работа ИВТб-1302-06-00 ИВТб-1302-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R7C2">1-109 Программирование Лабораторная работа ИВТб-1302-06-00 ИВТб-1302-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R47>
<TD CLASS="R47C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R47>
<TD CLASS="R47C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C2"><SPAN></SPAN></TD>
<TD CLASS="R47C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R49>
<TD CLASS="R49C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R49C2"><SPAN></SPAN></TD>
<TD CLASS="R49C2"><SPAN></SPAN></TD>
<TD CLASS="R49C2"><SPAN></SPAN></TD>
<TD CLASS="R49C2"><SPAN></SPAN></TD>
<TD CLASS="R49C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R49>
<TD CLASS="R50C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R50C2"><SPAN></SPAN></TD>
<TD CLASS="R50C2"><SPAN></SPAN></TD>
<TD CLASS="R50C2"><SPAN></SPAN></TD>
<TD CLASS="R50C2"><SPAN></SPAN></TD>
<TD CLASS="R49C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R23C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">вторник&nbsp;06.10.26&nbsp;</SPAN></TD>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2">1-237 Теория автоматов Практическое занятие ИВТб-2305-06-00 ИВТб-2305-06-00, 01 подгруппа ИВТб-2305-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2">1-237 Теория автоматов Практическое занятие ИВТб-2303-06-00 ИВТб-2303-06-00, 01 подгруппа ИВТб-2303-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R9C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R1>
<TD CLASS="R15C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R15C2"><SPAN></SPAN></TD>
<TD CLASS="R9C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R17>
<TD CLASS="R58C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">среда&nbsp;07.10.26&nbsp;</SPAN></TD>
<TD CLASS="R17C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R17C2">2-209 Программирование Лекция ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа ИВТб-1301-06-00, 02 подгруппа ИВТб-1301-06-00, 03 подгруппа ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа ИВТб-1302-06-00, 02 подгруппа ИВТб-1302-06-00, 03 подгруппа ИВТб-1303-06-00 ИВТб-1303-06-00, 01 подгруппа ИВТб-1303-06-00, 02 подгруппа ИВТб-1303-06-00, 03 подгруппа ИВТб-1304-06-00 ИВТб-1304-06-00, 01 подгруппа ИВТб-1304-06-00, 02 подгруппа ИВТб-1304-06-00, 03 подгруппа ИВТб-1305-06-00 ИВТб-1305-06-00, 01 подгруппа ИВТб-1305-06-00, 02 подгруппа ИВТб-1305-06-00, 03 подгруппа ИВТб-1306-06-00 ИВТб-1306-06-00, 01 подгруппа ИВТб-1306-06-00, 02 подгруппа ИВТб-1306-06-00, 03 подгруппа</TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R17>
<TD CLASS="R17C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2"><SPAN></SPAN></TD>
<TD CLASS="R17C2">2-209 Введение в профессию Лекция ИВТб-1301-06-00 ИВТб-1301-06-00, 01 подгруппа ИВТб-1301-06-00, 02 подгруппа ИВТб-1301-06-00, 03 подгруппа ИВТб-1302-06-00 ИВТб-1302-06-00, 01 подгруппа ИВТб-1302-06-00, 02 подгруппа ИВТб-1302-06-00, 03 подгруппа ИВТб-1303-06-00 ИВТб-1303-06-00, 01 подгруппа ИВТб-1303-06-00, 02 подгруппа ИВТб-1303-06-00, 03 подгруппа ИВТб-1304-06-00 ИВТб-1304-06-00, 01 подгруппа ИВТб-1304-06-00, 02 подгруппа ИВТб-1304-06-00, 03 подгруппа ИВТб-1305-06-00 ИВТб-1305-06-00, 01 подгруппа ИВТб-1305-06-00, 02 подгруппа ИВТб-1305-06-00, 03 подгруппа ИВТб-1306-06-00 ИВТб-1306-06-00, 01 подгруппа ИВТб-1306-06-00, 02 подгруппа ИВТб-1306-06-00, 03 подгруппа</TD>
<TD CLASS="R17C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R22C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R65C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">четверг&nbsp;08.10.26&nbsp;</SPAN></TD>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R16>
<TD CLASS="R16C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R16C2">1-435 Программирование Практическое занятие ИВТб-1303-06-00 ИВТб-1303-06-00, 01 подгруппа ИВТб-1303-06-00, 02 подгруппа ИВТб-1303-06-00, 03 подгруппа</TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R16>
<TD CLASS="R16C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R16C2">1-435 Программирование Практическое занятие ИВТб-1304-06-00 ИВТб-1304-06-00, 01 подгруппа ИВТб-1304-06-00, 02 подгруппа ИВТб-1304-06-00, 03 подгруппа</TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C2"><SPAN></SPAN></TD>
<TD CLASS="R16C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R7>
<TD CLASS="R7C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C2">2-205 Теория автоматов Практическое занятие ИВТб-2306-06-00 ИВТб-2306-06-00, 01 подгруппа ИВТб-2306-06-00, 02 подгруппа</TD>
<TD CLASS="R7C2"><SPAN></SPAN></TD>
<TD CLASS="R7C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R18C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R18>
<TD CLASS="R22C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R22C2"><SPAN></SPAN></TD>
<TD CLASS="R18C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">пятница&nbsp;09.10.26&nbsp;</SPAN></TD>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R74>
<TD CLASS="R74C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R74>
<TD CLASS="R74C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R74>
<TD CLASS="R74C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R74>
<TD CLASS="R74C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C2"><SPAN></SPAN></TD>
<TD CLASS="R74C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R74>
<TD CLASS="R78C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R78C2"><SPAN></SPAN></TD>
<TD CLASS="R78C2"><SPAN></SPAN></TD>
<TD CLASS="R78C2"><SPAN></SPAN></TD>
<TD CLASS="R78C2"><SPAN></SPAN></TD>
<TD CLASS="R74C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C0" ROWSPAN=7><div style="margin: 0 -17px;-moz-transform: rotate(-90deg); -webkit-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); transform: rotate(-90deg)"><SPAN STYLE="max-width:0px;">суббота&nbsp;10.10.26&nbsp;</SPAN></TD>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">08:20-09:50</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">10:00-11:30</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">11:45-13:15</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">14:00-15:30</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">15:45-17:15</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">17:20-18:50</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R37>
<TD CLASS="R37C1"><SPAN STYLE="white-space:nowrap;max-width:0px;">18:55-20:25</SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C2"><SPAN></SPAN></TD>
<TD CLASS="R37C6"><SPAN></SPAN></TD>
<TD><SPAN></SPAN></TD>
<TD></TD>
</TR>
<TR CLASS=R86>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD CLASS="R86C3"><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="position:relative; height:99px;width: 100%; overflow:hidden;"><SPAN></SPAN></DIV></TD>
<TD><DIV STYLE="width:100%;height:99px;overflow:hidden;">&nbsp;</DIV></TD>
</TR>
</TABLE>
</BODY>
</HTML>