feat: Add Settings and Subjects screens with theme reveal functionality

- Implemented SettingsScreen for user preferences including theme selection, reminders, and notifications.
- Created SubjectsScreen to display subjects with upcoming lessons and associated details.
- Introduced ThemeReveal for a smooth transition between light and dark themes.
- Added support for managing notes and tasks related to subjects.
- Enhanced UI components for better user experience and accessibility.
This commit is contained in:
2026-10-24 02:47:37 +03:00
parent 416527fb33
commit 297091cb0e
54 changed files with 182588 additions and 172428 deletions

View File

@@ -5,7 +5,8 @@ import { roomMoves } from "./rooms.js";
// whole text as the subject. These checks find such cells after parsing, so a new format or a
// typo in Word shows up as soon as the document is published instead of when a student notices.
const MONTHS = /(?<![а-яё])(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)[а-яё]*/iu;
// "мая" is a month, "Майнор" is not.
const MONTHS = /(?<![а-яё])(?:(?:январ|феврал|март|апрел|июн|июл|август|сентябр|октябр|ноябр|декабр)[а-яё]*|ма[йя](?![а-яё]))/iu;
// The longest semester has about twenty weeks; "27н." is a typo, not a week.
const LAST_WEEK = 24;

View File

@@ -10,7 +10,7 @@ import { parseWeekRanges, parseTextDates, DATE_LIST, weekday } from "../shared/w
import { roomMoves, onlyPlace, weekPlan, partsByWeek } from "./rooms.js";
import { placeNote, lessonPlace, placeOf } from "./places.js";
const run = promisify(execFile);
export const PARSER_VERSION = 18;
export const PARSER_VERSION = 19;
const days = [
"понедельник",
"вторник",
@@ -20,10 +20,17 @@ const days = [
"суббота",
"воскресенье",
];
const clean = (value) =>
export const clean = (value) =>
value
.replace(/\u00a0/g, " ")
// "Ф и з и ч е с к а я к у л ь т у р а": letters spaced out for show, words apart by
// several spaces. Joined back before the spaces collapse: "Физическая культура".
.replace(/(?<![\p{L}\d])(?:\p{L} ){3,}\p{L}(?![\p{L}\d])/gu, (word) => word.replace(/ /g, ""))
// "со 2 по 14 нед.": the usual "2-14 нед.".
.replace(/(?<![\p{L}\d])(?:с|со)\s*(\d{1,2})\s*по\s*(\d{1,2})(?=\s*н(?:ед)?\.?)/giu, "$1-$2")
.replace(/[\t ]+/g, " ")
.replace(/«\s+/g, "«")
.replace(/\s+»/g, "»")
.replace(/\s*\n\s*/g, "\n")
.trim();
// A group is a two-digit year and a capitalised abbreviation: "23-БА", "24-ДОи", "23-ИНС-1",
@@ -595,7 +602,9 @@ export function parseDocx(buffer, source = {}) {
date = null,
time = null,
pair = null,
timeCol = null;
timeCol = null,
// The day's first lesson time: a town note further down holds from its own row on.
dayStart = null;
// Session schedules use separate room, subject and teacher columns.
const session = rows[header].some((cell) => /пара/i.test(cell.text));
let sessionColumns = null;
@@ -625,6 +634,7 @@ export function parseDocx(buffer, source = {}) {
: null;
time = null;
pair = null;
dayStart = null;
}
if (!day) continue;
// Credits and exams give only the start ("8.30"); the bell table supplies the end.
@@ -637,6 +647,7 @@ export function parseDocx(buffer, source = {}) {
pair = session ? row.find((c) => c.col === 1)?.text : null;
}
if (!time) continue;
dayStart ??= time.start;
seenRows++;
const continuedTime =
row.find((c) => c.col === timeCol)?.merge === "continue";
@@ -667,7 +678,9 @@ export function parseDocx(buffer, source = {}) {
// are in that town. A lesson may follow it in the same cell.
const note = placeNote(raw);
if (note) {
places.push({ day, date, groups: targetGroups, subgroups: columnSubgroups, place: note.place, text: note.text });
// "Занятия в Полоцке" in the 13:40 row of 25-ИТ: the morning is in Novopolotsk.
const from = time.start > dayStart ? time.start : null;
places.push({ day, date, groups: targetGroups, subgroups: columnSubgroups, place: note.place, text: note.text, ...(from && { from }) });
raw = note.rest;
if (!raw.trim()) return;
}

View File

@@ -64,7 +64,12 @@ export function placeOf(lesson, places, labelsOf) {
let found = null;
for (const group of lesson.groups) {
const notes = places.filter(
(p) => p.day === lesson.day && (!p.date || p.date === lesson.date) && p.groups.includes(group),
(p) =>
p.day === lesson.day &&
(!p.date || p.date === lesson.date) &&
p.groups.includes(group) &&
// A note from the middle of the day holds from its row on.
(!p.from || lesson.start >= p.from),
);
if (!notes.length) return null;
const label = lesson.subgroups.find((s) => s.group === group)?.label;