Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fb1e3edca | ||
|
|
84da62c45d | ||
|
|
57e5bd9af8 | ||
|
|
c7298c0c84 | ||
|
|
ae0fea1ea6 | ||
|
|
af1f374b95 |
@@ -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
|
||||
|
||||
@@ -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) на выбранный переиод (ибо раскиданы по кафедрам)
|
||||
@@ -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)
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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,386 @@
|
||||
package agzam.vyatsu.ui.info
|
||||
|
||||
import agzam.vyatsu.data.static.CALLS
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import kotlinx.coroutines.delay
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CallsScreen() {
|
||||
|
||||
var now by remember {
|
||||
mutableStateOf(LocalTime.now())
|
||||
}
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
now = LocalTime.now()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
now = LocalTime.now()
|
||||
delay(15_000L)
|
||||
}
|
||||
}
|
||||
|
||||
val intervals = remember {
|
||||
val formatter = DateTimeFormatter.ofPattern("H:mm")
|
||||
|
||||
CALLS.mapIndexed { index, call ->
|
||||
CallInterval(
|
||||
number = index + 1,
|
||||
start = LocalTime.parse(call.start, formatter),
|
||||
end = LocalTime.parse(call.end, formatter)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val current = intervals.firstOrNull {
|
||||
!now.isBefore(it.start) && now.isBefore(it.end)
|
||||
}
|
||||
|
||||
val next = intervals.firstOrNull {
|
||||
now.isBefore(it.start)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
title = {
|
||||
Text(
|
||||
text = "Звонки",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = 16.dp,
|
||||
vertical = 12.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
|
||||
item {
|
||||
CallsStatusCard(
|
||||
now = now,
|
||||
current = current,
|
||||
next = next,
|
||||
first = intervals.firstOrNull()
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = intervals,
|
||||
key = { it.number }
|
||||
) { interval ->
|
||||
|
||||
val isCurrent =
|
||||
current?.number == interval.number
|
||||
|
||||
CallCard(
|
||||
interval = interval,
|
||||
now = now,
|
||||
isCurrent = isCurrent
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class CallInterval(
|
||||
val number: Int,
|
||||
val start: LocalTime,
|
||||
val end: LocalTime
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun CallCard(
|
||||
interval: CallInterval,
|
||||
now: LocalTime,
|
||||
isCurrent: Boolean
|
||||
) {
|
||||
val isFinished = !now.isBefore(interval.end)
|
||||
|
||||
val status = when {
|
||||
isCurrent -> "Идёт сейчас"
|
||||
isFinished -> "Завершена"
|
||||
else -> "Впереди"
|
||||
}
|
||||
|
||||
val totalSeconds = ChronoUnit.SECONDS.between(
|
||||
interval.start,
|
||||
interval.end
|
||||
).coerceAtLeast(1)
|
||||
|
||||
val elapsedSeconds = ChronoUnit.SECONDS.between(
|
||||
interval.start,
|
||||
now
|
||||
)
|
||||
|
||||
val progress = (
|
||||
elapsedSeconds.toFloat() / totalSeconds
|
||||
).coerceIn(0f, 1f)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isCurrent) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
}
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.size(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = if (isCurrent) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
},
|
||||
contentColor = if (isCurrent) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = interval.number.toString(),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = if (isCurrent) {
|
||||
"Текущая пара"
|
||||
} else {
|
||||
"Пара"
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = status,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = interval.start.toString(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = interval.end.toString(),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isCurrent) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallsStatusCard(
|
||||
now: LocalTime,
|
||||
current: CallInterval?,
|
||||
next: CallInterval?,
|
||||
first: CallInterval?
|
||||
) {
|
||||
val title: String
|
||||
val subtitle: String
|
||||
val progress: Float
|
||||
|
||||
when {
|
||||
current != null -> {
|
||||
title = "Сейчас идёт ${current.number} пара"
|
||||
subtitle = "До конца ${minutesUntil(now, current.end)} мин"
|
||||
|
||||
val total = ChronoUnit.SECONDS.between(
|
||||
current.start,
|
||||
current.end
|
||||
).coerceAtLeast(1)
|
||||
|
||||
val elapsed = ChronoUnit.SECONDS.between(
|
||||
current.start,
|
||||
now
|
||||
)
|
||||
|
||||
progress = (
|
||||
elapsed.toFloat() / total
|
||||
).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
next != null -> {
|
||||
title = if (first?.number == next.number) {
|
||||
"Занятия ещё не начались"
|
||||
} else {
|
||||
"Сейчас перемена"
|
||||
}
|
||||
|
||||
subtitle = "До ${next.number} пары ${minutesUntil(now, next.start)} мин"
|
||||
progress = 0f
|
||||
}
|
||||
|
||||
else -> {
|
||||
title = "Занятия закончились"
|
||||
subtitle = "На сегодня пар больше нет"
|
||||
progress = 1f
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
|
||||
if (current != null) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progress },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun minutesUntil(
|
||||
now: LocalTime,
|
||||
target: LocalTime
|
||||
): Long {
|
||||
val seconds = ChronoUnit.SECONDS.between(
|
||||
now,
|
||||
target
|
||||
).coerceAtLeast(0)
|
||||
|
||||
return (seconds + 59) / 60
|
||||
}
|
||||
|
||||
/*
|
||||
// Агзам
|
||||
@Composable
|
||||
fun CallsScreen() {
|
||||
LazyColumn(
|
||||
@@ -40,3 +405,5 @@ fun CallsScreen() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -1,29 +1,32 @@
|
||||
package agzam.vyatsu.ui.picker
|
||||
|
||||
import agzam.vyatsu.R
|
||||
import agzam.vyatsu.domain.model.Group
|
||||
import agzam.vyatsu.domain.model.Teacher
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
@@ -36,30 +39,429 @@ import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PickerScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: PickerViewModel,
|
||||
onGroupPicked: (Group) -> Unit,
|
||||
onTeacherPicked: (Teacher) -> Unit
|
||||
onTeacherPicked: (Teacher) -> Unit,
|
||||
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
|
||||
|
||||
LaunchedEffect(segment)
|
||||
{
|
||||
viewModel.selectSegment(segment)
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val groupsListState = rememberLazyListState()
|
||||
val teachersListState = rememberLazyListState()
|
||||
|
||||
val listState = when (segment) {
|
||||
PickerSegment.GROUPS -> groupsListState
|
||||
PickerSegment.TEACHERS -> teachersListState
|
||||
}
|
||||
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow { listState.isScrollInProgress }
|
||||
.collect { isScrolling ->
|
||||
if (isScrolling) {
|
||||
keyboardController?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val query = when (segment) {
|
||||
PickerSegment.GROUPS -> state.groupQuery
|
||||
PickerSegment.TEACHERS -> state.teacherQuery
|
||||
}
|
||||
|
||||
val onSearch: (String) -> Unit = when (segment) {
|
||||
PickerSegment.GROUPS -> viewModel::searchGroup
|
||||
PickerSegment.TEACHERS -> viewModel::searchTeacher
|
||||
}
|
||||
|
||||
val placeholder = when (segment) {
|
||||
PickerSegment.GROUPS -> "Поиск группы"
|
||||
PickerSegment.TEACHERS -> "Поиск преподавателя"
|
||||
}
|
||||
|
||||
|
||||
val isLoading = when (segment) {
|
||||
PickerSegment.GROUPS -> state.groupsLoading
|
||||
PickerSegment.TEACHERS -> state.teachersLoading
|
||||
}
|
||||
|
||||
val isEmpty = when (segment) {
|
||||
PickerSegment.GROUPS -> state.groups.isEmpty()
|
||||
PickerSegment.TEACHERS -> state.teachers.isEmpty()
|
||||
}
|
||||
|
||||
val onRetry: () -> Unit = when (segment) {
|
||||
PickerSegment.GROUPS -> viewModel::retryGroups
|
||||
PickerSegment.TEACHERS -> viewModel::retryTeachers
|
||||
}
|
||||
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Кого ищем",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
when (segment) {
|
||||
PickerSegment.GROUPS -> viewModel.refreshGroups()
|
||||
PickerSegment.TEACHERS -> viewModel.refreshTeachers()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.refresh_24dp),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Выберите группу или преподавателя",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
SegmentedButton(
|
||||
selected = segment == PickerSegment.GROUPS,
|
||||
onClick = {
|
||||
segment = PickerSegment.GROUPS
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 0,
|
||||
count = 2
|
||||
),
|
||||
icon = {
|
||||
Icon(
|
||||
painter = if (segment == PickerSegment.GROUPS)
|
||||
painterResource(R.drawable.groups_24dp_fill)
|
||||
else painterResource(R.drawable.groups_24dp_outline),
|
||||
contentDescription = "Select group"
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text("Группы")
|
||||
}
|
||||
|
||||
SegmentedButton(
|
||||
selected = segment == PickerSegment.TEACHERS,
|
||||
onClick = {
|
||||
segment = PickerSegment.TEACHERS
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = 1,
|
||||
count = 2
|
||||
),
|
||||
icon = {
|
||||
Icon(
|
||||
painter = if (segment == PickerSegment.TEACHERS)
|
||||
painterResource(R.drawable.person_24dp_fill)
|
||||
else painterResource(R.drawable.person_24dp_outline),
|
||||
contentDescription = "Select teacher"
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text("Преподаватели")
|
||||
}
|
||||
|
||||
}
|
||||
Spacer(Modifier.padding(8.dp))
|
||||
|
||||
PickerSearchBar(
|
||||
query = query,
|
||||
onQueryChange = onSearch,
|
||||
placeholder = placeholder
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
if (segment == PickerSegment.TEACHERS) {
|
||||
val progress = state.syncProgress
|
||||
|
||||
if (progress != null &&
|
||||
progress.total > 0 &&
|
||||
progress.done < progress.total
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
progress = {
|
||||
progress.done.toFloat() /
|
||||
progress.total.toFloat()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "Кафедры: ${progress.done}/${progress.total}",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading && isEmpty) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else if (state.error != null && isEmpty) {
|
||||
Text(
|
||||
text = state.error ?: "",
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
|
||||
TextButton(onClick = onRetry) {
|
||||
Text("Повторить")
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
when (segment) {
|
||||
PickerSegment.GROUPS -> {
|
||||
items(
|
||||
items = state.filteredGroups,
|
||||
key = { it.name }
|
||||
) { group ->
|
||||
PickerCard(
|
||||
name = group.name,
|
||||
segment = segment,
|
||||
onClick = {
|
||||
onGroupPicked(group)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PickerSegment.TEACHERS -> {
|
||||
items(
|
||||
items = state.filteredTeachers,
|
||||
key = { it.name }
|
||||
) { teacher ->
|
||||
PickerCard(
|
||||
name = teacher.name,
|
||||
segment = segment,
|
||||
onClick = {
|
||||
onTeacherPicked(teacher)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun PickerCard(
|
||||
modifier: Modifier = Modifier,
|
||||
name: String,
|
||||
segment: PickerSegment,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val iconRes = when (segment) {
|
||||
PickerSegment.GROUPS -> R.drawable.groups_24dp_outline
|
||||
PickerSegment.TEACHERS -> R.drawable.person_24dp_outline
|
||||
}
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessLow
|
||||
)
|
||||
),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 2.dp,
|
||||
pressedElevation = 4.dp
|
||||
),
|
||||
onClick = onClick
|
||||
) {
|
||||
Row (
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = MaterialTheme.shapes.small
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
contentDescription = "Icons",
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
Text (
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.chevron_right_24dp),
|
||||
contentDescription = "Select",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PickerSearchBar(
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
SearchBarDefaults.InputField(
|
||||
query = query,
|
||||
onQueryChange = onQueryChange,
|
||||
onSearch = {},
|
||||
expanded = false,
|
||||
onExpandedChange = {},
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(placeholder)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (query.isNotEmpty()) {
|
||||
IconButton(
|
||||
onClick = { onQueryChange("") }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = "Очистить поиск"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PickerScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: PickerViewModel,
|
||||
onGroupPicked: (Group) -> Unit,
|
||||
onTeacherPicked: (Teacher) -> Unit,
|
||||
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
var segment by rememberSaveable { mutableStateOf(PickerSegment.GROUPS) }
|
||||
LaunchedEffect(segment) { viewModel.selectSegment(segment) }
|
||||
|
||||
Scaffold { innerPadding ->
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = "Кого ищем",
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
actions = {
|
||||
IconButton(onClick = { onRefresh() } ) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.refresh_24dp),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -333,4 +735,4 @@ private fun TeacherList(teachers: List<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,225 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.LocalDate
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ScheduleScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ScheduleViewModel,
|
||||
onSwitchGroup: () -> Unit
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = state.groupName ?: "Расписание",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Расписание занятий",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (state.isLoading && state.schedule != null) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { viewModel.refresh() }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.refresh_24dp),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { onSwitchGroup() }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.sync_alt_24dp),
|
||||
contentDescription = "Изменить выбор"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
when {
|
||||
state.schedule == null && state.loadError -> {
|
||||
ScheduleError(
|
||||
onRetry = viewModel::refresh
|
||||
)
|
||||
}
|
||||
|
||||
state.schedule == null -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val schedule = state.schedule!!
|
||||
|
||||
val weekDays = state.weekStart
|
||||
?.let { weekStart ->
|
||||
schedule.periods.firstOrNull {
|
||||
it.weekStart == weekStart
|
||||
}
|
||||
}
|
||||
?.let { period ->
|
||||
schedule.daysOf(period)
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
if (weekDays.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Нет расписания на эту неделю")
|
||||
}
|
||||
} else {
|
||||
val selectedDayIndex = weekDays
|
||||
.indexOfFirst { it.date == state.dayDate }
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = selectedDayIndex,
|
||||
pageCount = { weekDays.size }
|
||||
)
|
||||
|
||||
LaunchedEffect(
|
||||
state.dayDate,
|
||||
weekDays
|
||||
) {
|
||||
val targetPage = weekDays.indexOfFirst {
|
||||
it.date == state.dayDate
|
||||
}
|
||||
|
||||
if (
|
||||
targetPage >= 0 &&
|
||||
targetPage != pagerState.currentPage
|
||||
) {
|
||||
pagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pagerState, weekDays) {
|
||||
snapshotFlow {
|
||||
pagerState.settledPage
|
||||
}.collect { page ->
|
||||
val selectedDate =
|
||||
weekDays.getOrNull(page)?.date
|
||||
|
||||
if (
|
||||
selectedDate != null &&
|
||||
selectedDate != state.dayDate
|
||||
) {
|
||||
viewModel.selectDay(selectedDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScheduleDateHeader(
|
||||
date = state.dayDate,
|
||||
todayDate = state.todayDate,
|
||||
periods = schedule.periods,
|
||||
selectedWeekStart = state.weekStart,
|
||||
onSelectWeek = viewModel::selectWeek
|
||||
)
|
||||
|
||||
DayChips(
|
||||
days = weekDays,
|
||||
selectedDate = state.dayDate,
|
||||
todayDate = state.todayDate,
|
||||
pagerState = pagerState,
|
||||
onSelect = viewModel::selectDay
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f),
|
||||
key = { page ->
|
||||
weekDays[page].date.toEpochDay()
|
||||
}
|
||||
) { page ->
|
||||
|
||||
val day = weekDays.getOrNull(page)
|
||||
|
||||
if (day == null ||
|
||||
day.intervals.all { it.lessons.isEmpty() }
|
||||
) {
|
||||
ScheduleEmptyDay()
|
||||
} else {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = 16.dp,
|
||||
bottom = 24.dp
|
||||
)
|
||||
) {
|
||||
items(
|
||||
items = day.intervals.withIndex()
|
||||
.filter { (_, slot) ->
|
||||
slot.lessons.any { lesson ->
|
||||
!lesson.subject.isNullOrBlank() ||
|
||||
lesson.raw.isNotBlank()
|
||||
}
|
||||
},
|
||||
key = { it.index }
|
||||
) { (slotIndex, slot) ->
|
||||
LessonSlot(
|
||||
slotIndex = slotIndex,
|
||||
slot = slot
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
|
||||
@@ -187,3 +412,4 @@ fun ScheduleScreen(viewModel: ScheduleViewModel, onSwitchGroup: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -1,10 +1,13 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import agzam.vyatsu.domain.model.Day
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||
import androidx.compose.foundation.lazy.LazyListLayoutInfo
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
@@ -15,6 +18,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -22,6 +26,7 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import java.time.LocalDate
|
||||
@@ -90,44 +95,67 @@ internal fun DayChips(
|
||||
}
|
||||
}
|
||||
}
|
||||
LazyRow(
|
||||
state = listState,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = hPadding,
|
||||
vertical = 8.dp
|
||||
)
|
||||
) {
|
||||
items(
|
||||
items = days,
|
||||
key = { it.date.toEpochDay() }
|
||||
) { day ->
|
||||
|
||||
LazyRow(state = listState, horizontalArrangement = Arrangement.spacedBy(6.dp), contentPadding = PaddingValues(horizontal = hPadding, vertical = 2.dp),) {
|
||||
items(days, key = { it.date.toEpochDay() }) { day ->
|
||||
val selected = selectedDate == day.date
|
||||
val isTodayChip = todayDate == day.date
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
val isToday = todayDate == day.date
|
||||
|
||||
Surface(
|
||||
onClick = { onSelect(day.date) },
|
||||
label = {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
dayName(day.date.dayOfWeek),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
Text(
|
||||
day.date.format(CHIP_DATE),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
}
|
||||
modifier = Modifier.size(
|
||||
width = 72.dp,
|
||||
height = 72.dp
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
color = if (selected) {
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
},
|
||||
modifier = Modifier.padding(vertical = 2.dp),
|
||||
border = if (isTodayChip && !selected) {
|
||||
FilterChipDefaults.filterChipBorder(
|
||||
borderColor = MaterialTheme.colorScheme.primary,
|
||||
borderWidth = 1.dp,
|
||||
enabled = true,
|
||||
selected = false
|
||||
contentColor = if (selected) {
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
},
|
||||
border = if (isToday && !selected) {
|
||||
BorderStroke(
|
||||
width = 1.5.dp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
} else {
|
||||
FilterChipDefaults.filterChipBorder(enabled = true, selected = selected)
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
null
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = dayName(day.date.dayOfWeek),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = day.date.format(CHIP_DATE),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -115,4 +216,6 @@ internal fun LessonSlot(slotIndex: Int, slot: TimeSlot, showTeacher: Boolean = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,68 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import agzam.vyatsu.domain.model.SchedulePeriod
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.LocalDate
|
||||
|
||||
@Composable
|
||||
internal fun ScheduleDateHeader(
|
||||
date: LocalDate?,
|
||||
todayDate: LocalDate?,
|
||||
periods: List<SchedulePeriod>,
|
||||
selectedWeekStart: LocalDate?,
|
||||
onSelectWeek: (LocalDate) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = hPadding,
|
||||
vertical = 12.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = if (date == todayDate) {
|
||||
"Сегодня"
|
||||
} else {
|
||||
"Выбранный день"
|
||||
},
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(
|
||||
text = date?.let {
|
||||
"${dayName(it.dayOfWeek)} · ${it.format(FULL_DATE)}"
|
||||
} ?: "Дата не выбрана",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
// WeekSelector(
|
||||
// periods = periods,
|
||||
// selectedWeekStart = selectedWeekStart,
|
||||
// todayWeekStart = todayDate?.let { today ->
|
||||
// periods.firstOrNull { period ->
|
||||
// !today.isBefore(period.weekStart) &&
|
||||
// !today.isAfter(period.weekEnd)
|
||||
// }?.weekStart
|
||||
// },
|
||||
// onSelect = onSelectWeek
|
||||
// )
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package agzam.vyatsu.ui.schedule.components
|
||||
|
||||
import agzam.vyatsu.R
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
internal fun ScheduleEmptyDay() {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.event_busy_24dp),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Занятий нет",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "На этот день расписание пустое",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun ScheduleError(
|
||||
onRetry: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Не удалось загрузить расписание",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Button(onClick = onRetry) {
|
||||
Text("Повторить")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,8 +1,11 @@
|
||||
package agzam.vyatsu.ui.teacher
|
||||
|
||||
import agzam.vyatsu.R
|
||||
import agzam.vyatsu.ui.schedule.components.DayChips
|
||||
import agzam.vyatsu.ui.schedule.components.FULL_DATE
|
||||
import agzam.vyatsu.ui.schedule.components.LessonSlot
|
||||
import agzam.vyatsu.ui.schedule.components.ScheduleDateHeader
|
||||
import agzam.vyatsu.ui.schedule.components.ScheduleError
|
||||
import agzam.vyatsu.ui.schedule.components.WeekSelector
|
||||
import agzam.vyatsu.ui.schedule.components.dayName
|
||||
import agzam.vyatsu.ui.schedule.components.hPadding
|
||||
@@ -17,7 +20,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -28,6 +33,7 @@ import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -38,8 +44,231 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TeacherScheduleScreen(
|
||||
viewModel: TeacherScheduleViewModel,
|
||||
onSwitchTeacher: () -> Unit
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
windowInsets = WindowInsets(0.dp),
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = state.teacherName ?: "Расписание",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Расписание преподавателя",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
if (state.isLoading && state.schedule != null) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { viewModel.refresh() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.refresh_24dp),
|
||||
contentDescription = "Обновить"
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = onSwitchTeacher
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.sync_alt_24dp),
|
||||
contentDescription = "Сменить преподавателя"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { innerPadding ->
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
) {
|
||||
when {
|
||||
state.schedule == null && state.loadError -> {
|
||||
ScheduleError(
|
||||
onRetry = viewModel::refresh
|
||||
)
|
||||
}
|
||||
|
||||
state.schedule == null -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
val schedule = state.schedule!!
|
||||
|
||||
val weekDays = schedule.days
|
||||
|
||||
val periods = state.weeks.ifEmpty {
|
||||
listOf(schedule.period)
|
||||
}
|
||||
|
||||
if (weekDays.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Нет расписания на эту неделю")
|
||||
}
|
||||
} else {
|
||||
val selectedDayIndex = weekDays
|
||||
.indexOfFirst { it.date == state.dayDate }
|
||||
.coerceAtLeast(0)
|
||||
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = selectedDayIndex,
|
||||
pageCount = { weekDays.size }
|
||||
)
|
||||
|
||||
// Синхронизация выбора дня с Pager
|
||||
LaunchedEffect(
|
||||
state.dayDate,
|
||||
weekDays
|
||||
) {
|
||||
val targetPage = weekDays.indexOfFirst {
|
||||
it.date == state.dayDate
|
||||
}
|
||||
|
||||
if (
|
||||
targetPage >= 0 &&
|
||||
targetPage != pagerState.currentPage
|
||||
) {
|
||||
pagerState.scrollToPage(targetPage)
|
||||
}
|
||||
}
|
||||
|
||||
// Переключение дня свайпом
|
||||
LaunchedEffect(pagerState, weekDays) {
|
||||
snapshotFlow {
|
||||
pagerState.settledPage
|
||||
}.collect { page ->
|
||||
|
||||
val selectedDate =
|
||||
weekDays.getOrNull(page)?.date
|
||||
|
||||
if (
|
||||
selectedDate != null &&
|
||||
selectedDate != state.dayDate
|
||||
) {
|
||||
viewModel.selectDay(selectedDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScheduleDateHeader(
|
||||
date = state.dayDate,
|
||||
todayDate = state.todayDate,
|
||||
periods = periods,
|
||||
selectedWeekStart = state.weekStart,
|
||||
onSelectWeek = viewModel::selectWeek
|
||||
)
|
||||
|
||||
DayChips(
|
||||
days = weekDays,
|
||||
selectedDate = state.dayDate,
|
||||
todayDate = state.todayDate,
|
||||
pagerState = pagerState,
|
||||
onSelect = viewModel::selectDay
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.weight(1f),
|
||||
key = { page ->
|
||||
weekDays[page].date.toEpochDay()
|
||||
}
|
||||
) { page ->
|
||||
|
||||
val day = weekDays.getOrNull(page)
|
||||
|
||||
val visibleSlots = day?.intervals
|
||||
?.withIndex()
|
||||
?.filter { (_, slot) ->
|
||||
slot.lessons.any { lesson ->
|
||||
!lesson.subject.isNullOrBlank() ||
|
||||
lesson.raw.isNotBlank()
|
||||
}
|
||||
}
|
||||
.orEmpty()
|
||||
|
||||
if (visibleSlots.isEmpty()) {
|
||||
ScheduleEmptyDay()
|
||||
} else {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement =
|
||||
Arrangement.spacedBy(12.dp),
|
||||
contentPadding = PaddingValues(
|
||||
top = 16.dp,
|
||||
bottom = 24.dp
|
||||
)
|
||||
) {
|
||||
items(
|
||||
items = visibleSlots,
|
||||
key = { it.index }
|
||||
) { (slotIndex, slot) ->
|
||||
LessonSlot(
|
||||
slotIndex = slotIndex,
|
||||
slot = slot,
|
||||
showTeacher = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScheduleEmptyDay() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
/*
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher: () -> Unit) {
|
||||
@@ -185,4 +414,6 @@ fun TeacherScheduleScreen(viewModel: TeacherScheduleViewModel, onSwitchTeacher:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -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>
|
||||
@@ -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
@@ -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;">Обновление 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;">Долженкова М.Л.</SPAN></TD>
|
||||
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Коржавина А.С.</SPAN></TD>
|
||||
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Мельцов В.Ю.</SPAN></TD>
|
||||
<TD CLASS="R1C2"><SPAN STYLE="white-space:nowrap;max-width:0px;">Прозоров Д.Е.</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;">понедельник 28.09.26 </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;">вторник 29.09.26 </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;">среда 30.09.26 </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;">четверг 01.10.26 </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;">пятница 02.10.26 </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;">суббота 03.10.26 </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;">понедельник 05.10.26 </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;">вторник 06.10.26 </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;">среда 07.10.26 </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;">четверг 08.10.26 </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;">пятница 09.10.26 </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;">суббота 10.10.26 </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;"> </DIV></TD>
|
||||
</TR>
|
||||
</TABLE>
|
||||
</BODY>
|
||||
</HTML>
|
||||
Reference in New Issue
Block a user