- Implemented a new Next Lesson widget to display the upcoming lesson details on the home screen. - Updated Today widget to dynamically adjust the number of lessons displayed based on available space. - Enhanced the Settings screen to allow users to manage hidden lessons more effectively. - Introduced harmonized color palettes for better visual consistency across themes. - Updated the timetable API to support a new range parameter for semester-based queries. - Added tests to ensure the new semester range functionality works as expected. - Refactored widget styling for improved appearance on devices with different Android versions.
151 lines
28 KiB
Markdown
151 lines
28 KiB
Markdown
# Расписание ПГУ
|
||
|
||
Первый рабочий PWA-клиент для расписаний Полоцкого государственного университета. При первом запуске предлагает найти свою группу; исходный сценарий проверки — **ФЭФ, 23-БА** из общего документа `23-ЛГ_23-БА.docx` за 2026–2027 учебный год.
|
||
|
||
## Запуск
|
||
|
||
Нужны Node.js 22.12+ (проверено на 24.14.1), npm и curl.
|
||
|
||
```sh
|
||
npm ci
|
||
npm run build
|
||
npm start
|
||
```
|
||
|
||
Откройте http://127.0.0.1:3001. Этот режим включает service worker, установку PWA и офлайн-кэш. Для разработки с обновлением интерфейса:
|
||
|
||
```sh
|
||
npm run dev
|
||
```
|
||
|
||
Vite работает на http://127.0.0.1:5173 и проксирует `/api` на порт 3001 (другой порт — через `API_PORT`). В режиме разработки service worker не регистрируется. `PORT` и `HOST` настраивают сервер; при размещении нужен HTTPS. Для доступа с телефона используйте HTTPS-хостинг либо локальный HTTPS-туннель.
|
||
|
||
## Возможности
|
||
|
||
- Живой каталог публичного Nextcloud университета: факультет, форма, курс и поиск по названию группы/файла. Проверенный каталог содержит 113 документов Word.
|
||
- Расписание по дням или на всю неделю, поиск предмета, преподавателя и аудитории.
|
||
- Экран «Группы»: поиск группы по номеру в любом написании («23-БА», «23 ба», «23ба») по всем документам каталога, без учёта фильтров; одно нажатие открывает нужный документ и группу. Ниже — текущая группа с подгруппой и соседними группами документа, недавние группы (открываются и без сети) и каталог чипами: факультет → курс → форма обучения. Первый запуск начинается с этого экрана, а не с чьей-то группы (`/api/groups`).
|
||
- Общие ячейки попадают в каждую соответствующую группу, чужие занятия отсекаются.
|
||
- Отдельные карточки для альтернативных занятий и условий по неделям. Сохраняются исходный текст, цвет ячейки, подгруппы, даты и ограничения.
|
||
- Учебные недели по таблице университета: переключатель недель с датами, показ только занятий выбранной недели, статус занятия («ещё 4 занятия · до 27 нояб.», «начнётся 20 нояб.») и сетка недель в деталях. Режим «Все строки Word» показывает документ как есть.
|
||
- Номера пар и текущая пара («Идёт · ещё 25 мин») по расписанию звонков.
|
||
- «Что изменилось»: когда университет перепубликует документ, приложение сравнивает новую версию с сохранённой и показывает для вашей группы изменённые поля (аудитория, время, преподаватель, недели), добавленные и удалённые занятия. Последний список изменений открывается из меню.
|
||
- Моя подгруппа и скрытые занятия: в «Группах» можно выбрать подгруппу — занятия другой подгруппы пропадут; любое занятие можно скрыть из его карточки и вернуть кнопкой «Скрыто · показать». Настройки хранятся только в этом браузере.
|
||
- **Поиск по университету** (вкладка «Поиск»): преподаватель или аудитория — занятия на выбранный день по всем расписаниям каталога; свободные аудитории на дату и пару. Сервер разбирает все документы каталога в фоне (первый раз около 20–30 секунд, дальше из кэша) и считает занятость с учётом недель, цвета строк, семестра и разовых переносов аудиторий вида «432 (07.10-ауд.272)». Дистанционные занятия («200 (ДО)») аудиторию не занимают. Каждый ответ сообщает, сколько документов учтено; «свободна» означает «свободна по расписаниям из Word», мероприятия и закрытые аудитории расписание не знает. Работает только онлайн.
|
||
- **Сессия**: экзамены, зачёты и консультации группы из всех документов каталога (распознаются по «(экз.)», «(зач.)», «(конс.)» и словам «Экзамен», «Зачёт», «Консультация»). На главном экране — карточка с обратным отсчётом до ближайшего экзамена или зачёта, полный список — в листе «Сессия». Список сохраняется для просмотра без сети. В текущем каталоге документов экзаменационной сессии ещё нет; формат проверен на образцах, а не на реальных файлах университета.
|
||
- Ссылка и QR-код на расписание группы (`/?doc=<id>&group=<группа>`) из меню «Поделиться расписанием». Ссылка открывает документ из каталога и нужную группу; подгруппа и скрытые занятия не передаются. Устаревшая ссылка (документ убран из каталога) открывает поиск группы. Для импортированных файлов ссылка недоступна.
|
||
- Фильтр белых/зелёных строк. Объединённое на обе строки занятие показывается в обоих вариантах без дублирования.
|
||
- Импорт `.docx` и `.doc` до 10 МБ с предварительным просмотром перед сохранением.
|
||
- Сохранение последних 12 документов и выбранной группы в этом браузере. После первой успешной загрузки собранной PWA интерфейс, шрифты и расписание доступны без сервера/сети.
|
||
- Манифест PWA, PNG-иконки, установка на главный экран и ссылка на исходный Word.
|
||
|
||
## Источник и обновления
|
||
|
||
[Каталог университета](https://nextcloud.psu.by/index.php/s/no5zKbMrns6j5jQ) читается через публичный DAV. Сервер обращается только к этой общей папке. Другие ссылки не принимаются.
|
||
|
||
**Учебный год выбирается сам.** Сервер читает корень каталога, находит папки вида `2026-2027` и с 1 сентября (Europe/Minsk) берёт папку нового года. Если её ещё нет, используется самая новая из прошлых, а `/api/health` предупреждает об этом. Таблица белых и зелёных недель и расписание звонков ищутся по смыслу имени («бел» + «зел» + «недел», «звонк»), сначала в папке года, затем в корне. Можно выложить несколько таблиц недель: они объединяются, при совпадении дат побеждает более новый файл. В список расписаний эти справочные файлы не попадают.
|
||
|
||
**Семестры.** Таблица делится на семестры там, где нумерация недель начинается заново или между неделями больше трёх недель. Расписание связывается со своим семестром по тексту Word («осенний семестр 2026/2027»), расписание сессии — по своим датам, документ без указания семестра — по семестру, в котором сегодняшняя дата. Если семестр не найден (например, весенней таблицы ещё нет), даты не рассчитываются и показываются все строки Word.
|
||
|
||
**Обновления у студента.** При запуске и при возвращении в приложение, если прошло больше часа, клиент сверяет сохранённое расписание с каталогом. Тот же файл с новой датой тихо перечитывается. Если для группы появился более новый документ (следующий семестр или год), показывается предложение его открыть; отказ запоминается. Если документ исчез из каталога, приложение предлагает найти группу заново.
|
||
|
||
Каталог и папка года перечитываются раз в 15 минут, таблица недель и звонки — раз в 6 часов, распознанный документ — при изменении даты файла или версии парсера. «Обновить» принудительно перечитывает всё. Если источник недоступен, используются сохранённые копии с явной пометкой. Фоновая синхронизация при закрытом приложении не реализована.
|
||
|
||
`GET /api/health` без обращения к сети показывает выбранный учебный год, возраст каталога, найденные файлы недель и звонков, семестры и предупреждения: папка нового года не найдена, сегодняшняя дата вне таблицы недель, источник не обновляется.
|
||
|
||
```sh
|
||
npm run sync
|
||
```
|
||
|
||
Серверный кэш находится в `.data/` (исключён из Git). Загрузки пользователей обрабатываются в памяти; временные файлы конвертации `.doc` удаляются. Данные из университетского каталога могут сохраняться в серверном кэше. Для офлайн-просмотра расписания хранятся в `localStorage`; очистка данных браузера удалит сохранённые документы. Аналитики и учётных записей нет.
|
||
|
||
## Границы парсера
|
||
|
||
Основной парсер читает XML и сетку таблиц `.docx`, включая горизонтальные и вертикальные объединения ячеек. Поддерживаются проверенные дневные шаблоны ФЭФ, ФИТ, ФКНЭ, ИСФ и ГФ, а также заочные установочные сессии с отдельными колонками предмета, аудитории и преподавателя. Это не универсальный парсер любых документов Word: без заголовков групп, дней и времени импорт возвращает понятную ошибку.
|
||
|
||
**Даты занятий рассчитываются по университетской таблице «Расписание белых и зеленых недель»** из корня каталога: неделя 1 начинается 01.09.2026 (вторник), нечётные недели белые, чётные зелёные. Пометка в белой или зелёной строке относится только к неделям этого цвета: `1-13н` в белой строке — недели 1, 3, …, 13, а в объединённой строке — каждая неделя. `с 3н.` означает «до конца таблицы». Текстовые даты вроде «15 октября» привязываются к своему дню. Нераспознанные пометки (`2,6-12-16н`) не скрываются, а помечаются для сверки с Word; исходная пометка всегда видна. Разбор пометок общий для сервера и браузера (`shared/weeks.js`), поэтому старые сохранённые расписания тоже получают даты. Вне периода таблицы приложение показывает все строки Word. Номера подгрупп соответствуют столбцам внутри группы; именованных подгрупп в этих шаблонах нет.
|
||
|
||
Поля предмета, преподавателя и аудитории извлекаются по типовым обозначениям; сложные многострочные записи могут содержать несколько преподавателей/аудиторий. Поэтому полный текст всегда доступен в деталях. Если в ячейке явно указано особое время, например `16.50` для кураторского часа, оно имеет приоритет; окончание не выдумывается.
|
||
|
||
Для старых `.doc` на сервере нужен LibreOffice Writer (`soffice` в PATH или `LIBREOFFICE_BIN`). Конвертация может изменить сложное форматирование, результат нужно сверить. Реальные документы выбранного источника — `.docx`; поддержка `.doc` проверяется отдельно на образце, преобразованном LibreOffice.
|
||
|
||
PDF, Excel, изображения и сканы пока не импортируются. Они не выдаются за распознанное расписание.
|
||
|
||
## Проверки
|
||
|
||
```sh
|
||
npm test
|
||
npm run build
|
||
```
|
||
|
||
Тесты используют реальные Word-документы источника в `test/fixtures/` с URL и метаданными рядом. Проверяются изоляция 23-БА/23-ЛГ, общие события, объединения ячеек, варианты недель, отдельное время кураторского часа, даты сессии и отклонение повреждённых файлов, а также таблица недель и звонков, разбор пометок с учётом цвета строки и согласие всех пометок 23-БА с таблицей.
|
||
|
||
## Docker
|
||
|
||
```sh
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Приложение откроется на http://localhost:3001. Кэш каталога, таблицы недель и распознанных документов хранится в томе `psu-data` и переживает пересборку. Первый запуск разбирает все документы каталога (около полуминуты), поэтому проверка здоровья (`/api/health`) даёт контейнеру минуту на старт. Порт и адрес меняются переменными `APP_PORT` и `APP_BIND`.
|
||
|
||
Для установки PWA на телефон нужен HTTPS. Профиль `https` поднимает Caddy, который сам получает сертификат Let's Encrypt (домен должен указывать на сервер, порты 80 и 443 открыты):
|
||
|
||
```sh
|
||
DOMAIN=schedule.example.by docker compose --profile https up -d --build
|
||
```
|
||
|
||
Если HTTPS уже даёт ваш обратный прокси, профиль не нужен; приложение можно закрыть от внешней сети через `APP_BIND=127.0.0.1`. Контейнер включает curl и LibreOffice для `.doc`. Файл `docker-compose.yml` проверен `docker compose config`; сборка образа в этой проверке не запускалась.
|
||
|
||
## Android-приложение
|
||
|
||
В папке `android/` — нативный клиент на Kotlin и Jetpack Compose с компонентами Material 3 Expressive (`MaterialExpressiveTheme`, `ModalWideNavigationRail` и `WideNavigationRail`, соединённые `ToggleButton`, `LoadingIndicator`). Базовая тема синяя (тональная схема M3); с «Цветами обоев» — динамическая, а цвета предметов и зелёной недели гармонизируются с ней. Разделы открываются из бокового меню (на планшете и в альбомной ориентации — постоянная боковая панель); карточки лежат на подложке с оттенком, системные панели следуют теме приложения.
|
||
|
||
- Обзор: отчёт за неделю (сколько пар прошло), следующие занятия сегодня с обратным отсчётом и ближайший учебный день.
|
||
- Расписание группы по дням с листанием или недельной сеткой по часам; неделя и её цвет, номера пар, текущая пара, детали занятия с исходной записью Word. Если семестр не найден в таблице недель — строки Word по дням недели.
|
||
- Предметы: пары по дням текущей недели; у каждого предмета — сокращение как в недельной сетке, следующее занятие с аудиторией, преподаватели и сколько пар на этой неделе. У каждого предмета свой постоянный цвет во всех разделах.
|
||
- Планы: задания и заметки по дням — к занятию или общие, без привязки к паре, на выбранный день, просроченные сверху, «+» прикрепляет задание к ближайшему занятию. Три макета, как в School Planner: «Повестка дня» (все дни с липкими заголовками и полосой недели, которая следует за списком), «Список» (только дни с делами) и «Доска» (колонки дней). В настройках вида — пары дня и выполненные задания.
|
||
- Календарь: месяц с отметками дней с парами и с планами, зелёные недели подсвечены; под ним лист с планами и парами выбранного дня. Лист можно поднять — месяц сворачивается до выбранной недели.
|
||
- Обратный отсчёт до сессии в «Обзоре»: дни до ближайшего экзамена или зачёта из `/api/exams` (консультации — только в списке), по нажатию — вся сессия. Пока университет не опубликовал сессию, отсчёт идёт до конца семестра. Экзамены кэшируются на устройстве.
|
||
- Задания и заметки к паре: из карточки занятия — к этой паре или к следующей паре того же предмета. Задания отмечаются выполненными, просроченные подсвечиваются; список — в «Обзоре», по предмету — в «Предметах», точка на блоке в недельной сетке. Хранятся только на устройстве (`notes.json`) и привязаны к группе.
|
||
- Посещаемость: «был» или «пропустил» в карточке занятия и одним касанием в «Обзоре» для прошедших сегодня пар; статистика по каждому предмету в «Предметах». Хранится на устройстве (`attendance.json`).
|
||
- Скрытие занятий: «Скрыть такие занятия» в карточке занятия убирает предмет с этим типом занятия из расписания, обзора, виджетов и напоминаний; вернуть можно в настройках.
|
||
- Второй виджет «Следующая пара» (4×1) и плитка в шторке быстрых настроек с текущей или следующей парой и аудиторией.
|
||
- Настройки отображения: цвета предметов, преподаватели, аудитории, номера пар, метки занятий и пометки из Word можно скрыть; полные сведения остаются в карточке занятия.
|
||
- Группы (кнопка группы в верхней панели «Обзора»; если у группы есть подгруппы, после выбора приложение предложит свою): поиск по номеру во всём каталоге, недавние, каталог по факультету и курсу, выбор подгруппы.
|
||
- Поиск по университету: занятия преподавателя или аудитории на выбранный день.
|
||
- Виджет «Сегодня» (Glance): текущая и следующая пара, обновляется каждые 15 минут без сети.
|
||
- Напоминания перед парой за 5–30 минут — локальные будильники из сохранённого расписания, без push-сервера. С разрешением «Будильники и напоминания» приходят точно в срок, без него — в окне ±5 минут.
|
||
- Офлайн: последнее расписание хранится на устройстве; каждые 6 часов WorkManager обновляет его, виджет и напоминания.
|
||
|
||
Даты считает сервер: `GET /api/timetable?doc=<id>&group=<группа>&from=ГГГГ-ММ-ДД&days=N` (до 62 дней) или с `&range=semester` — весь текущий семестр от первого до последнего дня; приложение хранит его целиком и работает с ним без сети. Ответ содержит дни с неделями, номерами пар и пометкой `uncertain` для нераспознанных пометок, плюс строки Word (`weekly`) и подгруппы. Поэтому правила белых и зелёных недель в Kotlin не дублируются.
|
||
|
||
Сборка (JDK 17, Android SDK 37):
|
||
|
||
```sh
|
||
cd android
|
||
./gradlew assembleDebug
|
||
```
|
||
|
||
Отладочная сборка ходит на `http://10.0.2.2:3010/` — это сервер из `docker compose` на компьютере, видимый из эмулятора (`APP_PORT=3010`). Другой адрес: `-PPSU_API_URL=…`. Релизная сборка принимает только HTTPS-адрес. Идентификатор приложения `ritm.schedule` — рабочий, перед публикацией его стоит заменить своим.
|
||
|
||
## Структура
|
||
|
||
- `src/App.tsx` — интерфейс, каталог и импорт с предпросмотром.
|
||
- `src/storage.ts` — сохранённые расписания, предпочтения и календарь недель.
|
||
- `src/calendar.ts`, `src/icons.tsx` — подписи дат и пар, иконки Material Symbols.
|
||
- `shared/weeks.js` — разбор пометок недель и расчёт дат, общий для сервера и браузера.
|
||
- `shared/lessons.js` — устойчивый ключ занятия, сравнение версий расписания, фильтр подгруппы и скрытых занятий.
|
||
- `shared/updates.js` — правила проверки обновлений сохранённого расписания.
|
||
- `server/directory.js` — индекс всех документов: преподаватели и аудитории из текста ячеек, занятость на дату, свободные аудитории, экзамены группы (`/api/directory/search`, `/api/directory/day`, `/api/directory/free`, `/api/exams`).
|
||
- `server/schedules.js` — разбор документа каталога с дисковым кэшем.
|
||
- `src/Search.tsx` — экран «Поиск».
|
||
- `server/source.js` — DAV-каталог, фиксированный источник и кэш; таблица недель и звонков из корня каталога (`/api/calendar`, кэш 6 часов).
|
||
- `server/calendar.js` — разбор таблицы недель и расписания звонков.
|
||
- `server/parser.js` — таблицы `.docx`, условия и преобразование `.doc`.
|
||
- `server/index.js` — API и выдача собранной PWA.
|
||
- `scripts/serviceworker.js` — генерация полного офлайн-кэша при сборке.
|
||
|
||
Это независимый клиент; университет остаётся источником расписания. Шрифт Roboto Flex поставляется локально под SIL Open Font License (`public/fonts/OFL-RobotoFlex.txt`).
|
||
|
||
Тема выбирается в меню: «Авто» (как в системе), «Светлая» или «Тёмная». Выбор хранится в браузере и применяется до первой отрисовки страницы, вместе с цветом строки состояния. QR-коды строятся в браузере библиотекой `qrcode-generator` (MIT). Иконки — Material Symbols Rounded (`@material-symbols/svg-400`, Apache 2.0), встроены в сборку и работают без сети. Интерфейс использует веб-адаптацию Material 3 Expressive: токены цвета и типографики, тональные поверхности, выразительные формы, навигационную панель и rail. Главный экран содержит расписание; импорт, установка и сведения о Word доступны в меню.
|