Compare commits

..

5 Commits

Author SHA1 Message Date
AdenMGB 3c613f4938 chore: bump ver 2026-04-03 10:55:41 +10:30
AdenMGB 04843a90fe fix: fix adaptive themeing to support current year subjects 2026-04-03 10:47:56 +10:30
AdenMGB 834d585ac7 chore: release ntoe 2026-03-29 20:26:19 +10:30
AdenMGB 343fa7ca9f feat: migrate pdfjs to local & bump ver 2026-03-29 20:25:06 +10:30
AdenMGB e049f34a5e feat: WIP Engage progress 2026-03-28 09:06:54 +10:30
14 changed files with 222 additions and 76 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "betterseqtaplus", "name": "betterseqtaplus",
"version": "3.5.1", "version": "3.5.3",
"type": "module", "type": "module",
"description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!", "description": "Enhance SEQTA Learn's usability and aesthetics! A fork of BetterSEQTA to continue development add add heaps more features!",
"browserslist": "> 0.5%, last 2 versions, not dead", "browserslist": "> 0.5%, last 2 versions, not dead",
+6 -1
View File
@@ -50,7 +50,12 @@ if (document.childNodes[1]) {
} }
async function init() { async function init() {
if (hasSEQTAText && document.title.includes("SEQTA Learn") && !IsSEQTAPage) { if (
hasSEQTAText &&
(document.title.includes("SEQTA Learn") ||
document.title.includes("SEQTA Engage")) &&
!IsSEQTAPage
) {
IsSEQTAPage = true; IsSEQTAPage = true;
console.info("[BetterSEQTA+] Verified SEQTA Page"); console.info("[BetterSEQTA+] Verified SEQTA Page");
+4 -1
View File
@@ -6,7 +6,10 @@ function reloadSeqtaPages() {
const result = browser.tabs.query({}); const result = browser.tabs.query({});
function open(tabs: any) { function open(tabs: any) {
for (let tab of tabs) { for (let tab of tabs) {
if (tab.title.includes("SEQTA Learn")) { if (
tab.title?.includes("SEQTA Learn") ||
tab.title?.includes("SEQTA Engage")
) {
browser.tabs.reload(tab.id); browser.tabs.reload(tab.id);
} }
} }
+26
View File
@@ -0,0 +1,26 @@
import * as pdfjs from "pdfjs-dist";
import browser from "webextension-polyfill";
import pdfWorkerHref from "pdfjs-dist/build/pdf.worker.min.mjs?url";
import pdfLegacyHref from "pdfjs-dist/legacy/build/pdf.min.mjs?url";
function extensionAssetUrl(viteAssetHref: string): string {
const path = viteAssetHref.replace(/^\/+/, "");
return browser.runtime.getURL(path);
}
let workerConfigured = false;
/** Required before pdfjs spawns a worker (content-script / extension isolate). */
export function ensurePdfjsWorker(): void {
if (workerConfigured) return;
pdfjs.GlobalWorkerOptions.workerSrc = extensionAssetUrl(pdfWorkerHref);
workerConfigured = true;
}
/** Page-context script on Firefox must load these chrome-extension:// URLs (see web_accessible_resources). */
export function getPdfjsPageContextUrls(): { lib: string; worker: string } {
return {
lib: extensionAssetUrl(pdfLegacyHref),
worker: extensionAssetUrl(pdfWorkerHref),
};
}
+6 -1
View File
@@ -32,7 +32,12 @@
], ],
"web_accessible_resources": [ "web_accessible_resources": [
{ {
"resources": ["resources/icons/*", "resources/update-image.webp"], "resources": [
"resources/icons/*",
"resources/update-image.webp",
"resources/pdfjs/pdf.worker.min.mjs",
"resources/pdfjs/pdf.legacy.min.mjs"
],
"matches": ["*://*/*"] "matches": ["*://*/*"]
} }
] ]
@@ -1,8 +1,12 @@
import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts"; import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements.ts";
import ReactFiber from "@/seqta/utils/ReactFiber.ts"; import ReactFiber from "@/seqta/utils/ReactFiber.ts";
import {
ensurePdfjsWorker,
getPdfjsPageContextUrls,
} from "@/lib/pdfjsExtension.ts";
import * as pdfjs from "pdfjs-dist"; import * as pdfjs from "pdfjs-dist";
pdfjs.GlobalWorkerOptions.workerSrc =
`https://cdn.jsdelivr.net/npm/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`; ensurePdfjsWorker();
export async function initStorage(api: any) { export async function initStorage(api: any) {
await api.storage.loaded; await api.storage.loaded;
@@ -219,6 +223,12 @@ async function fetchPDFAsArrayBuffer(url: string): Promise<ArrayBuffer> {
export async function extractPDFText(url: string): Promise<string> { export async function extractPDFText(url: string): Promise<string> {
try { try {
if (isFirefox) { if (isFirefox) {
const { lib: pdfLibUrl, worker: pdfWorkerUrl } = getPdfjsPageContextUrls();
const escJsSingleQuoted = (s: string) =>
s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
const pdfLibInj = escJsSingleQuoted(pdfLibUrl);
const pdfWorkerInj = escJsSingleQuoted(pdfWorkerUrl);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
const requestId = `pdf-extract-${Date.now()}-${Math.random()}`; const requestId = `pdf-extract-${Date.now()}-${Math.random()}`;
@@ -232,13 +242,15 @@ export async function extractPDFText(url: string): Promise<string> {
(function() { (function() {
const requestId = '${requestId}'; const requestId = '${requestId}';
const url = '${escapedUrl}'; const url = '${escapedUrl}';
const pdfLibSrc = '${pdfLibInj}';
const pdfWorkerSrc = '${pdfWorkerInj}';
if (window.pdfjsLib) { if (window.pdfjsLib) {
extractPDF(); extractPDF();
} else { } else {
const pdfjsScript = document.createElement('script'); const pdfjsScript = document.createElement('script');
pdfjsScript.src = 'https://cdn.jsdelivr.net/npm/pdfjs-dist/build/pdf.min.js'; pdfjsScript.src = pdfLibSrc;
pdfjsScript.type = 'text/javascript'; pdfjsScript.type = 'module';
pdfjsScript.onload = function() { pdfjsScript.onload = function() {
extractPDF(); extractPDF();
@@ -256,7 +268,7 @@ export async function extractPDFText(url: string): Promise<string> {
function extractPDF() { function extractPDF() {
try { try {
window.pdfjsLib.GlobalWorkerOptions.workerSrc = ''; window.pdfjsLib.GlobalWorkerOptions.workerSrc = pdfWorkerSrc;
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', url, true); xhr.open('GET', url, true);
+66 -16
View File
@@ -17,6 +17,7 @@ import { StorageChangeHandler } from "@/seqta/utils/listeners/StorageChanges";
import { eventManager } from "@/seqta/utils/listeners/EventManager"; import { eventManager } from "@/seqta/utils/listeners/EventManager";
// UI and theme management // UI and theme management
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners"; import RegisterClickListeners from "@/seqta/utils/listeners/ClickListeners";
import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements"; import { AddBetterSEQTAElements } from "@/seqta/ui/AddBetterSEQTAElements";
import { updateAllColors } from "@/seqta/ui/colors/Manager"; import { updateAllColors } from "@/seqta/ui/colors/Manager";
@@ -82,7 +83,12 @@ export function hideSideBar() {
} }
} }
let betterSeqtaFinishLoadDone = false;
export async function finishLoad() { export async function finishLoad() {
if (betterSeqtaFinishLoadDone) return;
betterSeqtaFinishLoadDone = true;
try { try {
document.querySelector(".legacy-root")?.classList.remove("hidden"); document.querySelector(".legacy-root")?.classList.remove("hidden");
@@ -115,19 +121,19 @@ export function GetCSSElement(file: string) {
} }
function removeThemeTagsFromNotices() { function removeThemeTagsFromNotices() {
// Grabs an array of the notice iFrames
const userHTMLArray = document.getElementsByClassName("userHTML"); const userHTMLArray = document.getElementsByClassName("userHTML");
// Iterates through the array, applying the iFrame css
for (const item of userHTMLArray) { for (const item of userHTMLArray) {
// Grabs the HTML of the body tag const iframe = item as HTMLIFrameElement;
const item1 = item as HTMLIFrameElement; try {
const body = item1.contentWindow!.document.querySelectorAll("body")[0]; const doc = iframe.contentDocument;
if (body) { if (!doc?.body) continue;
// Replaces the theme tag with nothing const body = doc.body;
const bodyText = body.innerHTML; const bodyText = body.innerHTML;
body.innerHTML = bodyText body.innerHTML = bodyText
.replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "") .replace(/\[\[[\w]+[:][\w]+[\]\]]+/g, "")
.replace(/ +/, " "); .replace(/ +/, " ");
} catch {
// Cross-origin or otherwise inaccessible iframe (common during Engage load / filter frames)
} }
} }
} }
@@ -296,6 +302,11 @@ async function handleNotices(node: Element): Promise<void> {
} }
async function handleSublink(sublink: string | undefined): Promise<void> { async function handleSublink(sublink: string | undefined): Promise<void> {
if (isSeqtaEngageExperience()) {
finishLoad();
return;
}
switch (sublink) { switch (sublink) {
case "news": case "news":
await handleNewsPage(); await handleNewsPage();
@@ -382,8 +393,11 @@ async function handleDashboard(node: Element): Promise<void> {
document.head.append(style); document.head.append(style);
await waitForElm(".dashlet", true, 10); await waitForElm(".dashlet", true, 10);
try {
const children = document.querySelectorAll(".dashboard > *");
if (children.length) {
animate( animate(
".dashboard > *", children,
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
{ {
delay: stagger(0.1), delay: stagger(0.1),
@@ -391,6 +405,10 @@ async function handleDashboard(node: Element): Promise<void> {
ease: [0.22, 0.03, 0.26, 1], ease: [0.22, 0.03, 0.26, 1],
}, },
); );
}
} catch {
// Avoid uncaught errors if motion hits an unexpected DOM state during load.
}
document.head.querySelector("style.dashboardHider")?.remove(); document.head.querySelector("style.dashboardHider")?.remove();
} }
@@ -400,8 +418,11 @@ async function handleDocuments(node: Element): Promise<void> {
if (!settingsState.animations) return; if (!settingsState.animations) return;
await waitForElm(".document", true, 10); await waitForElm(".document", true, 10);
try {
const rows = document.querySelectorAll(".documents tbody tr.document");
if (rows.length) {
animate( animate(
".documents tbody tr.document", rows,
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
{ {
delay: stagger(0.05), delay: stagger(0.05),
@@ -410,14 +431,21 @@ async function handleDocuments(node: Element): Promise<void> {
}, },
); );
} }
} catch {
// ignore
}
}
async function handleReports(node: Element): Promise<void> { async function handleReports(node: Element): Promise<void> {
if (!(node instanceof HTMLElement)) return; if (!(node instanceof HTMLElement)) return;
if (!settingsState.animations) return; if (!settingsState.animations) return;
await waitForElm(".report", true, 10); await waitForElm(".report", true, 10);
try {
const items = document.querySelectorAll(".reports .item");
if (items.length) {
animate( animate(
".reports .item", items,
{ opacity: [0, 1], y: [10, 0] }, { opacity: [0, 1], y: [10, 0] },
{ {
delay: stagger(0.05, { startDelay: 0.2 }), delay: stagger(0.05, { startDelay: 0.2 }),
@@ -426,6 +454,10 @@ async function handleReports(node: Element): Promise<void> {
}, },
); );
} }
} catch {
// ignore
}
}
function CheckNoticeTextColour(notice: any) { function CheckNoticeTextColour(notice: any) {
eventManager.register( eventManager.register(
@@ -449,6 +481,26 @@ function CheckNoticeTextColour(notice: any) {
} }
export function tryLoad() { export function tryLoad() {
if (isSeqtaEngageExperience()) {
updateIframesWithDarkMode();
window.addEventListener(
"load",
() => removeThemeTagsFromNotices(),
{ once: true },
);
window.addEventListener(
"load",
() => void finishLoad(),
{ once: true },
);
waitForElm(".login").then(() => void finishLoad());
waitForElm(".day-container").then(() => void finishLoad());
waitForElm(".code", true, 50).then((elm: any) => {
if (!elm.innerText.includes("BetterSEQTA")) void LoadPageElements();
});
return;
}
waitForElm(".login").then(() => { waitForElm(".login").then(() => {
finishLoad(); finishLoad();
}); });
@@ -466,13 +518,10 @@ export function tryLoad() {
}); });
updateIframesWithDarkMode(); updateIframesWithDarkMode();
// Waits for page to call on load, run scripts window.addEventListener(
document.addEventListener(
"load", "load",
function () { () => removeThemeTagsFromNotices(),
removeThemeTagsFromNotices(); { once: true },
},
true,
); );
} }
@@ -489,6 +538,7 @@ function ReplaceMenuSVG(element: HTMLElement, svg: string) {
const processedSymbol = Symbol("processed"); const processedSymbol = Symbol("processed");
export async function ObserveMenuItemPosition() { export async function ObserveMenuItemPosition() {
if (isSeqtaEngageExperience()) return;
await waitForElm("#menu > ul > li"); await waitForElm("#menu > ul > li");
eventManager.register( eventManager.register(
+6
View File
@@ -1,4 +1,5 @@
import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings"; import { addExtensionSettings } from "@/seqta/utils/Adders/AddExtensionSettings";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage"; import { loadHomePage } from "@/seqta/utils/Loaders/LoadHomePage";
import { SendNewsPage } from "@/seqta/utils/SendNewsPage"; import { SendNewsPage } from "@/seqta/utils/SendNewsPage";
import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton"; import { setupSettingsButton } from "@/seqta/utils/setupSettingsButton";
@@ -42,6 +43,11 @@ export async function getUserInfo() {
} }
export async function AddBetterSEQTAElements() { export async function AddBetterSEQTAElements() {
if (isSeqtaEngageExperience()) {
addExtensionSettings();
return;
}
if (settingsState.onoff) { if (settingsState.onoff) {
if (settingsState.DarkMode) { if (settingsState.DarkMode) {
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
+20 -15
View File
@@ -9,21 +9,8 @@ import Settings from "@/interface/pages/settings.svelte";
let isSettingsRendered = false; let isSettingsRendered = false;
export function addExtensionSettings() { function extensionOutsideClickHandler(extensionPopup: HTMLElement) {
const extensionPopup = document.createElement("div"); return (event: MouseEvent) => {
extensionPopup.classList.add("outside-container", "hide");
extensionPopup.id = "ExtensionPopup";
const extensionContainer = document.querySelector(
"#container",
) as HTMLDivElement;
if (extensionContainer) extensionContainer.appendChild(extensionPopup);
const container = document.getElementById("container");
new SettingsResizer();
container!.onclick = (event) => {
if (!SettingsClicked) return; if (!SettingsClicked) return;
if (!(event.target as HTMLElement).closest("#AddedSettings")) { if (!(event.target as HTMLElement).closest("#AddedSettings")) {
@@ -33,6 +20,24 @@ export function addExtensionSettings() {
}; };
} }
export function addExtensionSettings() {
if (document.getElementById("ExtensionPopup")) return;
const extensionPopup = document.createElement("div");
extensionPopup.classList.add("outside-container", "hide");
extensionPopup.id = "ExtensionPopup";
const extensionContainer =
document.querySelector("#container") ?? document.getElementById("container");
const mountParent = extensionContainer ?? document.body;
mountParent.appendChild(extensionPopup);
new SettingsResizer();
const handler = extensionOutsideClickHandler(extensionPopup);
(extensionContainer ?? document.body).addEventListener("click", handler, false);
}
export function renderSettingsIfNeeded() { export function renderSettingsIfNeeded() {
if (isSettingsRendered) return; if (isSettingsRendered) return;
@@ -32,6 +32,12 @@ export function OpenWhatsNewPopup() {
const text = stringToHTML(/* html */ ` const text = stringToHTML(/* html */ `
<div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;"> <div class="whatsnewTextContainer" style="height: 50%;overflow-y: auto;">
<h1>3.5.3 - Adaptive theme updates</h1>
<li>Fixed adaptive theming on current-year course and assessment pages.</li>
<h1>3.5.2 - PDF & store compliance</h1>
<li>Put PDF.js with the extension so assessment weighting stays compatible with Chrome Web Store rules</li>
<h1>3.5.1 - QR & session link fix</h1> <h1>3.5.1 - QR & session link fix</h1>
<li>Fixed DesQTA Connect Mobile App QR generation on Chrome</li> <li>Fixed DesQTA Connect Mobile App QR generation on Chrome</li>
+8 -3
View File
@@ -2,12 +2,17 @@ import { getUserInfo } from "@/seqta/ui/AddBetterSEQTAElements";
/** /**
* Parses the current page from window.location.hash. * Parses the current page from window.location.hash.
* Returns { programme, metaclass } for /courses/SEMESTER/X:Y or /assessments/SEMESTER/X:Y, or null. * Supports both old and current URL formats, e.g.
* e.g. #?page=/courses/2023S/4804:11066 or #?page=/assessments/2023S/4621:10772 * /courses/SEMESTER/X:Y and /courses/X:Y
* /assessments/SEMESTER/X:Y and /assessments/X:Y
* e.g. #?page=/courses/2023S/4804:11066,
* #?page=/courses/4804:11066,
* #?page=/assessments/2023S/4621:10772,
* #?page=/assessments/4621:10772
*/ */
function parsePageContext(): { programme: number; metaclass: number } | null { function parsePageContext(): { programme: number; metaclass: number } | null {
const hash = window.location.hash || ""; const hash = window.location.hash || "";
const match = hash.match(/[?&]page=\/(courses|assessments)\/[^/]+\/(\d+):(\d+)/); const match = hash.match(/[?&]page=\/(courses|assessments)\/(?:[^/]+\/)?(\d+):(\d+)/);
if (!match) return null; if (!match) return null;
const programme = parseInt(match[2], 10); const programme = parseInt(match[2], 10);
const metaclass = parseInt(match[3], 10); const metaclass = parseInt(match[3], 10);
+4
View File
@@ -0,0 +1,4 @@
/** SEQTA Engage (React) uses a different shell from classic SEQTA Learn. */
export function isSeqtaEngageExperience(): boolean {
return document.title.includes("SEQTA Engage");
}
+10 -9
View File
@@ -9,10 +9,11 @@ import { renderSettingsIfNeeded } from "./Adders/AddExtensionSettings";
import { delay } from "./delay"; import { delay } from "./delay";
export function setupSettingsButton() { export function setupSettingsButton() {
var AddedSettings = document.getElementById("AddedSettings"); const AddedSettings = document.getElementById("AddedSettings");
var extensionPopup = document.getElementById("ExtensionPopup"); const extensionPopup = document.getElementById("ExtensionPopup");
if (!AddedSettings || !extensionPopup) return;
AddedSettings!.addEventListener("click", async () => { AddedSettings.addEventListener("click", async () => {
if (SettingsClicked) { if (SettingsClicked) {
closeExtensionPopup(extensionPopup as HTMLElement); closeExtensionPopup(extensionPopup as HTMLElement);
} else { } else {
@@ -23,20 +24,20 @@ export function setupSettingsButton() {
if (settingsState.animations) { if (settingsState.animations) {
animate(0, 1, { animate(0, 1, {
onUpdate: (progress) => { onUpdate: (progress) => {
extensionPopup!.style.opacity = progress.toString(); extensionPopup.style.opacity = progress.toString();
extensionPopup!.style.transform = `scale(${progress})`; extensionPopup.style.transform = `scale(${progress})`;
}, },
type: "spring", type: "spring",
stiffness: 280, stiffness: 280,
damping: 20, damping: 20,
}); });
} else { } else {
extensionPopup!.style.opacity = "1"; extensionPopup.style.opacity = "1";
extensionPopup!.style.transform = "scale(1)"; extensionPopup.style.transform = "scale(1)";
extensionPopup!.style.transition = extensionPopup.style.transition =
"opacity 0s linear, transform 0s linear"; "opacity 0s linear, transform 0s linear";
} }
extensionPopup!.classList.remove("hide"); extensionPopup.classList.remove("hide");
changeSettingsClicked(true); changeSettingsClicked(true);
} }
}); });
+18
View File
@@ -84,6 +84,24 @@ export default defineConfig(({ command }) => ({
settings: join(__dirname, "src", "interface", "index.html"), settings: join(__dirname, "src", "interface", "index.html"),
pageState: join(__dirname, "src", "pageState.js"), pageState: join(__dirname, "src", "pageState.js"),
}, },
output: {
assetFileNames(info) {
const labels = [
...(info.names ?? []),
...(info.originalFileNames ?? []),
].join(" ");
if (
labels.includes("pdf.worker.min") ||
labels.includes("pdf.worker.mjs")
) {
return "resources/pdfjs/pdf.worker.min.mjs";
}
if (labels.includes("legacy") && labels.includes("pdf.min")) {
return "resources/pdfjs/pdf.legacy.min.mjs";
}
return "assets/[name]-[hash][extname]";
},
},
onwarn(warning, warn) { onwarn(warning, warn) {
if (warning.code === "FILE_NAME_CONFLICT") return; if (warning.code === "FILE_NAME_CONFLICT") return;
warn(warning); warn(warning);