- Implemented `Skeleton.kt` for loading placeholders in the app, including lesson cards, schedule, overview, and subjects. - Introduced `Undo.kt` to manage undo actions with a snackbar interface. - Created `WeekWidget.kt` for a widget displaying the week's schedule, including dynamic updates based on the current timetable. - Added various splash animations in the `animator` directory for a smooth launch experience. - Designed drawable resources for the widget and splash screen, including icons for city and room moves. - Defined XML resources for widget configuration and photo paths.
177 lines
7.7 KiB
JavaScript
177 lines
7.7 KiB
JavaScript
// A group's timetable by date, with week numbers already applied. Native and
|
||
// third-party clients use it instead of re-implementing the white/green week rules.
|
||
import { semesterView, addDays, weekday, weekIndexOf } from "../shared/weeks.js";
|
||
import { lessonKey, subgroupsOf } from "../shared/lessons.js";
|
||
import { placesOn } from "../shared/places.js";
|
||
import { occurrence, peopleAndRooms, roomsOn, teachersOn, movedOn } from "./directory.js";
|
||
import { apiError } from "./errors.js";
|
||
|
||
const MAX_DAYS = 62;
|
||
// A whole semester with its session fits easily; the cap only guards against abuse.
|
||
const MAX_SEMESTER_DAYS = 200;
|
||
const DAY_MS = 86_400_000;
|
||
|
||
// On a date: the room and teacher of that day and week ("1-9н. 502н", a move on 26.10).
|
||
function lessonOut(lesson, group, people, date, status, calendar, week = null) {
|
||
const bell = calendar?.bells.find((b) => b.start === lesson.start);
|
||
const rooms = date ? roomsOn(people, date, week) : people.rooms;
|
||
return {
|
||
key: lessonKey(lesson, group),
|
||
start: lesson.start,
|
||
end: lesson.end || bell?.end || null,
|
||
pair: bell?.pair ?? null,
|
||
subject: lesson.subject,
|
||
type: lesson.type,
|
||
teachers: date ? teachersOn(people, week) : people.teachers,
|
||
rooms,
|
||
roomMoved: Boolean(date && movedOn(people, date)),
|
||
// "Полоцк": the town, when the cell or the day's note names one.
|
||
place: lesson.place || null,
|
||
distance: people.distance,
|
||
condition: lesson.condition,
|
||
variant: lesson.variant,
|
||
subgroup: lesson.subgroups.find((s) => s.group === group)?.label || null,
|
||
// "Немецкий язык" when part of the group studies German and part French (see `choices`).
|
||
option: lesson.option || null,
|
||
// "foreign": for foreign students only (Russian as a foreign language); "local": for the others.
|
||
audience: lesson.audience || null,
|
||
groups: lesson.groups,
|
||
uncertain: status === "maybe",
|
||
raw: lesson.raw,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* days: one entry per date from `from`, with the lessons that take place that day.
|
||
* weekly: the Word rows by weekday, for when dates cannot be calculated.
|
||
*/
|
||
const groupKey = (g) => g.toLowerCase().replace(/[\s\-–]/g, "");
|
||
|
||
export function timetable({ file, schedule, calendar, group, from, days, range, today, session: extra = [] }) {
|
||
if (!schedule.groups.includes(group)) {
|
||
// The same group written another way: "24-ИЯ(РА)и-1" for "24-ИЯ (РА)и-1".
|
||
const same = schedule.groups.find((g) => groupKey(g) === groupKey(group));
|
||
// A name parser 13 split into groups: a client may still keep "24-ИЯ(РА)".
|
||
const split = schedule.groups.filter((g) => groupKey(g).startsWith(groupKey(group)));
|
||
if (same) group = same;
|
||
else
|
||
throw apiError(
|
||
404,
|
||
"group_not_found",
|
||
split.length
|
||
? `Группа ${group} теперь разделена: ${split.join(", ")}. Выберите свою в списке групп.`
|
||
: `В документе нет группы ${group}.`,
|
||
);
|
||
}
|
||
let count = Math.min(Math.max(Number(days) || 7, 1), MAX_DAYS);
|
||
const lessons = schedule.lessons.filter((l) => l.groups.includes(group));
|
||
const labels = subgroupsOf(schedule, group);
|
||
// A session schedule printed with a date on every day needs no week table: its own dates
|
||
// say when each lesson takes place, even in a year the table no longer covers.
|
||
const sessionDates = lessons.every((l) => l.date || l.dates?.length)
|
||
? lessons.flatMap((l) => (l.date ? [l.date] : l.dates)).sort()
|
||
: [];
|
||
const session = sessionDates.length
|
||
? {
|
||
id: `session-${sessionDates[0]}`,
|
||
label: schedule.period || "Сессия",
|
||
start: sessionDates[0],
|
||
end: sessionDates.at(-1),
|
||
}
|
||
: null;
|
||
// range=semester: every day of the semester `today` falls in, so a client can keep
|
||
// the whole term offline; a session schedule keeps its own span. Without either it
|
||
// keeps the plain from/days window. Exams and credits from other documents may follow
|
||
// the last week of the semester: the span then runs on to the last of them (up to 60 days).
|
||
if (range === "semester") {
|
||
const current = semesterView(calendar, schedule, today || from);
|
||
const span = current?.semester || session;
|
||
if (span) {
|
||
from = span.start;
|
||
const limit = addDays(span.end, 60);
|
||
const end = [span.end, ...extra.flatMap((e) => (e.lesson.date ? [e.lesson.date] : e.lesson.dates))]
|
||
.filter((d) => d >= from && d <= limit)
|
||
.sort()
|
||
.at(-1);
|
||
const length = Math.round((Date.parse(end) - Date.parse(from)) / DAY_MS) + 1;
|
||
count = Math.min(Math.max(length, 1), MAX_SEMESTER_DAYS);
|
||
}
|
||
}
|
||
const entries = lessons.map((lesson) => ({
|
||
doc: { id: file.id, schedule },
|
||
lesson,
|
||
...peopleAndRooms(lesson, schedule.academicYear),
|
||
}));
|
||
const view = semesterView(calendar, schedule, from);
|
||
const views = new Map();
|
||
const out = [];
|
||
for (let i = 0; i < count; i++) {
|
||
const date = addDays(from, i);
|
||
const week = view ? view.weeks[weekIndexOf(view, date)] || null : null;
|
||
const dayLessons = [];
|
||
if (view || session)
|
||
for (const entry of entries) {
|
||
const status = occurrence(entry, date, calendar, views);
|
||
if (status === "yes" || status === "maybe")
|
||
dayLessons.push(lessonOut(entry.lesson, group, entry, date, status, calendar, week));
|
||
}
|
||
// Session events printed with their own date need no week table.
|
||
for (const entry of extra)
|
||
if (entry.lesson.date === date || entry.lesson.dates?.includes(date))
|
||
dayLessons.push(lessonOut(entry.lesson, group, entry, date, "yes", calendar));
|
||
dayLessons.sort((a, b) => a.start.localeCompare(b.start));
|
||
// A day without lessons (a holiday, weeks of practice) is in no town.
|
||
const places = dayLessons.length ? placesOn(schedule.places, group, labels, weekday(date), date) : [];
|
||
out.push({
|
||
date,
|
||
weekday: weekday(date),
|
||
week: week ? { n: week.n, color: week.color } : null,
|
||
lessons: dayLessons,
|
||
...(places.length && { places }),
|
||
});
|
||
}
|
||
const weekly = [1, 2, 3, 4, 5, 6, 7]
|
||
.map((day) => ({
|
||
day,
|
||
lessons: entries
|
||
.filter((e) => e.lesson.day === day && !e.lesson.date)
|
||
.map((e) => lessonOut(e.lesson, group, e, null, "yes", calendar))
|
||
.sort((a, b) => a.start.localeCompare(b.start)),
|
||
}))
|
||
.filter((d) => d.lessons.length)
|
||
.map((d) => {
|
||
const places = placesOn(schedule.places, group, labels, d.day);
|
||
return places.length ? { ...d, places } : d;
|
||
});
|
||
return {
|
||
group,
|
||
document: {
|
||
id: file.id,
|
||
name: file.name,
|
||
faculty: file.faculty || schedule.source.faculty || "",
|
||
course: file.course || schedule.source.course || "",
|
||
modified: file.modified || schedule.source.modified || null,
|
||
},
|
||
period: schedule.period,
|
||
semester: view?.semester || session,
|
||
note: view || session
|
||
? null
|
||
: "Семестр этого расписания не найден в таблице недель: даты не рассчитаны, смотрите строки Word (weekly).",
|
||
subgroups: [
|
||
...new Set([
|
||
...subgroupsOf(schedule, group),
|
||
...extra.map((e) => e.lesson.subgroups?.find((s) => s.group === group)?.label).filter(Boolean),
|
||
]),
|
||
].sort((a, b) => a.localeCompare(b, "ru", { numeric: true })),
|
||
// What parts of the group choose between: [["Немецкий язык", "Французский язык"]].
|
||
choices: (schedule.choices || []).filter((c) => c.groups.includes(group)).map((c) => c.options),
|
||
// Some lessons are for foreign students only, or for the others: worth asking which one you are.
|
||
audience: lessons.some((l) => l.audience),
|
||
bells: calendar?.bells || [],
|
||
from,
|
||
days: out,
|
||
weekly,
|
||
notes: schedule.notes,
|
||
};
|
||
}
|