refac: rewrite a lil in rust

This commit is contained in:
2026-05-04 17:38:49 +09:30
parent 608fc96c4e
commit aa1e1a925e
42 changed files with 1367 additions and 103 deletions
@@ -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>`;
}
+6 -6
View File
@@ -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>`;
}
+3 -2
View File
@@ -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%;
`;
+31
View File
@@ -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}`;
+14 -1
View File
@@ -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;
+20 -1
View File
@@ -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);
})();
};
+21 -1
View File
@@ -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);
}
}
+18
View File
@@ -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}`;
+38
View File
@@ -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;
}
+20 -6
View File
@@ -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);
}
+10
View File
@@ -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");
}
+49 -17
View File
@@ -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");
}