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:
build:
runs-on: ubuntu-latest
permissions:
contents: read
releases: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
# Ставим JDK вручную и явно прописываем JAVA_HOME.
# actions/setup-java не всегда передаёт JAVA_HOME дальше в Gitea Actions.
- name: Verify Environment
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
javac -version
- 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"
echo "SDK path: $ANDROID_HOME"
- name: Grant execute permission for gradlew
run: |
chmod +x gradlew
java -version
# Версия по дате (ГГГГ.ММ.ДД), при совпадении тега добавляем уникальный суффикс.
- 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)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}
+1
View File
@@ -12,6 +12,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/icon"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
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")
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)
suspend fun insertDays(days: List<TeacherDayEntity>): List<Long>
@@ -41,6 +44,7 @@ interface TeacherScheduleDao {
@Transaction
suspend fun replaceSchedule(schedule: TeacherScheduleEntity, days: List<TeacherDayEntity>, lessons: List<TeacherLessonEntity>) {
deleteSchedule(schedule.departmentId, schedule.season.name, schedule.weekStart)
val scheduleId = insertSchedule(schedule)
val daysWithScheduleId = days.map { it.copy(scheduleId = scheduleId) }
deleteDaysForSchedule(scheduleId)
@@ -57,13 +61,15 @@ interface TeacherScheduleDao {
fun observeDaysForPeriod(season: String, weekStart: Long): Flow<List<TeacherDayEntity>>
@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 " +
"LEFT JOIN departments ON departments.departmentId = teacher_schedules.departmentId " +
"WHERE teacher_schedules.season = :season AND teacher_schedules.weekStart <= :weekStart " +
"AND teacher_schedules.weekEnd >= :weekStart " +
"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(
"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>)
/**
* Столбец расписания преподавателя: имя и абсолютный индекс ячейки в шапке.
* Индекс нужен для точного выравнивания с рядами данных — в шапке могут
* присутствовать пустые колонки-разделители, которые не должны сдвигать
* соответствие «преподаватель → столбец».
*/
private data class Column(val teacher: String, val headerIndex: Int)
fun parse(html: String): ParsedTeacherSchedule {
val doc = Jsoup.parse(html)
var headerFound = false
var columns: List<Column> = emptyList()
var teachers: List<String> = emptyList()
val days = mutableListOf<Day>()
var currentDate: LocalDate? = null
@@ -43,10 +52,12 @@ object TeacherScheduleParser {
val first = cellText(tds[0])
val second = cellText(tds[1])
if (!headerFound && first == "День" && second == "Интервал") {
teachers = tds.drop(2)
.map { cellText(it.selectFirst("span") ?: it) }
.filter { it.isNotEmpty() }
.distinct()
columns = tds.mapIndexedNotNull { index, td ->
if (index < 2) return@mapIndexedNotNull null
val name = cellText(td.selectFirst("span") ?: td)
if (name.isEmpty()) null else Column(name, index)
}.distinctBy { it.teacher }
teachers = columns.map { it.teacher }
headerFound = true
continue
}
@@ -58,7 +69,7 @@ object TeacherScheduleParser {
currentIntervals = mutableListOf()
}
if (intervalMatch != null && currentIntervals != null) {
parseIntervalRow(tds, intervalMatch, if (dayMatch != null) 2 else 1, teachers, currentIntervals!!)
parseIntervalRow(tds, intervalMatch, dayMatch != null, columns, currentIntervals!!)
}
}
flushDay()
@@ -83,16 +94,20 @@ object TeacherScheduleParser {
private fun parseIntervalRow(
tds: List<Element>,
intervalMatch: MatchResult,
startCol: Int,
teachers: List<String>,
dayRow: Boolean,
columns: List<Column>,
intervals: MutableList<TimeSlot>
) {
val start = LocalTime.parse(intervalMatch.groupValues[1])
val end = LocalTime.parse(intervalMatch.groupValues[2])
val lessons = mutableListOf<Lesson>()
for (i in teachers.indices) {
val cell = tds.getOrNull(startCol + i) ?: continue
lessons += parseCell(cell, teachers[i])
for (column in columns) {
// Ряд с днём содержит обе ведущие ячейки (День, Интервал) и совпадает
// по индексам с шапкой; ряд только с интервалом пропускает ячейку дня
// (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))
}
@@ -4,6 +4,7 @@ import agzam.vyatsu.data.local.DepartmentPeriodEntity
import agzam.vyatsu.data.local.TeacherDao
import agzam.vyatsu.data.local.TeacherDayEntity
import agzam.vyatsu.data.local.TeacherLessonEntity
import agzam.vyatsu.data.local.TeacherLessonWithDepartment
import agzam.vyatsu.data.local.TeacherScheduleDao
import agzam.vyatsu.data.local.TeacherScheduleEntity
import agzam.vyatsu.data.remote.TeacherScheduleParser
@@ -56,7 +57,7 @@ class TeacherScheduleRepositoryImpl(
scheduleDao.observeDistinctTeachers(period.season.name, period.weekStart.toEpochDay())
.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
// delete+insert from concurrent syncPeriod() callers.
syncMutex.withLock {
@@ -68,18 +69,27 @@ class TeacherScheduleRepositoryImpl(
return@withLock
}
targets.map { it.season to it.weekStart }
.distinct()
.forEach { (season, weekStart) ->
withContext(Dispatchers.IO) {
scheduleDao.deletePeriod(season.name, weekStart)
}
val now = now()
var skippedCount = 0
val nonSkipped = mutableListOf<DepartmentPeriodEntity>()
for (target in targets) {
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 semaphore = Semaphore(PARALLELISM)
val jobs = coroutineScope {
targets.map { target ->
nonSkipped.map { target ->
async {
semaphore.withPermit {
try {
@@ -102,7 +112,7 @@ class TeacherScheduleRepositoryImpl(
}
}
var done = 0
var done = skippedCount
var failed = 0
for (job in jobs) {
if (job.await()) done++ else failed++
@@ -229,30 +239,30 @@ class TeacherScheduleRepositoryImpl(
private fun buildTeacherSchedule(
period: SchedulePeriod,
dayEntities: List<TeacherDayEntity>,
lessonEntities: List<TeacherLessonEntity>
lessons: List<TeacherLessonWithDepartment>
): TeacherSchedule? {
val weekDays = dayEntities
.filter {
it.date >= period.weekStart.toEpochDay() && it.date <= period.weekEnd.toEpochDay()
}
if (weekDays.isEmpty()) return null
val lessonsByDate = lessonEntities.groupBy { it.date }
val lessonsByDate = lessons.groupBy { it.lesson.date }
val days = weekDays
.sortedBy { it.date }
.distinctBy { it.date }
.map { day ->
val lessons = lessonsByDate[day.date].orEmpty()
val dayLessons = lessonsByDate[day.date].orEmpty()
Log.d(
TAG,
"build: date=${day.date} totalLessons=${lessons.size} " +
"groupedByStart=${lessons.groupBy { it.start }.keys.sorted()}"
"build: date=${day.date} totalLessons=${dayLessons.size} " +
"groupedByStart=${dayLessons.groupBy { it.lesson.start }.keys.sorted()}"
)
val slots = lessons.groupBy { it.start }
val slots = dayLessons.groupBy { it.lesson.start }
.toSortedMap()
.map { (startMin, slotLessons) ->
val first = slotLessons.first()
val times = slotLessons.map { "${it.start}-${it.end}" }.distinct()
val teachers = slotLessons.map { it.teacher }.distinct()
val first = slotLessons.first().lesson
val times = slotLessons.map { "${it.lesson.start}-${it.lesson.end}" }.distinct()
val teachers = slotLessons.map { it.lesson.teacher }.distinct()
Log.d(
TAG,
"build: date=${day.date} start=$startMin firstEnd=$first.end " +
@@ -262,7 +272,7 @@ class TeacherScheduleRepositoryImpl(
start = LocalTime.of(startMin / 60, startMin % 60),
end = LocalTime.of(first.end / 60, first.end % 60),
lessons = slotLessons
.sortedBy { it.lessonIndex }
.sortedBy { it.lesson.lessonIndex }
.map { it.toDomain() }
)
}
@@ -271,16 +281,33 @@ class TeacherScheduleRepositoryImpl(
return TeacherSchedule(period = period, days = days)
}
private fun TeacherLessonEntity.toDomain(): Lesson = Lesson(
raw = raw,
room = room,
group = groupName,
subgroup = subgroup,
subject = subject,
type = type,
teacher = teacher
private fun TeacherLessonWithDepartment.toDomain(): Lesson = Lesson(
raw = lesson.raw,
room = lesson.room,
group = lesson.groupName,
subgroup = lesson.subgroup,
subject = lesson.subject,
type = lesson.type,
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 now(): Long = System.currentTimeMillis()
@@ -26,4 +26,6 @@ data class Lesson(
/** Преподаватель **/
val teacher: String? = null,
/** Кафедра-источник (только в расписании преподавателей) **/
val department: String? = null,
)
@@ -10,7 +10,7 @@ import java.time.LocalDate
interface TeacherScheduleRepository {
fun observeTeachers(period: SchedulePeriod): Flow<List<Teacher>>
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 weeksFor(date: LocalDate): List<SchedulePeriod>
suspend fun clearAll()
@@ -1,5 +1,6 @@
package agzam.vyatsu.ui.home
import agzam.vyatsu.R
import agzam.vyatsu.ui.info.BuildingsScreen
import agzam.vyatsu.ui.info.CallsScreen
import agzam.vyatsu.ui.schedule.ScheduleScreen
@@ -8,18 +9,18 @@ import agzam.vyatsu.ui.settings.SettingsScreen
import agzam.vyatsu.ui.settings.UpdateViewModel
import agzam.vyatsu.ui.teacher.TeacherScheduleScreen
import agzam.vyatsu.ui.teacher.TeacherScheduleViewModel
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -27,6 +28,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
private enum class HomeTab(val label: String) {
SCHEDULE("Расписание"),
@@ -35,8 +38,12 @@ private enum class HomeTab(val label: String) {
SETTINGS("Настройки")
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
modifier: Modifier = Modifier,
scheduleViewModel: ScheduleViewModel,
teacherScheduleViewModel: TeacherScheduleViewModel,
updateViewModel: UpdateViewModel,
@@ -47,19 +54,32 @@ fun HomeScreen(
val teacherState by teacherScheduleViewModel.uiState.collectAsState()
Scaffold(
modifier = Modifier,
bottomBar = {
NavigationBar {
NavigationBar { // Новое нижнее меню
HomeTab.entries.forEach { t ->
NavigationBarItem(
selected = tab == t,
onClick = { tab = t },
icon = {
Icon(
imageVector = when (t) {
HomeTab.SCHEDULE -> Icons.Default.DateRange
HomeTab.CALLS -> Icons.Default.Info
HomeTab.BUILDINGS -> Icons.Default.Home
HomeTab.SETTINGS -> Icons.Default.Settings
painter = when (t) {
HomeTab.SCHEDULE -> {
if (tab == t) painterResource(R.drawable.calendar_month_24dp_fill)
else painterResource(R.drawable.calendar_month_24dp_outline)
}
HomeTab.CALLS -> {
if (tab == t) painterResource(R.drawable.notifications_active_24dp_fill)
else painterResource(R.drawable.notifications_active_outline)
}
HomeTab.BUILDINGS -> {
if (tab == t) painterResource(R.drawable.location_on_24dp_fill)
else painterResource(R.drawable.location_on_24dp_outline)
}
HomeTab.SETTINGS -> {
if (tab == t) painterResource(R.drawable.settings_24dp_fill)
else painterResource(R.drawable.settings_24dp_outline)
}
},
contentDescription = t.label
)
@@ -70,7 +90,7 @@ fun HomeScreen(
}
}
) { innerPadding ->
androidx.compose.foundation.layout.Box(
Box(
Modifier
.fillMaxSize()
.padding(innerPadding)
@@ -23,7 +23,156 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import agzam.vyatsu.BuildConfig
import agzam.vyatsu.R
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.ui.Alignment
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BuildingsScreen() { // Новый корпусов
val context = LocalContext.current
Scaffold(
modifier = Modifier,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Text(
text = "Корпуса", // Заголовок
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 12.dp,
bottom = 24.dp
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
Column(
modifier = Modifier
.padding(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "Расположение корпусов", // Еще заголовков
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
Text(
text = "Адреса учебных корпусов",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
items(BUILDINGS) { building ->
// Карточки корпусов
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
onClick = {
val uri = Uri.parse("geo:${building.latitude},${building.longitude}?q=${building.latitude},${building.longitude}(Корпус ${building.number})")
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
},
colors = CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
modifier = Modifier.size(60.dp),
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) {
Box(
contentAlignment = Alignment.Center
) {
Text(
text = "${building.number}",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
}
// Название
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "Корпус ${building.number}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = "${building.address}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Время
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Icon (
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = "Enter"
)
}
}
}
}
}
}
}
/*
@Composable
fun BuildingsScreen() {
val context = LocalContext.current
@@ -56,3 +205,5 @@ fun BuildingsScreen() {
}
}
}
*/
@@ -1,21 +1,370 @@
package agzam.vyatsu.ui.info
import agzam.vyatsu.data.static.CALLS
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider
import androidx.compose.foundation.lazy.items
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.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.delay
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
@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
fun CallsScreen() {
LazyColumn(
@@ -40,3 +389,5 @@ fun CallsScreen() {
}
}
}
*/
@@ -1,65 +1,466 @@
package agzam.vyatsu.ui.picker
import agzam.vyatsu.R
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.Teacher
import androidx.compose.foundation.clickable
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerScreen(
modifier: Modifier = Modifier,
viewModel: PickerViewModel,
onGroupPicked: (Group) -> Unit,
onTeacherPicked: (Teacher) -> Unit
onTeacherPicked: (Teacher) -> Unit,
) {
val state by viewModel.uiState.collectAsState()
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
LaunchedEffect(segment)
{
viewModel.selectSegment(segment)
}
val keyboardController = LocalSoftwareKeyboardController.current
val groupsListState = rememberLazyListState()
val teachersListState = rememberLazyListState()
val listState = when (segment) {
PickerSegment.GROUPS -> groupsListState
PickerSegment.TEACHERS -> teachersListState
}
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.collect { isScrolling ->
if (isScrolling) {
keyboardController?.hide()
}
}
}
val query = when (segment) {
PickerSegment.GROUPS -> state.groupQuery
PickerSegment.TEACHERS -> state.teacherQuery
}
val onSearch: (String) -> Unit = when (segment) {
PickerSegment.GROUPS -> viewModel::searchGroup
PickerSegment.TEACHERS -> viewModel::searchTeacher
}
val placeholder = when (segment) {
PickerSegment.GROUPS -> "Поиск группы"
PickerSegment.TEACHERS -> "Поиск преподавателя"
}
val isLoading = when (segment) {
PickerSegment.GROUPS -> state.groupsLoading
PickerSegment.TEACHERS -> state.teachersLoading
}
val isEmpty = when (segment) {
PickerSegment.GROUPS -> state.groups.isEmpty()
PickerSegment.TEACHERS -> state.teachers.isEmpty()
}
val onRetry: () -> Unit = when (segment) {
PickerSegment.GROUPS -> viewModel::retryGroups
PickerSegment.TEACHERS -> viewModel::retryTeachers
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Кого ищем",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
actions = {
IconButton(onClick = {
when (segment) {
PickerSegment.GROUPS -> viewModel.refreshGroups()
PickerSegment.TEACHERS -> viewModel.refreshTeachers()
}
}) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Выберите группу или преподавателя",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth()
) {
SegmentedButton(
selected = segment == PickerSegment.GROUPS,
onClick = {
segment = PickerSegment.GROUPS
},
shape = SegmentedButtonDefaults.itemShape(
index = 0,
count = 2
),
icon = {
Icon(
painter = if (segment == PickerSegment.GROUPS)
painterResource(R.drawable.groups_24dp_fill)
else painterResource(R.drawable.groups_24dp_outline),
contentDescription = "Select group"
)
}
) {
Text("Группы")
}
SegmentedButton(
selected = segment == PickerSegment.TEACHERS,
onClick = {
segment = PickerSegment.TEACHERS
},
shape = SegmentedButtonDefaults.itemShape(
index = 1,
count = 2
),
icon = {
Icon(
painter = if (segment == PickerSegment.TEACHERS)
painterResource(R.drawable.person_24dp_fill)
else painterResource(R.drawable.person_24dp_outline),
contentDescription = "Select teacher"
)
}
) {
Text("Преподаватели")
}
}
Spacer(Modifier.padding(8.dp))
PickerSearchBar(
query = query,
onQueryChange = onSearch,
placeholder = placeholder
)
Spacer(Modifier.height(16.dp))
if (segment == PickerSegment.TEACHERS) {
val progress = state.syncProgress
if (progress != null &&
progress.total > 0 &&
progress.done < progress.total
) {
LinearProgressIndicator(
progress = {
progress.done.toFloat() /
progress.total.toFloat()
},
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Text(
text = "Кафедры: ${progress.done}/${progress.total}",
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.height(8.dp))
}
}
if (isLoading && isEmpty) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
} else if (state.error != null && isEmpty) {
Text(
text = state.error ?: "",
color = MaterialTheme.colorScheme.error
)
TextButton(onClick = onRetry) {
Text("Повторить")
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(bottom = 16.dp)
) {
when (segment) {
PickerSegment.GROUPS -> {
items(
items = state.filteredGroups,
key = { it.name }
) { group ->
PickerCard(
name = group.name,
segment = segment,
onClick = {
onGroupPicked(group)
}
)
}
}
PickerSegment.TEACHERS -> {
items(
items = state.filteredTeachers,
key = { it.name }
) { teacher ->
PickerCard(
name = teacher.name,
segment = segment,
onClick = {
onTeacherPicked(teacher)
}
)
}
}
}
}
}
}
}
}
@Composable
private fun PickerCard(
modifier: Modifier = Modifier,
name: String,
segment: PickerSegment,
onClick: () -> Unit,
) {
val iconRes = when (segment) {
PickerSegment.GROUPS -> R.drawable.groups_24dp_outline
PickerSegment.TEACHERS -> R.drawable.person_24dp_outline
}
Card(
modifier = modifier
.fillMaxWidth()
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
shape = MaterialTheme.shapes.large,
elevation = CardDefaults.cardElevation(
defaultElevation = 2.dp,
pressedElevation = 4.dp
),
onClick = onClick
) {
Row (
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Box(
modifier = Modifier
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.small
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(iconRes),
contentDescription = "Icons",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
}
Text (
text = name,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Icon(
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = "Select",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PickerSearchBar(
query: String,
onQueryChange: (String) -> Unit,
placeholder: String,
modifier: Modifier = Modifier
) {
SearchBarDefaults.InputField(
query = query,
onQueryChange = onQueryChange,
onSearch = {},
expanded = false,
onExpandedChange = {},
modifier = modifier.fillMaxWidth(),
placeholder = {
Text(placeholder)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = null
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(
onClick = { onQueryChange("") }
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Очистить поиск"
)
}
}
}
)
}
/*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PickerScreen(
modifier: Modifier = Modifier,
viewModel: PickerViewModel,
onGroupPicked: (Group) -> Unit,
onTeacherPicked: (Teacher) -> Unit,
) {
val state by viewModel.uiState.collectAsState()
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
LaunchedEffect(segment) { viewModel.selectSegment(segment) }
Scaffold { innerPadding ->
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = "Кого ищем",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surface
),
actions = {
IconButton(onClick = { onRefresh() } ) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
}
)
}
) { innerPadding ->
Column(
Modifier
.fillMaxSize()
@@ -333,4 +734,4 @@ private fun TeacherList(teachers: List<Teacher>, onPick: (Teacher) -> Unit) {
HorizontalDivider(thickness = 1.dp, color = MaterialTheme.colorScheme.outlineVariant)
}
}
}
}*/
@@ -146,7 +146,15 @@ class PickerViewModel(
syncJob = viewModelScope.launch {
try {
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) {
throw e
@@ -193,4 +201,29 @@ class PickerViewModel(
fun retryTeachers() {
syncAndObserveTeachers()
}
// Добавил обновление списка преподов
fun refreshTeachers() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(
teachersLoading = true,
error = null
)
try {
withContext(Dispatchers.IO) {
teacherRepository.forceRefresh()
}
syncAndObserveTeachers()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
teachersLoading = false,
error = e.message ?: "Не удалось обновить преподавателей"
)
}
}
}
}
@@ -1,8 +1,13 @@
package agzam.vyatsu.ui.schedule
import agzam.vyatsu.R
import agzam.vyatsu.domain.model.SchedulePeriod
import agzam.vyatsu.ui.schedule.components.DayChips
import agzam.vyatsu.ui.schedule.components.FULL_DATE
import agzam.vyatsu.ui.schedule.components.LessonSlot
import agzam.vyatsu.ui.schedule.components.ScheduleDateHeader
import agzam.vyatsu.ui.schedule.components.ScheduleEmptyDay
import agzam.vyatsu.ui.schedule.components.ScheduleError
import agzam.vyatsu.ui.schedule.components.WeekSelector
import agzam.vyatsu.ui.schedule.components.dayName
import agzam.vyatsu.ui.schedule.components.hPadding
@@ -17,7 +22,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
@@ -28,6 +35,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -38,8 +46,226 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import java.time.LocalDate
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScheduleScreen(
modifier: Modifier = Modifier,
viewModel: ScheduleViewModel,
onSwitchGroup: () -> Unit
) {
val state by viewModel.uiState.collectAsState()
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Column {
Text(
text = state.groupName ?: "Расписание",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "Расписание занятий",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
actions = {
if (state.isLoading && state.schedule != null) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
}
IconButton(onClick = { viewModel.refresh() }) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
IconButton(onClick = { onSwitchGroup() }) {
Icon(
painter = painterResource(R.drawable.sync_alt_24dp),
contentDescription = "Изменить выбор"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when {
state.schedule == null && state.loadError -> {
ScheduleError(
onRetry = viewModel::refresh
)
}
state.schedule == null -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
else -> {
val schedule = state.schedule!!
val weekDays = state.weekStart
?.let { weekStart ->
schedule.periods.firstOrNull {
it.weekStart == weekStart
}
}
?.let { period ->
schedule.daysOf(period)
}
.orEmpty()
if (weekDays.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Нет расписания на эту неделю")
}
} else {
val selectedDayIndex = weekDays
.indexOfFirst { it.date == state.dayDate }
.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = selectedDayIndex,
pageCount = { weekDays.size }
)
LaunchedEffect(
state.dayDate,
weekDays
) {
val targetPage = weekDays.indexOfFirst {
it.date == state.dayDate
}
if (
targetPage >= 0 &&
targetPage != pagerState.currentPage
) {
pagerState.scrollToPage(targetPage)
}
}
LaunchedEffect(pagerState, weekDays) {
snapshotFlow {
pagerState.settledPage
}.collect { page ->
val selectedDate =
weekDays.getOrNull(page)?.date
if (
selectedDate != null &&
selectedDate != state.dayDate
) {
viewModel.selectDay(selectedDate)
}
}
}
ScheduleDateHeader(
date = state.dayDate,
todayDate = state.todayDate,
periods = schedule.periods,
selectedWeekStart = state.weekStart,
onSelectWeek = viewModel::selectWeek
)
DayChips(
days = weekDays,
selectedDate = state.dayDate,
todayDate = state.todayDate,
pagerState = pagerState,
onSelect = viewModel::selectDay
)
HorizontalDivider()
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f),
key = { page ->
weekDays[page].date.toEpochDay()
}
) { page ->
val day = weekDays.getOrNull(page)
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)
@Composable
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
import agzam.vyatsu.domain.model.Day
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListLayoutInfo
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.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -22,6 +23,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collectLatest
import java.time.LocalDate
@@ -90,44 +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 isTodayChip = todayDate == day.date
FilterChip(
selected = selected,
val isToday = todayDate == day.date
Surface(
onClick = { onSelect(day.date) },
label = {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
dayName(day.date.dayOfWeek),
style = MaterialTheme.typography.labelMedium
)
Text(
day.date.format(CHIP_DATE),
style = MaterialTheme.typography.labelSmall,
color = if (selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
)
}
modifier = Modifier.size(
width = 72.dp,
height = 72.dp
),
shape = RoundedCornerShape(20.dp),
color = if (selected) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surfaceContainerLow
},
modifier = Modifier.padding(vertical = 2.dp),
border = if (isTodayChip && !selected) {
FilterChipDefaults.filterChipBorder(
borderColor = MaterialTheme.colorScheme.primary,
borderWidth = 1.dp,
enabled = true,
selected = false
contentColor = if (selected) {
MaterialTheme.colorScheme.onPrimaryContainer
} else {
MaterialTheme.colorScheme.onSurface
},
border = if (isToday && !selected) {
BorderStroke(
width = 1.5.dp,
color = MaterialTheme.colorScheme.primary
)
} else {
FilterChipDefaults.filterChipBorder(enabled = true, selected = selected)
},
shape = RoundedCornerShape(8.dp)
)
null
}
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = dayName(day.date.dayOfWeek),
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Medium
)
Text(
text = day.date.format(CHIP_DATE),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
@@ -3,12 +3,15 @@ package agzam.vyatsu.ui.schedule.components
import agzam.vyatsu.domain.model.LessonType
import agzam.vyatsu.domain.model.TimeSlot
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -18,6 +21,94 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@Composable
internal fun LessonSlot(
slotIndex: Int,
slot: TimeSlot,
showTeacher: Boolean = true
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = hPadding),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "${slotIndex + 1} пара",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "${slot.start.format(TIME)}–${slot.end.format(TIME)}",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
slot.lessons.forEach { lesson ->
val subject = lesson.subject
?.takeIf { it.isNotBlank() }
?: lesson.raw
val details = listOfNotNull(
lesson.type.takeIf { it != LessonType.OTHER }?.label,
lesson.subgroup,
if (showTeacher) lesson.teacher else lesson.group,
lesson.room
).filter { it.isNotBlank() }
Card(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor =
MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = subject,
style = MaterialTheme.typography.titleMedium
)
if (details.isNotEmpty()) {
Text(
text = details.joinToString(" · "),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
lesson.department
?.takeIf { it.isNotBlank() }
?.let { department ->
HorizontalDivider()
Text(
text = department,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
/*
@Composable
internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = true) {
val context = LocalContext.current
@@ -70,6 +161,16 @@ internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = t
) {
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(
if (lesson.type == LessonType.OTHER) {
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
import agzam.vyatsu.BuildConfig
import agzam.vyatsu.R
import agzam.vyatsu.update.UpdateState
import android.widget.HorizontalScrollView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(viewModel: UpdateViewModel) {
val autoEnabled by viewModel.autoEnabled.collectAsState()
val updateState by viewModel.updateState.collectAsState()
Scaffold(
modifier = Modifier,
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Text(
text = "Настройки",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
)
}
)
}
) { innerPadding ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
item {
SectionTitle("Обновления")
}
item {
SettingsGroup {
SettingsRow(
icon = painterResource(R.drawable.cached_24dp),
title = "Автоматические обновления",
subtitle = "Проверять новые версии при запуске",
trailing = {
Switch(
checked = autoEnabled,
onCheckedChange = viewModel::setAutoEnabled
)
},
onClick = {
viewModel.setAutoEnabled(!autoEnabled)
}
)
HorizontalDivider(
modifier = Modifier.padding(start = 72.dp),
color = MaterialTheme.colorScheme.outlineVariant
)
SettingsRow(
icon = painterResource(R.drawable.download_24dp),
title = "Проверить обновления",
subtitle = updateStatusText(
updateState,
autoEnabled
),
trailing = {
Icon(
painter = painterResource(R.drawable.chevron_right_24dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
},
onClick = viewModel::checkNow
)
}
}
if (
autoEnabled &&
(updateState is UpdateState.Available ||
updateState is UpdateState.ReadyToInstall)
) {
item {
SettingsGroup {
SettingsRow(
icon = painterResource(R.drawable.download_done_24dp),
title = "Установить обновление",
subtitle = installHint(
updateState,
viewModel
),
onClick = {
when (updateState) {
is UpdateState.Available ->
viewModel.downloadAndInstall()
is UpdateState.ReadyToInstall ->
viewModel.installDownloadedApk()
else -> Unit
}
}
)
}
}
}
item {
SectionTitle("О приложении")
}
item {
SettingsGroup {
SettingsRow(
icon = painterResource(R.drawable.info_24dp),
title = "Версия приложения",
subtitle = "Текущая установленная версия",
trailing = {
Text(
text = BuildConfig.VERSION_NAME,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
}
}
}
}
@Composable
private fun SectionTitle(
title: String
) {
Text(
text = title,
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 4.dp)
)
}
@Composable
private fun SettingsGroup(
content: @Composable ColumnScope.() -> Unit
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceContainerLow
) {
Column(content = content)
}
}
@Composable
private fun SettingsRow(
icon: Painter,
title: String,
subtitle: String? = null,
trailing: (@Composable () -> Unit)? = null,
onClick: (() -> Unit)? = null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(
if (onClick != null) {
Modifier.clickable(onClick = onClick)
} else {
Modifier
}
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Surface(
modifier = Modifier.size(44.dp),
shape = RoundedCornerShape(14.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
painter = icon,
contentDescription = null
)
}
}
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
if (subtitle != null) {
Text(
text = subtitle,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
trailing?.invoke()
}
}
private fun updateStatusText(state: UpdateState, autoEnabled: Boolean): String = when (state) {
is UpdateState.Idle -> "Проверить сейчас"
is UpdateState.Checking -> "Проверка…"
is UpdateState.UpToDate -> "Установлена последняя версия"
is UpdateState.Available ->
if (autoEnabled) "Доступна версия ${state.version}"
else "Доступна версия ${state.version} — включите автообновления для установки"
is UpdateState.Downloading -> "Скачивание: ${state.progress?.let { "$it%" } ?: "…"}"
is UpdateState.ReadyToInstall -> "Обновление скачано, нажмите для установки"
is UpdateState.Failed -> "Ошибка: ${state.message}"
}
private fun installHint(state: UpdateState, viewModel: UpdateViewModel): String = when {
state is UpdateState.ReadyToInstall && !viewModel.canRequestInstalls() ->
"Разрешите установку приложений из этого источника"
else -> "Нажмите, чтобы продолжить"
}
@Composable
private fun SectionHeader(title: String) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
/*
@Composable
fun SettingsScreen(viewModel: UpdateViewModel) {
val autoEnabled by viewModel.autoEnabled.collectAsState()
@@ -87,36 +358,10 @@ fun SettingsScreen(viewModel: UpdateViewModel) {
SettingsDivider()
}
}
}
}*/
private fun updateStatusText(state: UpdateState, autoEnabled: Boolean): String = when (state) {
is UpdateState.Idle -> "Проверить сейчас"
is UpdateState.Checking -> "Проверка…"
is UpdateState.UpToDate -> "Установлена последняя версия"
is UpdateState.Available ->
if (autoEnabled) "Доступна версия ${state.version}"
else "Доступна версия ${state.version} — включите автообновления для установки"
is UpdateState.Downloading -> "Скачивание: ${state.progress?.let { "$it%" } ?: "…"}"
is UpdateState.ReadyToInstall -> "Обновление скачано, нажмите для установки"
is UpdateState.Failed -> "Ошибка: ${state.message}"
}
private fun installHint(state: UpdateState, viewModel: UpdateViewModel): String = when {
state is UpdateState.ReadyToInstall && !viewModel.canRequestInstalls() ->
"Разрешите установку приложений из этого источника"
else -> "Нажмите, чтобы продолжить"
}
@Composable
private fun SectionHeader(title: String) {
Text(
title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
/*
@Composable
private fun SettingsItem(
title: String,
@@ -150,3 +395,5 @@ private fun SettingsItem(
private fun SettingsDivider() {
HorizontalDivider(Modifier.padding(horizontal = 16.dp))
}
*/
@@ -1,35 +1,31 @@
package agzam.vyatsu.ui.teacher
import agzam.vyatsu.R
import agzam.vyatsu.ui.schedule.components.DayChips
import agzam.vyatsu.ui.schedule.components.FULL_DATE
import agzam.vyatsu.ui.schedule.components.LessonSlot
import agzam.vyatsu.ui.schedule.components.WeekSelector
import agzam.vyatsu.ui.schedule.components.dayName
import agzam.vyatsu.ui.schedule.components.hPadding
import agzam.vyatsu.ui.schedule.components.ScheduleDateHeader
import agzam.vyatsu.ui.schedule.components.ScheduleError
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.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.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -38,8 +34,231 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TeacherScheduleScreen(
viewModel: TeacherScheduleViewModel,
onSwitchTeacher: () -> Unit
) {
val state by viewModel.uiState.collectAsState()
Scaffold(
contentWindowInsets = WindowInsets(0.dp),
topBar = {
TopAppBar(
windowInsets = WindowInsets(0.dp),
title = {
Column {
Text(
text = state.teacherName ?: "Расписание",
style = MaterialTheme.typography.headlineSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Расписание преподавателя",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
actions = {
if (state.isLoading && state.schedule != null) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp
)
}
IconButton(
onClick = { viewModel.refresh() }
) {
Icon(
painter = painterResource(R.drawable.refresh_24dp),
contentDescription = "Обновить"
)
}
IconButton(
onClick = onSwitchTeacher
) {
Icon(
painter = painterResource(R.drawable.sync_alt_24dp),
contentDescription = "Сменить преподавателя"
)
}
}
)
}
) { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
when {
state.schedule == null && state.loadError -> {
ScheduleError(
onRetry = viewModel::refresh
)
}
state.schedule == null -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
else -> {
val schedule = state.schedule!!
val weekDays = schedule.days
val periods = state.weeks.ifEmpty {
listOf(schedule.period)
}
if (weekDays.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Нет расписания на эту неделю")
}
} else {
val selectedDayIndex = weekDays
.indexOfFirst { it.date == state.dayDate }
.coerceAtLeast(0)
val pagerState = rememberPagerState(
initialPage = selectedDayIndex,
pageCount = { weekDays.size }
)
// Синхронизация выбора дня с Pager
LaunchedEffect(
state.dayDate,
weekDays
) {
val targetPage = weekDays.indexOfFirst {
it.date == state.dayDate
}
if (
targetPage >= 0 &&
targetPage != pagerState.currentPage
) {
pagerState.scrollToPage(targetPage)
}
}
// Переключение дня свайпом
LaunchedEffect(pagerState, weekDays) {
snapshotFlow {
pagerState.settledPage
}.collect { page ->
val selectedDate =
weekDays.getOrNull(page)?.date
if (
selectedDate != null &&
selectedDate != state.dayDate
) {
viewModel.selectDay(selectedDate)
}
}
}
ScheduleDateHeader(
date = state.dayDate,
todayDate = state.todayDate,
periods = periods,
selectedWeekStart = state.weekStart,
onSelectWeek = viewModel::selectWeek
)
DayChips(
days = weekDays,
selectedDate = state.dayDate,
todayDate = state.todayDate,
pagerState = pagerState,
onSelect = viewModel::selectDay
)
HorizontalDivider()
HorizontalPager(
state = pagerState,
modifier = Modifier.weight(1f),
key = { page ->
weekDays[page].date.toEpochDay()
}
) { page ->
val day = weekDays.getOrNull(page)
val visibleSlots = day?.intervals
?.withIndex()
?.filter { (_, slot) ->
slot.lessons.any { lesson ->
!lesson.subject.isNullOrBlank() ||
lesson.raw.isNotBlank()
}
}
.orEmpty()
if (visibleSlots.isEmpty()) {
ScheduleEmptyDay()
} else {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
verticalArrangement =
Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(
top = 16.dp,
bottom = 24.dp
)
) {
items(
items = visibleSlots,
key = { it.index }
) { (slotIndex, slot) ->
LessonSlot(
slotIndex = slotIndex,
slot = slot,
showTeacher = false
)
}
}
}
}
}
}
}
}
}
}
@Composable
fun ScheduleEmptyDay() {
TODO("Not yet implemented")
}
/*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher: () -> Unit) {
@@ -186,3 +405,5 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher:
}
}
}
*/
@@ -89,7 +89,9 @@ class TeacherScheduleViewModel(
if (!scheduleRepository.hasData(period)) {
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
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) {
throw e
} catch (e: Exception) {
@@ -199,7 +201,7 @@ class TeacherScheduleViewModel(
try {
_uiState.value = _uiState.value.copy(isLoading = true, loadError = false)
try {
scheduleRepository.syncPeriod(ScheduleDates.moscowToday()).collect { }
scheduleRepository.syncPeriod(ScheduleDates.moscowToday(), force = true).collect { }
} catch (e: CancellationException) {
throw e
} 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"))
.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
fun teachersHeaderParsedInOrderAndDeduped() {
val parsed = TeacherScheduleParser.parse(fixture())
@@ -83,4 +91,94 @@ class TeacherScheduleParserTest {
val total = parsed.days.sumOf { day -> day.intervals.sumOf { it.lessons.size } }
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>