feat: add Next Lesson widget and enhance existing widgets

- 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.
This commit is contained in:
2026-10-18 00:39:32 +03:00
parent 29df0aa61f
commit d900e038b6
52 changed files with 278058 additions and 183859 deletions

View File

@@ -29,8 +29,8 @@ app.use("/api", (_req, res, next) => {
next();
});
// Raised whenever an endpoint is added or changed, so a deployment is easy to verify:
// 2 — /api/timetable for the Android client.
const API_VERSION = 2;
// 2 — /api/timetable for the Android client; 3 — its range=semester.
const API_VERSION = 3;
// Liveness plus what the server currently reads from the university, without network calls.
app.get("/api/health", async (_req, res) => {
@@ -110,6 +110,8 @@ app.get("/api/timetable", async (req, res, next) => {
group,
from,
days: req.query.days,
range: req.query.range === "semester" ? "semester" : undefined,
today: todayIso(),
}),
);
} catch (e) {

View File

@@ -5,6 +5,9 @@ import { lessonKey, subgroupsOf } from "../shared/lessons.js";
import { occurrence, peopleAndRooms, roomsOn } from "./directory.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;
function lessonOut(lesson, group, people, date, status, calendar) {
const bell = calendar?.bells.find((b) => b.start === lesson.start);
@@ -33,13 +36,23 @@ function lessonOut(lesson, group, people, date, status, calendar) {
* 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.
*/
export function timetable({ file, schedule, calendar, group, from, days }) {
export function timetable({ file, schedule, calendar, group, from, days, range, today }) {
if (!schedule.groups.includes(group)) {
const error = new Error(`В документе нет группы ${group}.`);
error.status = 404;
throw error;
}
const count = Math.min(Math.max(Number(days) || 7, 1), MAX_DAYS);
let count = Math.min(Math.max(Number(days) || 7, 1), MAX_DAYS);
// range=semester: every day of the semester `today` falls in, so a client can keep
// the whole term offline. Without a known semester it keeps the plain from/days window.
if (range === "semester") {
const current = semesterView(calendar, schedule, today || from);
if (current) {
from = current.semester.start;
const span = Math.round((Date.parse(current.semester.end) - Date.parse(from)) / DAY_MS) + 1;
count = Math.min(Math.max(span, 1), MAX_SEMESTER_DAYS);
}
}
const lessons = schedule.lessons.filter((l) => l.groups.includes(group));
const entries = lessons.map((lesson) => ({
doc: { id: file.id, schedule },