fix(themes): theme wallpapers dont apply on firefox

This commit is contained in:
2026-06-22 21:08:12 +09:30
parent 2be27299a5
commit 4b7aa8da75
50 changed files with 744 additions and 389 deletions
+2
View File
@@ -8,6 +8,7 @@ import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import pageState from "@/pageState.js?url";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { installSeqtaMenuColourPatch } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
import { installThemeImagePagePatch } from "@/seqta/utils/patchThemeImagesPageContext";
// Stylesheets
import injectedCSS from "@/css/injected.scss?inline";
@@ -18,6 +19,7 @@ export async function main() {
if (settingsState.onoff) {
injectPageState();
installSeqtaMenuColourPatch();
installThemeImagePagePatch();
// Rather permanent FIX for bug! -> this is a hack to get the injected.css file to have HMR in development mode as this import system is currently broken with crxjs
if (import.meta.env.MODE === "development") {
+2 -1
View File
@@ -13,13 +13,14 @@ 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 { verboseInfo } from "@/utils/verboseLog";
let LessonInterval: any;
let currentSelectedDate = new Date();
let loadingTimeout: any;
export async function loadHomePage() {
console.info("[BetterSEQTA+] Started Loading Home Page");
verboseInfo("[BetterSEQTA+] Started Loading Home Page");
currentSelectedDate = new Date();
+2 -1
View File
@@ -5,9 +5,10 @@ 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 { verboseInfo } from "@/utils/verboseLog";
export async function SendNewsPage() {
console.info("[BetterSEQTA+] Started Loading News Page");
verboseInfo("[BetterSEQTA+] Started Loading News Page");
document.title = "News ― SEQTA Learn";
await delay(10);
@@ -29,6 +29,7 @@ const OPTIONAL_UNSET_MEANS_DEFAULT_KEYS = [
"themeOfTheMonthLastSeenId",
"justupdated",
"devMode",
"verboseLogging",
"hideSensitiveContent",
"mockNotices",
"devGhReleaseVersionOverride",
+2 -1
View File
@@ -1,4 +1,5 @@
import { settingsState } from "./listeners/SettingsState";
import { verboseInfo } from "@/utils/verboseLog";
const STYLE_ID = "bsplus-menuitem-visibility";
@@ -28,7 +29,7 @@ export function applyMenuItemVisibility(): void {
)) {
if (config && !config.toggle) {
css += hideRule(menuItem);
console.info(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
verboseInfo(`[BetterSEQTA+] Hiding ${menuItem} menu item`);
}
}
@@ -0,0 +1,112 @@
/**
* Bridge theme CSS and decorative images into PAGE JavaScript context.
*/
import patchScript from "@/seqta/utils/themeImagePagePatch.js?url";
import { resolveExtensionAssetUrl } from "@/lib/extensionAssetUrl";
import { blobToBase64Data } from "@/plugins/built-in/themes/themeImageUrl";
const PAGE_PATCH_LOADER_ID = "bsplus-theme-image-page-patch-loader";
const BRIDGE_ID = "bsplus-theme-image-bridge";
const PAYLOAD_ID = "bsplus-theme-image-payload";
export type ThemePageSyncInput = {
images?: Array<{ variableName: string; blob: Blob }>;
customCss?: string;
previewCss?: string;
clear?: boolean;
clearPreview?: boolean;
};
export function installThemeImagePagePatch(): void {
if (document.getElementById(PAGE_PATCH_LOADER_ID)) return;
const script = document.createElement("script");
script.id = PAGE_PATCH_LOADER_ID;
script.src = resolveExtensionAssetUrl(patchScript);
script.addEventListener("load", () => script.remove());
(document.documentElement || document.head).appendChild(script);
}
function ensureBridgeElements(): void {
if (!document.getElementById(PAYLOAD_ID)) {
const payload = document.createElement("textarea");
payload.id = PAYLOAD_ID;
payload.hidden = true;
payload.setAttribute("aria-hidden", "true");
payload.tabIndex = -1;
document.documentElement.appendChild(payload);
}
if (!document.getElementById(BRIDGE_ID)) {
const bridge = document.createElement("div");
bridge.id = BRIDGE_ID;
bridge.hidden = true;
bridge.setAttribute("aria-hidden", "true");
document.documentElement.appendChild(bridge);
}
}
function bumpBridgeRevision(): void {
const bridge = document.getElementById(BRIDGE_ID);
if (!bridge) return;
const rev = Number(bridge.getAttribute("data-rev") || "0") + 1;
bridge.setAttribute("data-rev", String(rev));
}
function sendPayload(payload: Record<string, unknown>): void {
installThemeImagePagePatch();
ensureBridgeElements();
const payloadEl = document.getElementById(PAYLOAD_ID) as HTMLTextAreaElement;
payloadEl.value = JSON.stringify(payload);
bumpBridgeRevision();
}
export async function syncThemeToPage(input: ThemePageSyncInput): Promise<void> {
const payload: Record<string, unknown> = {};
if (input.clear) {
sendPayload({ clear: true });
return;
}
if (input.clearPreview) {
payload.clearPreview = true;
}
if (input.customCss !== undefined) {
payload.customCss = input.customCss;
}
if (input.previewCss !== undefined) {
payload.previewCss = input.previewCss;
}
if (input.images !== undefined) {
payload.images = await Promise.all(
input.images.map(async (image) => ({
variableName: image.variableName,
data: await blobToBase64Data(image.blob),
mime: image.blob.type || "image/png",
})),
);
}
if (Object.keys(payload).length === 0) return;
sendPayload(payload);
}
export function clearThemeInPage(): void {
sendPayload({ clear: true });
}
/** @deprecated Use clearThemeInPage */
export function clearThemeImagesInPage(): void {
clearThemeInPage();
}
/** @deprecated Use syncThemeToPage */
export async function syncThemeImagesToPage(
images: Array<{ variableName: string; blob: Blob }>,
): Promise<void> {
await syncThemeToPage({ images });
}
+1
View File
@@ -11,6 +11,7 @@
var TUTOR_COLOUR_PREF_PREFIX = "timetable.tutor.";
function log(event, detail) {
if (!document.documentElement.hasAttribute("data-bsplus-verbose-log")) return;
if (detail !== undefined) {
console.info(LOG, event, detail);
} else {
+210
View File
@@ -0,0 +1,210 @@
/**
* PAGE context only — theme CustomCSS and decorative image variables must live
* in page-origin stylesheets. Extension-injected #custom-theme cannot load
* page blob: URLs referenced via var() on Firefox.
*/
(function () {
if (window.__bsplusThemePagePatched) return;
window.__bsplusThemePagePatched = true;
var LOG = "[BetterSEQTA+] theme page:";
var BRIDGE_ID = "bsplus-theme-image-bridge";
var PAYLOAD_ID = "bsplus-theme-image-payload";
var IMAGES_STYLE_ID = "bsplus-theme-images";
var THEME_STYLE_ID = "custom-theme";
var PREVIEW_STYLE_ID = "custom-theme-preview";
var urlCache = {};
var state = {
customCss: "",
previewCss: "",
};
var headObserver = null;
function log(event, detail) {
if (!document.documentElement.hasAttribute("data-bsplus-verbose-log")) return;
if (detail !== undefined) {
console.info(LOG, event, detail);
} else {
console.info(LOG, event);
}
}
function base64ToBlob(base64, mime) {
var byteString = atob(base64);
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mime || "image/png" });
}
function releaseCachedUrls() {
for (var key in urlCache) {
if (!urlCache.hasOwnProperty(key)) continue;
try {
URL.revokeObjectURL(urlCache[key]);
} catch (e) {
// ignore
}
}
urlCache = {};
}
function ensureStyleElement(id) {
var style = document.getElementById(id);
if (!style) {
style = document.createElement("style");
style.id = id;
document.head.appendChild(style);
}
return style;
}
function ensureThemeStyleLast() {
var style = document.getElementById(THEME_STYLE_ID);
if (!style || !document.head.contains(style)) return;
if (document.head.lastElementChild === style) return;
document.head.appendChild(style);
}
function ensureHeadObserver() {
if (headObserver) return;
headObserver = new MutationObserver(function () {
ensureThemeStyleLast();
});
headObserver.observe(document.head, { childList: true });
}
function clearAll() {
releaseCachedUrls();
state.customCss = "";
state.previewCss = "";
var imagesStyle = document.getElementById(IMAGES_STYLE_ID);
if (imagesStyle) imagesStyle.textContent = "";
var themeStyle = document.getElementById(THEME_STYLE_ID);
if (themeStyle) themeStyle.remove();
var previewStyle = document.getElementById(PREVIEW_STYLE_ID);
if (previewStyle) previewStyle.remove();
if (headObserver) {
headObserver.disconnect();
headObserver = null;
}
log("cleared");
}
function applyThemeImages(images) {
releaseCachedUrls();
if (!images || !images.length) {
var emptyStyle = document.getElementById(IMAGES_STYLE_ID);
if (emptyStyle) emptyStyle.textContent = "";
return;
}
var lines = [":root {"];
for (var i = 0; i < images.length; i++) {
var img = images[i];
if (!img || !img.variableName || !img.data) continue;
try {
var blob = base64ToBlob(img.data, img.mime);
var url = URL.createObjectURL(blob);
urlCache[img.variableName] = url;
lines.push(" --" + img.variableName + ": url(\"" + url + "\");");
} catch (e) {
console.warn(LOG, "skip image", img.variableName, e);
}
}
lines.push("}");
ensureStyleElement(IMAGES_STYLE_ID).textContent = lines.join("\n");
log("images applied", { count: images.length });
}
function applyCustomCss(css) {
if (!css) {
var existing = document.getElementById(THEME_STYLE_ID);
if (existing) existing.remove();
return;
}
ensureStyleElement(THEME_STYLE_ID).textContent = css;
ensureHeadObserver();
ensureThemeStyleLast();
}
function applyPreviewCss(css) {
if (!css) {
var existing = document.getElementById(PREVIEW_STYLE_ID);
if (existing) existing.remove();
return;
}
ensureStyleElement(PREVIEW_STYLE_ID).textContent = css;
}
function processPayload() {
var payloadEl = document.getElementById(PAYLOAD_ID);
if (!payloadEl) return;
var raw = payloadEl.value;
if (!raw) {
clearAll();
return;
}
try {
var payload = JSON.parse(raw);
if (!payload || payload.clear) {
clearAll();
return;
}
if (payload.images !== undefined) {
applyThemeImages(payload.images);
}
if (payload.customCss !== undefined) {
state.customCss = payload.customCss || "";
applyCustomCss(state.customCss);
log("custom css applied");
}
if (payload.previewCss !== undefined) {
state.previewCss = payload.previewCss || "";
applyPreviewCss(state.previewCss);
}
if (payload.clearPreview) {
state.previewCss = "";
applyPreviewCss("");
}
} catch (e) {
console.warn(LOG, "invalid payload", e);
}
}
function ensureBridge() {
if (!document.getElementById(PAYLOAD_ID)) {
var payload = document.createElement("textarea");
payload.id = PAYLOAD_ID;
payload.hidden = true;
payload.setAttribute("aria-hidden", "true");
payload.tabIndex = -1;
document.documentElement.appendChild(payload);
}
if (!document.getElementById(BRIDGE_ID)) {
var bridge = document.createElement("div");
bridge.id = BRIDGE_ID;
bridge.hidden = true;
bridge.setAttribute("aria-hidden", "true");
document.documentElement.appendChild(bridge);
}
}
ensureBridge();
var bridge = document.getElementById(BRIDGE_ID);
new MutationObserver(function () {
processPayload();
}).observe(bridge, {
attributes: true,
attributeFilter: ["data-rev"],
});
processPayload();
log("patch active");
})();
+2 -1
View File
@@ -5,6 +5,7 @@
*/
import { dismissStaleColourDialogs } from "@/seqta/utils/patchSeqtaMenuUpdateColours";
import { verboseInfo } from "@/utils/verboseLog";
let attached = false;
let dismissTimer: ReturnType<typeof setTimeout> | null = null;
@@ -83,7 +84,7 @@ export function attachTimetableColorisRecovery(): void {
if (!pickerOpen) {
const result = dismissTimetableUiBlockers();
if (result.slideRemoved > 0 || result.modalRemoved > 0) {
console.info(
verboseInfo(
"[BetterSEQTA+] timetable colour: content-script cleanup",
result,
);