import { ref, watch, computed } from "vue"; import { getNextAndPrevDay, setUTCMidnight } from "@/util"; import i18n from "@/i18n"; /* Router */ export const shouldLogin = ref(false); export const loading = ref(false); export const loadingProgress = ref(0); export const loadingFailed = ref(false); /* Preferences */ export const profiles = ref( JSON.parse(localStorage.getItem("profiles")) || [ { id: 0, name: "Default Profile", classFilter: "none", timetableId: "none", timetableGroups: [], }, ], ); export const activeProfile = computed(() => { return ( profiles.value.find((e) => e.id == activeProfileId.value) || profiles.value[0] ); }); export const activeProfileId = ref( localStorage.getItem("activeProfile") || profiles.value[0].id, ); watch( () => activeProfile.value.classFilter, () => { fetchData(getNextAndPrevDay(selectedDate.value), false); }, ); export const localTimetables = ref( JSON.parse(localStorage.getItem("timetables")) || [], ); export const theme = ref(localStorage.getItem("theme") || "auto"); watch( profiles, (newValue) => { localStorage.setItem("profiles", JSON.stringify(newValue)); }, { deep: true }, ); watch(activeProfileId, (newValue) => { localStorage.setItem("activeProfile", newValue); }); watch( localTimetables, (newValue) => { localStorage.setItem("timetables", JSON.stringify(newValue)); }, { deep: true }, ); watch(theme, (newValue) => { localStorage.setItem("theme", newValue); }); /* Date selector */ export const selectedDate = ref(setUTCMidnight(new Date())); export const selectedDay = computed(() => selectedDate.value.getDay() - 1); // Jump to next Monday if it is weekend if (selectedDay.value == 5) selectedDate.value = new Date(selectedDate.value.getTime() + 86400000 * 2); if (selectedDay.value == -1) selectedDate.value = new Date(selectedDate.value.getTime() + 86400000); // Set this to a positive or negative integer // to change selectedDate by this number export const changeDay = ref(0); // Set this to jump to a specific date export const changeDate = ref(new Date()); /* Data store */ export const timetable = computed(() => { const localTimetable = localTimetables.value.find( (e) => e.id == activeProfile.value.timetableId, ); const remoteTimetable = timetables.value.find( (e) => e.id == activeProfile.value.timetableId, ); return ( localTimetable || remoteTimetable || { trusted: true, source: "", data: [] } ); }); export const sessionInfo = ref({}); export const timetables = ref([]); export const times = ref([]); export const substitutions = ref({}); export const history = ref({}); export const classList = ref([]); /* API functions */ // Set the `VITE_API_ENDPOINT` env variable when // building the frontend to use an external api server export const baseUrl = import.meta.env.VITE_API_ENDPOINT || "/api"; export async function fetchData(days, partial) { if (!timetable.value.data || Object.keys(classList.value).length == 0) { partial = false; } const steps = 2 * days.length + (partial ? 0 : 2); let step = 1; loadingFailed.value = false; loadingProgress.value = 0.1; loading.value = true; // Check if the API server is reachable // and the user is authenticated if (!(await fetchSessionInfo())) { loadingFailed.value = true; loadingProgress.value = 1; return; } loadingProgress.value = step++ / steps; if (!partial) { await fetchClassList(); loadingProgress.value = step++ / steps; await fetchTimetables(); loadingProgress.value = step++ / steps; } for (const day of days) { await fetchSubstitutions(day); loadingProgress.value = step++ / steps; await fetchHistory(day); loadingProgress.value = step++ / steps; } loadingProgress.value = 1; loading.value = false; } // Load new data if date changes watch(selectedDate, () => fetchData(getNextAndPrevDay(selectedDate.value), true), ); export async function fetchSessionInfo() { try { const checkResponse = await fetch(`${baseUrl}/info`); if (checkResponse.status == 401) { shouldLogin.value = true; return false; } else if (checkResponse.status != 200) { console.log("Other error while fetching data: " + checkResponse.status); return false; } else { sessionInfo.value = await checkResponse.json(); } } catch { console.log("Error while fetching data: No internet connection!"); return false; } return true; } export async function fetchClassList() { const classListResponse = await fetch(`${baseUrl}/classes`); const classListData = await classListResponse.json(); classList.value = classListData; } export async function fetchTimetables() { const timetableResponse = await fetch( `${baseUrl}/timetable?class=${activeProfile.value.classFilter}`, ); const timetableData = await timetableResponse.json(); if (timetableData.error) { console.warn("API Error: " + timetableData.error); timetables.value = []; } else { timetables.value = timetableData.timetables; times.value = timetableData.times; } } export async function fetchSubstitutions(day) { const requestDate = `?date=${day}`; const substitutionResponse = await fetch( activeProfile.value.classFilter == "none" ? `${baseUrl}/substitutions${requestDate}` : `${baseUrl}/substitutions${requestDate}&class=${activeProfile.value.classFilter}`, ); const substitutionData = await substitutionResponse.json(); substitutions.value[day] = substitutionData; } export async function fetchHistory(day) { const requestDate = `?date=${day}`; const historyResponse = await fetch( activeProfile.value.classFilter == "none" ? `${baseUrl}/history${requestDate}` : `${baseUrl}/history${requestDate}&class=${activeProfile.value.classFilter}`, ); const historyData = await historyResponse.json(); if (historyData.error) console.warn("API Error: " + historyData.error); else history.value[day] = historyData; } /* Preprocess the timetable data */ export const parsedTimetable = computed(() => { // Check if timetable data exists if (!timetable.value.data) return []; return timetable.value.data.map((day) => { const parsedDay = []; for (const lesson of day) { let usedLesson = { ...lesson }; // Check if lesson has multiple options // (timetable groups) if (Array.isArray(lesson) && lesson.length > 1) { let matchingLesson = lesson.find((e) => activeProfile.value.timetableGroups.includes(e.group), ); // If no valid timetable group is configured // add a dummy lesson showing a notice if (!matchingLesson) { matchingLesson = { subject: lesson.map((e) => e.subject).join(" / "), teacher: i18n.global.t("timetable.configureTimetableGroup"), length: lesson[0].length || 1, }; } usedLesson = { ...matchingLesson }; } else if (Array.isArray(lesson)) { usedLesson = { ...lesson[0] }; } // Duplicate the lesson if its length is > 1 for it // to show up multiple times in the timetable view const lessonLength = usedLesson.length || 1; delete usedLesson.length; for (var i = 0; i < lessonLength; i++) parsedDay.push(usedLesson); } return parsedDay; }); }); export const possibleTimetableGroups = computed(() => { const foundTimetableGroups = []; if (!timetable.value.data) return []; // Make a list of all possible timetable groups // found in the current timetable for (const day of timetable.value.data) { for (const lesson of day) { if (Array.isArray(lesson) && lesson.length > 1) { for (const group of lesson) { if (group.group && !foundTimetableGroups.includes(group.group)) { foundTimetableGroups.push(group.group); } } } } } return foundTimetableGroups; }); // Initially fetch data for the // current, next and previous day fetchData(getNextAndPrevDay(selectedDate.value), false);