- 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.
89 lines
4.1 KiB
JavaScript
89 lines
4.1 KiB
JavaScript
// Towns of a lesson, as Word cells name them. Most lessons are in Novopolotsk; some faculties
|
||
// teach in Polotsk, and a cell says when a lesson or a whole day is in the other town:
|
||
// "Занятия в Полоцке" over a day's first row (that day, the column's lessons are in Polotsk),
|
||
// "Новополоцк" or "г.Новополоцк" on a line of its own, "246(Новополоцк)" after a room.
|
||
|
||
// Towns a cell may name without "г.": a bare capitalised word is otherwise a subject.
|
||
const TOWNS = "Новополоцк|Полоцк|Междуречье|Витебск|Минск";
|
||
const TOWN = String.raw`(?:г\.\s*([А-ЯЁ][а-яё]+(?:-[А-ЯЁ][а-яё]+)?)|(${TOWNS})е?)`;
|
||
// "Занятия в Полоцке", "Занятия проходят в г. Новополоцк": the rest of the line, if any, is a lesson.
|
||
const NOTE = new RegExp(
|
||
String.raw`^[Зз]анятия\s+(?:(?:проход[яи]т|будут\s+проходить)\s+)?в\s+(?:г\.\s*)?([А-ЯЁ][а-яё]+(?:-[А-ЯЁ][а-яё]+)?)(?=$|[\s.,;:!–—-])[.!]?`,
|
||
"u",
|
||
);
|
||
// A line that starts with the town: "г.Новополоцк", or "г. Полоцк, Дом офицеров" with the venue.
|
||
const LINE = new RegExp(String.raw`^\(?${TOWN}\)?(?:[.,]?$|(,\s*\S.*))`, "u");
|
||
// "246(Новополоцк)", "121 (г.Новополоцк)".
|
||
const AFTER_ROOM = new RegExp(String.raw`(?<=\d[А-ЯЁа-яё]{0,2})\s*\(${TOWN}\)`, "u");
|
||
|
||
/** "Полоцке" → "Полоцк", "Витебске" → "Витебск"; "Междуречье" stays. */
|
||
export const townName = (word) => word.replace(/(?<=(?:к|ст))е$/u, "");
|
||
|
||
/**
|
||
* A note that a day is in another town, on a line of the cell: `{text, place, rest}`, the
|
||
* cell without the note in `rest`; null when the cell has none.
|
||
*/
|
||
export function placeNote(text) {
|
||
const lines = (text || "").split("\n");
|
||
for (const [i, line] of lines.entries()) {
|
||
const m = line.trim().match(NOTE);
|
||
if (!m) continue;
|
||
const rest = [...lines.slice(0, i), line.trim().slice(m[0].length).trim(), ...lines.slice(i + 1)]
|
||
.filter((l) => l.trim())
|
||
.join("\n");
|
||
return { text: m[0].replace(/[.!]$/, "").replace(/\s+/g, " "), place: townName(m[1]), rest };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** The town a lesson's own text names, and the text without it: `{place, rest}`. */
|
||
export function lessonPlace(text) {
|
||
let place = null;
|
||
const rest = (text || "")
|
||
.split("\n")
|
||
.filter((line) => {
|
||
const m = line.trim().match(LINE);
|
||
if (m && !place) place = townName(m[1] || m[2]);
|
||
// The town alone goes; a venue after it stays in the text.
|
||
return !m || m[3];
|
||
})
|
||
.join("\n")
|
||
.replace(AFTER_ROOM, (_, named, town) => {
|
||
place ||= townName(named || town);
|
||
return "";
|
||
});
|
||
return { place, rest };
|
||
}
|
||
|
||
/**
|
||
* The town of a lesson from the notes of its day: for every group of the lesson, a note for its
|
||
* subgroup or the whole group, or — for a lesson of the whole group — notes in every subgroup
|
||
* that name the same town. `labelsOf(group)`: the group's subgroup labels.
|
||
*/
|
||
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) &&
|
||
// 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;
|
||
const own = (p) => p.subgroups.filter((s) => s.group === group).map((s) => s.label);
|
||
let place = notes.find((p) => !own(p).length || (label && own(p).includes(label)))?.place;
|
||
if (!place && !label) {
|
||
const towns = new Map(notes.flatMap((p) => own(p).map((l) => [l, p.place])));
|
||
const labels = labelsOf(group);
|
||
if (labels.length && labels.every((l) => towns.has(l)) && new Set(towns.values()).size === 1)
|
||
place = towns.get(labels[0]);
|
||
}
|
||
if (!place || (found && found !== place)) return null;
|
||
found = place;
|
||
}
|
||
return found;
|
||
}
|