mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
refac: rewrite a lil in rust
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { animate } from "motion";
|
||||
import browser from "webextension-polyfill";
|
||||
import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
|
||||
import { GetThresholdOfColor } from "@/seqta/ui/colors/getThresholdColour";
|
||||
import { convertTo12HourFormat } from "@/seqta/utils/convertTo12HourFormat";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
toISODate,
|
||||
weekRangeContaining,
|
||||
} from "@/seqta/utils/Loaders/engageParentTimetable";
|
||||
import { extensionAssetUrl } from "@/seqta/utils/extensionAsset";
|
||||
|
||||
export function updateEngageHomeMenuActive(isHome: boolean): void {
|
||||
const home = document.getElementById("homebutton");
|
||||
@@ -128,7 +128,7 @@ function renderEngageDayLessons(): void {
|
||||
if (lessons.length === 0) {
|
||||
dayContainer.innerHTML = `
|
||||
<div class="day-empty">
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" alt="" />
|
||||
<img src="${extensionAssetUrl(LogoLight)}" alt="" />
|
||||
<p>No lessons for this day.</p>
|
||||
</div>`;
|
||||
} else {
|
||||
@@ -273,7 +273,7 @@ function processEngageNotices(response: any, labelArray: string[]): void {
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
img.src = extensionAssetUrl(LogoLight);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No notices for today.";
|
||||
emptyState.append(img, text);
|
||||
@@ -285,7 +285,7 @@ function processEngageNotices(response: any, labelArray: string[]): void {
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
img.src = extensionAssetUrl(LogoLight);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No notices for today.";
|
||||
emptyState.append(img, text);
|
||||
@@ -713,7 +713,7 @@ function showEngageTimetableError(message: string): void {
|
||||
dayContainer.classList.remove("loading");
|
||||
dayContainer.innerHTML = `
|
||||
<div class="day-empty">
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" alt="" />
|
||||
<img src="${extensionAssetUrl(LogoLight)}" alt="" />
|
||||
<p>${message}</p>
|
||||
</div>`;
|
||||
}
|
||||
@@ -724,7 +724,7 @@ function showEngageNoticesSectionError(message: string): void {
|
||||
noticeContainer.classList.remove("loading");
|
||||
noticeContainer.innerHTML = `
|
||||
<div class="day-empty">
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" alt="" />
|
||||
<img src="${extensionAssetUrl(LogoLight)}" alt="" />
|
||||
<p>${message}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { animate, stagger } from "motion";
|
||||
import browser from "webextension-polyfill";
|
||||
import LogoLight from "@/resources/icons/betterseqta-light-icon.png";
|
||||
import assessmentsicon from "@/seqta/icons/assessmentsIcon";
|
||||
import coursesicon from "@/seqta/icons/coursesIcon";
|
||||
@@ -13,6 +12,7 @@ import { CreateElement } from "@/seqta/utils/CreateEnable/CreateElement";
|
||||
import { FilterUpcomingAssessments } from "@/seqta/utils/FilterUpcomingAssessments";
|
||||
import { getMockNotices } from "@/seqta/ui/dev/hideSensitiveContent";
|
||||
import { setupFixedTooltips } from "@/seqta/utils/fixedTooltip";
|
||||
import { extensionAssetUrl } from "@/seqta/utils/extensionAsset";
|
||||
|
||||
let LessonInterval: any;
|
||||
let currentSelectedDate = new Date();
|
||||
@@ -155,7 +155,7 @@ export async function loadHomePage() {
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
img.src = extensionAssetUrl(LogoLight);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No notices available.";
|
||||
emptyState.append(img, text);
|
||||
@@ -304,7 +304,7 @@ function processNotices(response: any, labelArray: string[]) {
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
img.src = extensionAssetUrl(LogoLight);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No notices for today.";
|
||||
emptyState.append(img, text);
|
||||
@@ -316,7 +316,7 @@ function processNotices(response: any, labelArray: string[]) {
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
img.src = extensionAssetUrl(LogoLight);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No notices for today.";
|
||||
emptyState.append(img, text);
|
||||
@@ -735,7 +735,7 @@ function callHomeTimetable(date: string, change?: any) {
|
||||
const dummyDay = document.createElement("div");
|
||||
dummyDay.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLight);
|
||||
img.src = extensionAssetUrl(LogoLight);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No lessons available.";
|
||||
dummyDay.append(img, text);
|
||||
@@ -987,7 +987,7 @@ async function CreateUpcomingSection(assessments: any, activeSubjects: any) {
|
||||
if (assessments.length === 0) {
|
||||
upcomingitemcontainer!.innerHTML = `
|
||||
<div class="day-empty">
|
||||
<img src="${browser.runtime.getURL(LogoLight)}" />
|
||||
<img src="${extensionAssetUrl(LogoLight)}" />
|
||||
<p>No assessments available.</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { settingsState } from "./listeners/SettingsState";
|
||||
import browser from "webextension-polyfill";
|
||||
import LogoLightOutline from "@/resources/icons/betterseqta-light-outline.png";
|
||||
import { animate, stagger } from "motion";
|
||||
import { extensionAssetUrl } from "@/seqta/utils/extensionAsset";
|
||||
|
||||
export async function SendNewsPage() {
|
||||
console.info("[BetterSEQTA+] Started Loading News Page");
|
||||
@@ -58,7 +59,7 @@ export async function SendNewsPage() {
|
||||
const emptyState = document.createElement("div");
|
||||
emptyState.classList.add("day-empty");
|
||||
const img = document.createElement("img");
|
||||
img.src = browser.runtime.getURL(LogoLightOutline);
|
||||
img.src = extensionAssetUrl(LogoLightOutline);
|
||||
const text = document.createElement("p");
|
||||
text.innerText = "No news articles available right now.";
|
||||
emptyState.append(img, text);
|
||||
@@ -79,7 +80,7 @@ export async function SendNewsPage() {
|
||||
|
||||
if (article.urlToImage == "null" || article.urlToImage == null) {
|
||||
articleimage.style.cssText = `
|
||||
background-image: url(${browser.runtime.getURL(LogoLightOutline)});
|
||||
background-image: url(${extensionAssetUrl(LogoLightOutline)});
|
||||
width: 20%;
|
||||
margin: 0 7.5%;
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
||||
import {
|
||||
isBetterseqtaWasmReady,
|
||||
normalizeSeqtaSubjectHexColour,
|
||||
parseSeqtaCoursesAssessmentsPageJson,
|
||||
} from "@/wasm/init";
|
||||
|
||||
/**
|
||||
* Parses the current page from window.location.hash.
|
||||
@@ -12,6 +17,24 @@ import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
|
||||
*/
|
||||
function parsePageContext(): { programme: number; metaclass: number } | null {
|
||||
const hash = window.location.hash || "";
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
const json = parseSeqtaCoursesAssessmentsPageJson(hash);
|
||||
if (json) {
|
||||
const o = JSON.parse(json) as { programme: number; metaclass: number };
|
||||
if (
|
||||
typeof o.programme === "number" &&
|
||||
typeof o.metaclass === "number" &&
|
||||
!Number.isNaN(o.programme) &&
|
||||
!Number.isNaN(o.metaclass)
|
||||
) {
|
||||
return { programme: o.programme, metaclass: o.metaclass };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
const match = hash.match(/[?&]page=\/(courses|assessments)\/(?:[^/]+\/)?(\d+):(\d+)/);
|
||||
if (!match) return null;
|
||||
const programme = parseInt(match[2], 10);
|
||||
@@ -112,6 +135,14 @@ export async function getAdaptiveColour(): Promise<string | null> {
|
||||
const colour = await getSubjectColour(subjectCode, userId);
|
||||
if (!colour || typeof colour !== "string") return null;
|
||||
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
const normalized = normalizeSeqtaSubjectHexColour(colour);
|
||||
if (normalized) return normalized;
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
// Basic hex validation
|
||||
if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(colour)) return colour;
|
||||
if (/^[0-9A-Fa-f]{6}$/.test(colour)) return `#${colour}`;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const base64ToBlob = (base64: string, contentType: string = ""): Blob => {
|
||||
import { decodeBase64, isBetterseqtaWasmReady } from "@/wasm/init";
|
||||
|
||||
const base64ToBlobTs = (base64: string, contentType: string = ""): Blob => {
|
||||
const byteCharacters = atob(base64);
|
||||
const byteArrays: Uint8Array[] = [];
|
||||
|
||||
@@ -14,4 +16,15 @@ const base64ToBlob = (base64: string, contentType: string = ""): Blob => {
|
||||
return new Blob(byteArrays, { type: contentType });
|
||||
};
|
||||
|
||||
const base64ToBlob = (base64: string, contentType: string = ""): Blob => {
|
||||
const trimmed = base64.trim();
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
const bytes = decodeBase64(trimmed);
|
||||
if (bytes.byteLength > 0 || trimmed.length === 0) {
|
||||
return new Blob([bytes], { type: contentType });
|
||||
}
|
||||
}
|
||||
return base64ToBlobTs(trimmed, contentType);
|
||||
};
|
||||
|
||||
export default base64ToBlob;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export const blobToBase64 = (blob: Blob) => {
|
||||
import { encodeDataUrl, isBetterseqtaWasmReady } from "@/wasm/init";
|
||||
|
||||
function readAsDataUrl(blob: Blob): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
@@ -8,4 +10,21 @@ export const blobToBase64 = (blob: Blob) => {
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
export const blobToBase64 = (blob: Blob) => {
|
||||
return (async () => {
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
const buf = await blob.arrayBuffer();
|
||||
return encodeDataUrl(
|
||||
blob.type || "application/octet-stream",
|
||||
new Uint8Array(buf),
|
||||
);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
return readAsDataUrl(blob);
|
||||
})();
|
||||
};
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
export function convertTo12HourFormat(
|
||||
import {
|
||||
convertTo12HourFormatWasm,
|
||||
isBetterseqtaWasmReady,
|
||||
} from "@/wasm/init";
|
||||
|
||||
function convertTo12HourFormatTs(
|
||||
time: string,
|
||||
noMinutes: boolean = false,
|
||||
): string {
|
||||
@@ -19,3 +24,18 @@ export function convertTo12HourFormat(
|
||||
|
||||
return `${hoursStr}${noMinutes ? "" : `:${minutes.toString().padStart(2, "0")}`}${period}`;
|
||||
}
|
||||
|
||||
/** 12-hour time label; Rust/WASM when initialized, else TypeScript. */
|
||||
export function convertTo12HourFormat(
|
||||
time: string,
|
||||
noMinutes: boolean = false,
|
||||
): string {
|
||||
if (!isBetterseqtaWasmReady()) {
|
||||
return convertTo12HourFormatTs(time, noMinutes);
|
||||
}
|
||||
try {
|
||||
return convertTo12HourFormatWasm(time, noMinutes);
|
||||
} catch {
|
||||
return convertTo12HourFormatTs(time, noMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import {
|
||||
isBetterseqtaWasmReady,
|
||||
parseEngageRoutePage,
|
||||
} from "@/wasm/init";
|
||||
|
||||
/**
|
||||
* Learn-style hash routes on Engage: `#?page=/home` → `"home"`.
|
||||
* Falls back to the legacy path segment used by classic Learn routing.
|
||||
*/
|
||||
export function getEngageRoutePage(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
return parseEngageRoutePage(
|
||||
window.location.hash,
|
||||
window.location.href,
|
||||
);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
|
||||
const hash = window.location.hash.replace(/^#/, "");
|
||||
if (hash) {
|
||||
const qs = hash.startsWith("?") ? hash : `?${hash}`;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
function hasExtensionRuntimeGetUrl(): boolean {
|
||||
const runtime = (globalThis as any)?.chrome?.runtime;
|
||||
return (
|
||||
!!runtime &&
|
||||
typeof runtime.getURL === "function" &&
|
||||
typeof runtime.id === "string" &&
|
||||
runtime.id.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an extension asset URL safely across content-script and page contexts.
|
||||
* Falls back to the provided path when extension runtime APIs are unavailable.
|
||||
*/
|
||||
export function extensionAssetUrl(path: string): string {
|
||||
if (!path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Already fully-resolved URL.
|
||||
if (
|
||||
path.startsWith("chrome-extension://") ||
|
||||
path.startsWith("moz-extension://") ||
|
||||
path.startsWith("http://") ||
|
||||
path.startsWith("https://") ||
|
||||
path.startsWith("data:") ||
|
||||
path.startsWith("blob:")
|
||||
) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (hasExtensionRuntimeGetUrl()) {
|
||||
return (globalThis as any).chrome.runtime.getURL(path.replace(/^\/+/, ""));
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
import {
|
||||
decodeBase64,
|
||||
isBetterseqtaWasmReady,
|
||||
stripDataUrlBase64Payload,
|
||||
} from "@/wasm/init";
|
||||
|
||||
export function base64toblobURL(base64: string) {
|
||||
// Extract base64 data from the data URI
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
const payload = stripDataUrlBase64Payload(base64);
|
||||
const bytes = decodeBase64(payload.trim());
|
||||
if (bytes.byteLength > 0) {
|
||||
const blob = new Blob([bytes], { type: "image/png" });
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
|
||||
const base64Index = base64.indexOf(",") + 1;
|
||||
const imageBase64 = base64.substring(base64Index);
|
||||
|
||||
// Convert base64 to blob
|
||||
const byteCharacters = atob(imageBase64);
|
||||
const byteNumbers = new Uint8Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
@@ -12,8 +29,5 @@ export function base64toblobURL(base64: string) {
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: "image/png" });
|
||||
|
||||
// Convert blob to blob URL
|
||||
const imageUrl = URL.createObjectURL(blob);
|
||||
|
||||
return imageUrl;
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { isBetterseqtaWasmReady, titleIsSeqtaEngage } from "@/wasm/init";
|
||||
|
||||
/** SEQTA Engage (React) uses a different shell from classic SEQTA Learn. */
|
||||
export function isSeqtaEngageExperience(): boolean {
|
||||
if (typeof document === "undefined") return false;
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
return titleIsSeqtaEngage(document.title);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
return document.title.includes("SEQTA Engage");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
import {
|
||||
formatTimetableTimeLabel,
|
||||
formatTimetableTimeRange,
|
||||
isBetterseqtaWasmReady,
|
||||
locationHashIncludesTimetablePage,
|
||||
} from "@/wasm/init";
|
||||
import { convertTo12HourFormat } from "./convertTo12HourFormat";
|
||||
import { waitForElm } from "./waitForElm";
|
||||
|
||||
function timetableLabel12(original: string, noMinutes: boolean): string {
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
return formatTimetableTimeLabel(original, noMinutes);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
return convertTo12HourFormat(original, noMinutes)
|
||||
.toLowerCase()
|
||||
.replace(" ", "");
|
||||
}
|
||||
|
||||
function timetableRange12(original: string): string | undefined {
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
const r = formatTimetableTimeRange(original);
|
||||
if (r) return r;
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
if (!original.includes("–") && !original.includes("-")) return undefined;
|
||||
const [start, end] = original.split(/[-–]/).map((p) => p.trim());
|
||||
if (!start || !end) return undefined;
|
||||
return `${timetableLabel12(start, false)}–${timetableLabel12(end, false)}`;
|
||||
}
|
||||
|
||||
let timetableObserver: MutationObserver | null = null;
|
||||
let isOnTimetablePage = false;
|
||||
let isInitialized = false;
|
||||
@@ -19,9 +53,7 @@ function updateTimeElements(): void {
|
||||
const original = el.dataset.original;
|
||||
if (!original) return;
|
||||
|
||||
el.textContent = convertTo12HourFormat(original, true)
|
||||
.toLowerCase()
|
||||
.replace(" ", "");
|
||||
el.textContent = timetableLabel12(original, true);
|
||||
});
|
||||
|
||||
const entryTimes = timetablePage.querySelectorAll<HTMLElement>(".entry .times");
|
||||
@@ -30,30 +62,30 @@ function updateTimeElements(): void {
|
||||
const original = el.dataset.original || "";
|
||||
if (!original.includes("–") && !original.includes("-")) return;
|
||||
|
||||
const [start, end] = original.split(/[-–]/).map((p) => p.trim());
|
||||
if (!start || !end) return;
|
||||
|
||||
const start12 = convertTo12HourFormat(start).toLowerCase().replace(" ", "");
|
||||
const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", "");
|
||||
el.textContent = `${start12}–${end12}`;
|
||||
const ranged = timetableRange12(original);
|
||||
if (ranged) el.textContent = ranged;
|
||||
});
|
||||
|
||||
const quickbarTimes = document.querySelectorAll<HTMLElement>(".quickbar .meta .times");
|
||||
const quickbarTimes = document.querySelectorAll<HTMLElement>(
|
||||
".quickbar .meta .times",
|
||||
);
|
||||
quickbarTimes.forEach((el) => {
|
||||
if (!el.dataset.original) el.dataset.original = el.textContent || "";
|
||||
const original = el.dataset.original || "";
|
||||
|
||||
if (!original.includes("–") && !original.includes("-")) return;
|
||||
const [start, end] = original.split(/[-–]/).map((p) => p.trim());
|
||||
|
||||
if (!start || !end) return;
|
||||
const start12 = convertTo12HourFormat(start).toLowerCase().replace(" ", "");
|
||||
const end12 = convertTo12HourFormat(end).toLowerCase().replace(" ", "");
|
||||
el.textContent = `${start12}–${end12}`;
|
||||
const ranged = timetableRange12(original);
|
||||
if (ranged) el.textContent = ranged;
|
||||
});
|
||||
}
|
||||
|
||||
function checkIfOnTimetablePage(): boolean {
|
||||
if (isBetterseqtaWasmReady()) {
|
||||
try {
|
||||
return locationHashIncludesTimetablePage(window.location.hash);
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
}
|
||||
return window.location.hash.includes("page=/timetable");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user